diff --git a/control.lua b/control.lua
index 7a53e789..3b4fbc77 100644
--- a/control.lua
+++ b/control.lua
@@ -1,4 +1,4 @@
---[[ not_luadoc=true
+-- not_luadoc=true
function _log(...) log(...) end -- do not remove this is used for smaller verbose lines
Manager = require("FactorioSoftmodManager")
Manager.setVerbose{
@@ -12,15 +12,16 @@ Manager.setVerbose{
output=Manager._verbose -- can be: can be: print || log || other function
}
Manager() -- can be Manager.loadModules() if called else where
-]]
+
+--[[
+require 'utils.data_stages'
local Container = require 'container'
Container.handlers = {
- --event
- --global
+ Event='utils.event',
+ Global='utils.global',
error=error,
logging=function(...) log(...) end,
- --debug
tableToString=serpent.line
}
Container.loadHandlers()
@@ -28,3 +29,4 @@ Container.files = {
'modules.test'
}
Container.loadFiles()
+]]
\ No newline at end of file
diff --git a/modules/test.lua b/modules/test.lua
index 8c15a328..29814085 100644
--- a/modules/test.lua
+++ b/modules/test.lua
@@ -1,3 +1,7 @@
function thisIsATestFunction(...)
game.print(serpent.line({...}))
-end
\ No newline at end of file
+end
+
+Event.add(defines.events.on_console_chat,function(event)
+ if event.player_index then game.print('Message: '..event.message) end
+end)
\ No newline at end of file
diff --git a/utils/data_stages.lua b/utils/data_stages.lua
new file mode 100644
index 00000000..406cd4df
--- /dev/null
+++ b/utils/data_stages.lua
@@ -0,0 +1,13 @@
+-- Info on the data lifecycle and how we use it: https://github.com/Refactorio/RedMew/wiki/The-data-lifecycle
+-- Non-applicable stages are commented out.
+_STAGE = {
+ --settings = 1,
+ --data = 2,
+ --migration = 3,
+ control = 4,
+ init = 5,
+ load = 6,
+ --config_change = 7,
+ runtime = 8
+}
+_LIFECYCLE = _STAGE.control
\ No newline at end of file
diff --git a/utils/debug.lua b/utils/debug.lua
new file mode 100644
index 00000000..1c1a3633
--- /dev/null
+++ b/utils/debug.lua
@@ -0,0 +1,164 @@
+-- localised functions
+local format = string.format
+local match = string.match
+local gsub = string.gsub
+local serialize = serpent.line
+local debug_getupvalue = debug.getupvalue
+
+-- this
+local Debug = {}
+
+global.debug_message_count = 0
+
+---@return number next index
+local function increment()
+ local next = global.debug_message_count + 1
+ global.debug_message_count = next
+
+ return next
+end
+
+--- Takes the table output from debug.getinfo and pretties it
+local function cleanup_debug(debug_table)
+ local short_src = match(debug_table.source, '/[^/]*/[^/]*$')
+ -- require will not return a valid string so short_src may be nil here
+ if short_src then
+ short_src = gsub(short_src, '%.lua', '')
+ end
+
+ return format('[function: %s file: %s line number: %s]', debug_table.name, short_src, debug_table.currentline)
+end
+
+---Shows the given message if debug is enabled. Uses serpent to print non scalars.
+-- @param message
+-- @param stack_traceback levels of stack trace to give, defaults to 1 level if nil
+function Debug.print(message, trace_levels)
+ if not _DEBUG then
+ return
+ end
+
+ if not trace_levels then
+ trace_levels = 2
+ else
+ trace_levels = trace_levels + 1
+ end
+
+ local traceback_string = ''
+ if type(message) ~= 'string' and type(message) ~= 'number' and type(message) ~= 'boolean' then
+ message = serialize(message)
+ end
+
+ message = format('[%d] %s', increment(), tostring(message))
+
+ if trace_levels >= 2 then
+ for i = 2, trace_levels do
+ local debug_table = debug.getinfo(i)
+ if debug_table then
+ traceback_string = format('%s -> %s', traceback_string, cleanup_debug(debug_table))
+ else
+ break
+ end
+ end
+ message = format('%s - Traceback%s', message, traceback_string)
+ end
+
+ if _LIFECYCLE == _STAGE.runtime then
+ game.print(message)
+ end
+ log(message)
+end
+
+local function get(obj, prop)
+ return obj[prop]
+end
+
+local function get_lua_object_type_safe(obj)
+ local s, r = pcall(get, obj, 'help')
+
+ if not s then
+ return
+ end
+
+ return r():match('Lua%a+')
+end
+
+--- Returns the value of the key inside the object
+-- or 'InvalidLuaObject' if the LuaObject is invalid.
+-- or 'InvalidLuaObjectKey' if the LuaObject does not have an entry at that key
+-- @param object LuaObject or metatable
+-- @param key
+-- @return
+function Debug.get_meta_value(object, key)
+ if Debug.object_type(object) == 'InvalidLuaObject' then
+ return 'InvalidLuaObject'
+ end
+
+ local suc, value = pcall(get, object, key)
+ if not suc then
+ return 'InvalidLuaObjectKey'
+ end
+
+ return value
+end
+
+--- Returns the Lua data type or the factorio LuaObject type
+-- or 'NoHelpLuaObject' if the LuaObject does not have a help function
+-- or 'InvalidLuaObject' if the LuaObject is invalid.
+-- @param object
+-- @return string
+function Debug.object_type(object)
+ local obj_type = type(object)
+
+ if obj_type ~= 'table' or type(object.__self) ~= 'userdata' then
+ return obj_type
+ end
+
+ local suc, valid = pcall(get, object, 'valid')
+ if not suc then
+ -- no 'valid' property
+ return get_lua_object_type_safe(object) or 'NoHelpLuaObject'
+ end
+
+ if not valid then
+ return 'InvalidLuaObject'
+ else
+ return get_lua_object_type_safe(object) or 'NoHelpLuaObject'
+ end
+end
+
+---Shows the given message if debug is on.
+---@param position Position
+---@param message string
+function Debug.print_position(position, message)
+ Debug.print(format('%s %s', serialize(position), message))
+end
+
+---Executes the given callback if cheating is enabled.
+---@param callback function
+function Debug.cheat(callback)
+ if _CHEATS then
+ callback()
+ end
+end
+
+--- Returns true if the function is a closure, false otherwise.
+-- A closure is a function that contains 'upvalues' or in other words
+-- has a reference to a local variable defined outside the function's scope.
+-- @param func
+-- @return boolean
+function Debug.is_closure(func)
+ local i = 1
+ while true do
+ local n = debug_getupvalue(func, i)
+
+ if n == nil then
+ return false
+ elseif n ~= '_ENV' then
+ return true
+ end
+
+ i = i + 1
+ end
+end
+
+return Debug
diff --git a/utils/event.lua b/utils/event.lua
new file mode 100644
index 00000000..e183bb2c
--- /dev/null
+++ b/utils/event.lua
@@ -0,0 +1,457 @@
+--- This Module allows for registering multiple handlers to the same event, overcoming the limitation of script.register.
+--
+-- ** Event.add(event_name, handler) **
+--
+-- Handlers added with Event.add must be added at the control stage or in Event.on_init or Event.on_load.
+-- Remember that for each player, on_init or on_load is run, never both. So if you can't add the handler in the
+-- control stage add the handler in both on_init and on_load.
+-- Handlers added with Event.add cannot be removed.
+-- For handlers that need to be removed or added at runtime use Event.add_removable.
+-- @usage
+-- local Event = require 'utils.event'
+-- Event.add(
+-- defines.events.on_built_entity,
+-- function(event)
+-- game.print(serpent.block(event)) -- prints the content of the event table to console.
+-- end
+-- )
+--
+-- ** Event.add_removable(event_name, token) **
+--
+-- For conditional event handlers. Event.add_removable can be safely called at runtime without desync risk.
+-- Only use this if you need to add the handler at runtime or need to remove the handler, otherwise use Event.add
+--
+-- Event.add_removable can be safely used at the control stage or in Event.on_init. If used in on_init you don't
+-- need to also add in on_load (unlike Event.add).
+-- Event.add_removable cannot be called in on_load, doing so will crash the game on loading.
+-- Token is used because it's a desync risk to store closures inside the global table.
+--
+-- @usage
+-- local Token = require 'utils.token'
+-- local Event = require 'utils.event'
+--
+-- Token.register must not be called inside an event handler.
+-- local handler =
+-- Token.register(
+-- function(event)
+-- game.print(serpent.block(event)) -- prints the content of the event table to console.
+-- end
+-- )
+--
+-- The below code would typically be inside another event or a custom command.
+-- Event.add_removable(defines.events.on_built_entity, handler)
+--
+-- When you no longer need the handler.
+-- Event.remove_removable(defines.events.on_built_entity, handler)
+--
+-- It's not an error to register the same token multiple times to the same event, however when
+-- removing only the first occurrence is removed.
+--
+-- ** Event.add_removable_function(event_name, func) **
+--
+-- Only use this function if you can't use Event.add_removable. i.e you are registering the handler at the console.
+-- The same restrictions that apply to Event.add_removable also apply to Event.add_removable_function.
+-- func cannot be a closure in this case, as there is no safe way to store closures in the global table.
+-- A closure is a function that uses a local variable not defined in the function.
+--
+-- @usage
+-- local Event = require 'utils.event'
+--
+-- If you want to remove the handler you will need to keep a reference to it.
+-- global.handler = function(event)
+-- game.print(serpent.block(event)) -- prints the content of the event table to console.
+-- end
+--
+-- The below code would typically be used at the command console.
+-- Event.add_removable_function(defines.events.on_built_entity, global.handler)
+--
+-- When you no longer need the handler.
+-- Event.remove_removable_function(defines.events.on_built_entity, global.handler)
+--
+-- ** Other Events **
+--
+-- Use Event.on_init(handler) for script.on_init(handler)
+-- Use Event.on_load(handler) for script.on_load(handler)
+--
+-- Use Event.on_nth_tick(tick, handler) for script.on_nth_tick(tick, handler)
+-- Favour this event over Event.add(defines.events.on_tick, handler)
+-- There are also Event.add_removable_nth_tick(tick, token) and Event.add_removable_nth_tick_function(tick, func)
+-- That work the same as above.
+--
+-- ** Custom Scenario Events **
+--
+-- local Event = require 'utils.event'
+--
+-- local event_id = script.generate_event_name()
+--
+-- Event.add(
+-- event_id,
+-- function(event)
+-- game.print(serpent.block(event)) -- prints the content of the event table to console.
+-- end
+-- )
+--
+-- The table contains extra information that you want to pass to the handler.
+-- script.raise_event(event_id, {extra = 'data'})
+
+local EventCore = require 'utils.event_core'
+local Global = require 'utils.global'
+local Token = require 'utils.token'
+local Debug = require 'utils.debug'
+
+local table_remove = table.remove
+local core_add = EventCore.add
+local core_on_init = EventCore.on_init
+local core_on_load = EventCore.on_load
+local core_on_nth_tick = EventCore.on_nth_tick
+local stage_load = _STAGE.load
+local script_on_event = script.on_event
+local script_on_nth_tick = script.on_nth_tick
+
+local Event = {}
+
+local handlers_added = false -- set to true after the removeable event handlers have been added.
+
+local event_handlers = EventCore.get_event_handlers()
+local on_nth_tick_event_handlers = EventCore.get_on_nth_tick_event_handlers()
+
+local token_handlers = {}
+local token_nth_tick_handlers = {}
+local function_handlers = {}
+local function_nth_tick_handlers = {}
+
+Global.register(
+ {
+ token_handlers = token_handlers,
+ token_nth_tick_handlers = token_nth_tick_handlers,
+ function_handlers = function_handlers,
+ function_nth_tick_handlers = function_nth_tick_handlers
+ },
+ function(tbl)
+ token_handlers = tbl.token_handlers
+ token_nth_tick_handlers = tbl.token_nth_tick_handlers
+ function_handlers = tbl.function_handlers
+ function_nth_tick_handlers = tbl.function_nth_tick_handlers
+ end
+)
+
+local function remove(tbl, handler)
+ if tbl == nil then
+ return
+ end
+
+ -- the handler we are looking for is more likly to be at the back of the array.
+ for i = #tbl, 1, -1 do
+ if tbl[i] == handler then
+ table_remove(tbl, i)
+ break
+ end
+ end
+end
+
+--- Register a handler for the event_name event.
+-- This function must be called in the control stage or in Event.on_init or Event.on_load.
+-- See documentation at top of file for details on using events.
+-- @param event_name
+-- @param handler
+function Event.add(event_name, handler)
+ if _LIFECYCLE == 8 then
+ error('Calling Event.add after on_init() or on_load() has run is a desync risk.', 2)
+ end
+
+ core_add(event_name, handler)
+end
+
+--- Register a handler for the script.on_init event.
+-- This function must be called in the control stage or in Event.on_init or Event.on_load
+-- See documentation at top of file for details on using events.
+-- @param handler
+function Event.on_init(handler)
+ if _LIFECYCLE == 8 then
+ error('Calling Event.on_init after on_init() or on_load() has run is a desync risk.', 2)
+ end
+
+ core_on_init(handler)
+end
+
+--- Register a handler for the script.on_load event.
+-- This function must be called in the control stage or in Event.on_init or Event.on_load
+-- See documentation at top of file for details on using events.
+-- @param handler
+function Event.on_load(handler)
+ if _LIFECYCLE == 8 then
+ error('Calling Event.on_load after on_init() or on_load() has run is a desync risk.', 2)
+ end
+
+ core_on_load(handler)
+end
+
+--- Register a handler for the nth_tick event.
+-- This function must be called in the control stage or in Event.on_init or Event.on_load.
+-- See documentation at top of file for details on using events.
+-- @param tick The handler will be called every nth tick
+-- @param handler
+function Event.on_nth_tick(tick, handler)
+ if _LIFECYCLE == 8 then
+ error('Calling Event.on_nth_tick after on_init() or on_load() has run is a desync risk.', 2)
+ end
+
+ core_on_nth_tick(tick, handler)
+end
+
+--- Register a token handler that can be safely added and removed at runtime.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param event_name
+-- @param token
+function Event.add_removable(event_name, token)
+ if type(token) ~= 'number' then
+ error('token must be a number', 2)
+ end
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+
+ local tokens = token_handlers[event_name]
+ if not tokens then
+ token_handlers[event_name] = {token}
+ else
+ tokens[#tokens + 1] = token
+ end
+
+ if handlers_added then
+ local handler = Token.get(token)
+ core_add(event_name, handler)
+ end
+end
+
+--- Removes a token handler for the given event_name.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param event_name
+-- @param token
+function Event.remove_removable(event_name, token)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ local tokens = token_handlers[event_name]
+
+ if not tokens then
+ return
+ end
+
+ local handler = Token.get(token)
+ local handlers = event_handlers[event_name]
+
+ remove(tokens, token)
+ remove(handlers, handler)
+
+ if #handlers == 0 then
+ script_on_event(event_name, nil)
+ end
+end
+
+--- Register a handler that can be safely added and removed at runtime.
+-- The handler must not be a closure, as that is a desync risk.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param event_name
+-- @param func
+function Event.add_removable_function(event_name, func)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ if type(func) ~= 'function' then
+ error('func must be a function', 2)
+ end
+
+ if Debug.is_closure(func) then
+ error(
+ 'func cannot be a closure as that is a desync risk. Consider using Event.add_removable(event_name, token) instead.',
+ 2
+ )
+ end
+
+ local funcs = function_handlers[event_name]
+ if not funcs then
+ function_handlers[event_name] = {func}
+ else
+ funcs[#funcs + 1] = func
+ end
+
+ if handlers_added then
+ core_add(event_name, func)
+ end
+end
+
+--- Removes a handler for the given event_name.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param event_name
+-- @param func
+function Event.remove_removable_function(event_name, func)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ local funcs = function_handlers[event_name]
+
+ if not funcs then
+ return
+ end
+
+ local handlers = event_handlers[event_name]
+
+ remove(funcs, func)
+ remove(handlers, func)
+
+ if #handlers == 0 then
+ script_on_event(event_name, nil)
+ end
+end
+
+--- Register a token handler for the nth tick that can be safely added and removed at runtime.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param tick
+-- @param token
+function Event.add_removable_nth_tick(tick, token)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ if type(token) ~= 'number' then
+ error('token must be a number', 2)
+ end
+
+ local tokens = token_nth_tick_handlers[tick]
+ if not tokens then
+ token_nth_tick_handlers[tick] = {token}
+ else
+ tokens[#tokens + 1] = token
+ end
+
+ if handlers_added then
+ local handler = Token.get(token)
+ core_on_nth_tick(tick, handler)
+ end
+end
+
+--- Removes a token handler for the nth tick.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param tick
+-- @param token
+function Event.remove_removable_nth_tick(tick, token)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ local tokens = token_nth_tick_handlers[tick]
+
+ if not tokens then
+ return
+ end
+
+ local handler = Token.get(token)
+ local handlers = on_nth_tick_event_handlers[tick]
+
+ remove(tokens, token)
+ remove(handlers, handler)
+
+ if #handlers == 0 then
+ script_on_nth_tick(tick, nil)
+ end
+end
+
+--- Register a handler for the nth tick that can be safely added and removed at runtime.
+-- The handler must not be a closure, as that is a desync risk.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param tick
+-- @param func
+function Event.add_removable_nth_tick_function(tick, func)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ if type(func) ~= 'function' then
+ error('func must be a function', 2)
+ end
+
+ if Debug.is_closure(func) then
+ error(
+ 'func cannot be a closure as that is a desync risk. Consider using Event.add_removable_nth_tick(tick, token) instead.',
+ 2
+ )
+ end
+
+ local funcs = function_nth_tick_handlers[tick]
+ if not funcs then
+ function_nth_tick_handlers[tick] = {func}
+ else
+ funcs[#funcs + 1] = func
+ end
+
+ if handlers_added then
+ core_on_nth_tick(tick, func)
+ end
+end
+
+--- Removes a handler for the nth tick.
+-- Do NOT call this method during on_load.
+-- See documentation at top of file for details on using events.
+-- @param tick
+-- @param func
+function Event.remove_removable_nth_tick_function(tick, func)
+ if _LIFECYCLE == stage_load then
+ error('cannot call during on_load', 2)
+ end
+ local funcs = function_nth_tick_handlers[tick]
+
+ if not funcs then
+ return
+ end
+
+ local handlers = on_nth_tick_event_handlers[tick]
+
+ remove(funcs, func)
+ remove(handlers, func)
+
+ if #handlers == 0 then
+ script_on_nth_tick(tick, nil)
+ end
+end
+
+local function add_handlers()
+ for event_name, tokens in pairs(token_handlers) do
+ for i = 1, #tokens do
+ local handler = Token.get(tokens[i])
+ core_add(event_name, handler)
+ end
+ end
+
+ for event_name, funcs in pairs(function_handlers) do
+ for i = 1, #funcs do
+ local handler = funcs[i]
+ core_add(event_name, handler)
+ end
+ end
+
+ for tick, tokens in pairs(token_nth_tick_handlers) do
+ for i = 1, #tokens do
+ local handler = Token.get(tokens[i])
+ core_on_nth_tick(tick, handler)
+ end
+ end
+
+ for tick, funcs in pairs(function_nth_tick_handlers) do
+ for i = 1, #funcs do
+ local handler = funcs[i]
+ core_on_nth_tick(tick, handler)
+ end
+ end
+
+ handlers_added = true
+end
+
+core_on_init(add_handlers)
+core_on_load(add_handlers)
+
+return Event
diff --git a/utils/event_core.lua b/utils/event_core.lua
new file mode 100644
index 00000000..6ea0dac0
--- /dev/null
+++ b/utils/event_core.lua
@@ -0,0 +1,132 @@
+-- This module exists to break the circular dependency between event.lua and global.lua.
+-- It is not expected that any user code would require this module instead event.lua should be required.
+
+local Public = {}
+
+local init_event_name = -1
+local load_event_name = -2
+
+-- map of event_name to handlers[]
+local event_handlers = {}
+-- map of nth_tick to handlers[]
+local on_nth_tick_event_handlers = {}
+
+local pcall = pcall
+local log = log
+local script_on_event = script.on_event
+local script_on_nth_tick = script.on_nth_tick
+
+local function call_handlers(handlers, event)
+ if _DEBUG then
+ for i = 1, #handlers do
+ local handler = handlers[i]
+ handler(event)
+ end
+ else
+ for i = 1, #handlers do
+ local handler = handlers[i]
+ local success, error = pcall(handler, event)
+ if not success then
+ log(error)
+ end
+ end
+ end
+end
+
+local function on_event(event)
+ local handlers = event_handlers[event.name]
+ call_handlers(handlers, event)
+end
+
+local function on_init()
+ _LIFECYCLE = 5 -- on_init
+ local handlers = event_handlers[init_event_name]
+ call_handlers(handlers)
+
+ event_handlers[init_event_name] = nil
+ event_handlers[load_event_name] = nil
+
+ _LIFECYCLE = 8 -- Runtime
+end
+
+local function on_load()
+ _LIFECYCLE = 6 -- on_load
+ local handlers = event_handlers[load_event_name]
+ call_handlers(handlers)
+
+ event_handlers[init_event_name] = nil
+ event_handlers[load_event_name] = nil
+
+ _LIFECYCLE = 8 -- Runtime
+end
+
+local function on_nth_tick_event(event)
+ local handlers = on_nth_tick_event_handlers[event.nth_tick]
+ call_handlers(handlers, event)
+end
+
+--- Do not use this function, use Event.add instead as it has safety checks.
+function Public.add(event_name, handler)
+ local handlers = event_handlers[event_name]
+ if not handlers then
+ event_handlers[event_name] = {handler}
+ script_on_event(event_name, on_event)
+ else
+ table.insert(handlers, handler)
+ if #handlers == 1 then
+ script_on_event(event_name, on_event)
+ end
+ end
+end
+
+--- Do not use this function, use Event.on_init instead as it has safety checks.
+function Public.on_init(handler)
+ local handlers = event_handlers[init_event_name]
+ if not handlers then
+ event_handlers[init_event_name] = {handler}
+ script.on_init(on_init)
+ else
+ table.insert(handlers, handler)
+ if #handlers == 1 then
+ script.on_init(on_init)
+ end
+ end
+end
+
+--- Do not use this function, use Event.on_load instead as it has safety checks.
+function Public.on_load(handler)
+ local handlers = event_handlers[load_event_name]
+ if not handlers then
+ event_handlers[load_event_name] = {handler}
+ script.on_load(on_load)
+ else
+ table.insert(handlers, handler)
+ if #handlers == 1 then
+ script.on_load(on_load)
+ end
+ end
+end
+
+--- Do not use this function, use Event.on_nth_tick instead as it has safety checks.
+function Public.on_nth_tick(tick, handler)
+ local handlers = on_nth_tick_event_handlers[tick]
+ if not handlers then
+ on_nth_tick_event_handlers[tick] = {handler}
+ script_on_nth_tick(tick, on_nth_tick_event)
+ else
+ table.insert(handlers, handler)
+ if #handlers == 1 then
+ script_on_nth_tick(tick, on_nth_tick_event)
+ end
+ end
+end
+
+function Public.get_event_handlers()
+ return event_handlers
+end
+
+function Public.get_on_nth_tick_event_handlers()
+ return on_nth_tick_event_handlers
+end
+
+return Public
diff --git a/utils/global.lua b/utils/global.lua
new file mode 100644
index 00000000..4d23bd43
--- /dev/null
+++ b/utils/global.lua
@@ -0,0 +1,79 @@
+local Event = require 'utils.event_core'
+local Token = require 'utils.token'
+
+local Global = {}
+
+function Global.register(tbl, callback)
+ if _LIFECYCLE ~= _STAGE.control then
+ error('can only be called during the control stage', 2)
+ end
+ local token = Token.register_global(tbl)
+
+ Event.on_load(
+ function()
+ callback(Token.get_global(token))
+ end
+ )
+end
+
+function Global.register_init(tbl, init_handler, callback)
+ if _LIFECYCLE ~= _STAGE.control then
+ error('can only be called during the control stage', 2)
+ end
+ local token = Token.register_global(tbl)
+
+ Event.on_init(
+ function()
+ init_handler(tbl)
+ callback(tbl)
+ end
+ )
+
+ Event.on_load(
+ function()
+ callback(Token.get_global(token))
+ end
+ )
+end
+
+if _DEBUG then
+ local concat = table.concat
+
+ local names = {}
+ Global.names = names
+
+ function Global.register(tbl, callback)
+ local filepath = debug.getinfo(2, 'S').source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
+ local token = Token.register_global(tbl)
+
+ names[token] = concat {token, ' - ', filepath}
+
+ Event.on_load(
+ function()
+ callback(Token.get_global(token))
+ end
+ )
+ end
+
+ function Global.register_init(tbl, init_handler, callback)
+ local filepath = debug.getinfo(2, 'S').source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
+ local token = Token.register_global(tbl)
+
+ names[token] = concat {token, ' - ', filepath}
+
+ Event.on_init(
+ function()
+ init_handler(tbl)
+ callback(tbl)
+ end
+ )
+
+ Event.on_load(
+ function()
+ callback(Token.get_global(token))
+ end
+ )
+ end
+end
+
+return Global
diff --git a/utils/token.lua b/utils/token.lua
new file mode 100644
index 00000000..f9d511b7
--- /dev/null
+++ b/utils/token.lua
@@ -0,0 +1,55 @@
+local Token = {}
+
+local tokens = {}
+
+local counter = 0
+
+--- Assigns a unquie id for the given var.
+-- This function cannot be called after on_init() or on_load() has run as that is a desync risk.
+-- Typically this is used to register functions, so the id can be stored in the global table
+-- instead of the function. This is becasue closures cannot be safely stored in the global table.
+-- @param var
+-- @return number the unique token for the variable.
+function Token.register(var)
+ if _LIFECYCLE == 8 then
+ error('Calling Token.register after on_init() or on_load() has run is a desync risk.', 2)
+ end
+
+ counter = counter + 1
+
+ tokens[counter] = var
+
+ return counter
+end
+
+function Token.get(token_id)
+ return tokens[token_id]
+end
+
+global.tokens = {}
+
+function Token.register_global(var)
+ local c = #global.tokens + 1
+
+ global.tokens[c] = var
+
+ return c
+end
+
+function Token.get_global(token_id)
+ return global.tokens[token_id]
+end
+
+function Token.set_global(token_id, var)
+ global.tokens[token_id] = var
+end
+
+local uid_counter = 0
+
+function Token.uid()
+ uid_counter = uid_counter + 1
+
+ return uid_counter
+end
+
+return Token