From d1f2eb5686d03e44a48acd892aa1d74684e5e4d4 Mon Sep 17 00:00:00 2001 From: grilledham Date: Fri, 28 Dec 2018 16:21:45 +0000 Subject: [PATCH] Event (#565) * First pass at removable events * added documentation * added is_closure function * fixed return multiple values * is_closure now ignores _ENV --- utils/debug.lua | 21 ++ utils/event.lua | 497 +++++++++++++++++++++++++++++++++---------- utils/event_core.lua | 107 ++++++++++ utils/global.lua | 2 +- utils/token.lua | 12 ++ 5 files changed, 522 insertions(+), 117 deletions(-) create mode 100644 utils/event_core.lua diff --git a/utils/debug.lua b/utils/debug.lua index aee24706..8f169419 100644 --- a/utils/debug.lua +++ b/utils/debug.lua @@ -1,6 +1,7 @@ -- dependencies local format = string.format local serialize = serpent.line +local debug_getupvalue = debug.getupvalue -- this local Debug = {} @@ -46,4 +47,24 @@ function Debug.cheat(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 index aba7ddaa..ee6c97fc 100644 --- a/utils/event.lua +++ b/utils/event.lua @@ -1,122 +1,220 @@ +--- 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, other wise use Event.add +-- 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 occurance 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. +-- 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 Event = {} -local init_event_name = -1 -local load_event_name = -2 +local event_handlers = EventCore.get_event_handlers() +local on_nth_tick_event_handlers = EventCore.get_on_nth_tick_event_handlers() -local control_stage = true +local token_handlers = {} +local token_nth_tick_handlers = {} +local function_handlers = {} +local function_nth_tick_handlers = {} --- map of event_name to handlers[] -local event_handlers = {} --- map of nth_tick to handlers[] -local on_nth_tick_event_handlers = {} - -local function call_handlers(handlers, event) - if _DEBUG then - for _, handler in ipairs(handlers) do - handler(event) - end - else - for _, handler in ipairs(handlers) do - local success, error = pcall(handler, event) - if not success then - log(error) - end - end +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 -end +) -local function on_event(event) - local handlers = event_handlers[event.name] - call_handlers(handlers, event) -end - -local function on_init() - local handlers = event_handlers[init_event_name] - call_handlers(handlers) -end - -local function on_load() - local handlers = event_handlers[load_event_name] - call_handlers(handlers) -end - -local function on_nth_tick_event(event) - local handlers = on_nth_tick_event_handlers[event.nth_tick] - call_handlers(handlers, event) -end - -function Event.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) - end -end - -function Event.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) - end -end - -function Event.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) - end -end - -function Event.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) - end -end - -local Token = require 'utils.token' -global.event_tokens = {} - -function Event.add_removable(event_name, token) - local event_tokens = global.event_tokens - - local tokens = event_tokens[event_name] - if not tokens then - event_tokens[event_name] = {token} - else - table.insert(tokens, token) - end - - if not control_stage then - local handler = Token.get(token) - Event.add(event_name, handler) - end -end - -local function remove(t, e) - for i, v in ipairs(t) do - if v == e then - table.remove(t, i) +local function remove(tbl, handler) + -- 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 -function Event.remove_removable(event_name, token) - local event_tokens = global.event_tokens +--- 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 EventCore.runtime then + error('Calling Event.add after on_init() or on_load() has run is a desync risk.', 2) + end - local tokens = event_tokens[event_name] + 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 EventCore.runtime 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 EventCore.runtime 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 EventCore.runtime 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. +-- 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 + + local tokens = token_handlers[event_name] + if not tokens then + token_handlers[event_name] = {token} + else + tokens[#tokens + 1] = token + end + + -- If this is called before runtime, we don't need to add the handlers + -- as they will be added later either in on_init or on_load. + if EventCore.runtime then + local handler = Token.get(token) + core_add(event_name, handler) + end +end + +--- Removes a token handler for the given event_name. +-- See documentation at top of file for details on using events. +-- @param event_name +-- @param token +function Event.remove_removable(event_name, token) + local tokens = token_handlers[event_name] if not tokens then return @@ -133,20 +231,187 @@ function Event.remove_removable(event_name, token) end end -local function add_token_handlers() - control_stage = false +--- 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. +-- 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 type(func) ~= 'function' then + error('func must be a function', 2) + end - local event_tokens = global.event_tokens + 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 - for event_name, tokens in pairs(event_tokens) do - for _, token in ipairs(tokens) do - local handler = Token.get(token) - Event.add(event_name, handler) + local funcs = function_handlers[event_name] + if not funcs then + function_handlers[event_name] = {func} + else + funcs[#funcs + 1] = func + end + + -- If this is called before runtime, we don't need to add the handlers + -- as they will be added later either in on_init or on_load. + if EventCore.runtime then + core_add(event_name, func) + end +end + +--- Removes a handler for the given event_name. +-- See documentation at top of file for details on using events. +-- @param event_name +-- @param func +function Event.remove_removable_function(event_name, func) + 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. +-- See documentation at top of file for details on using events. +-- @param event_name +-- @param token +function Event.add_removable_nth_tick(tick, token) + 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 this is called before runtime, we don't need to add the handlers + -- as they will be added later either in on_init or on_load. + if EventCore.runtime then + local handler = Token.get(token) + core_on_nth_tick(tick, handler) + end +end + +--- Removes a token handler for the nth tick. +-- See documentation at top of file for details on using events. +-- @param event_name +-- @param token +function Event.remove_removable_nth_tick(tick, token) + 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. +-- See documentation at top of file for details on using events. +-- @param event_name +-- @param func +function Event.add_removable_nth_tick_function(tick, func) + 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 this is called before runtime, we don't need to add the handlers + -- as they will be added later either in on_init or on_load. + if EventCore.runtime then + core_on_nth_tick(tick, func) + end +end + +--- Removes a handler for the nth tick. +-- See documentation at top of file for details on using events. +-- @param event_name +-- @param func +function Event.remove_removable_nth_tick_function(tick, func) + 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 end -Event.on_init(add_token_handlers) -Event.on_load(add_token_handlers) +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..87ea2d4b --- /dev/null +++ b/utils/event_core.lua @@ -0,0 +1,107 @@ +-- 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 + +Public.runtime = false + +-- map of event_name to handlers[] +local event_handlers = {} +-- map of nth_tick to handlers[] +local on_nth_tick_event_handlers = {} + +local function call_handlers(handlers, event) + if _DEBUG then + for _, handler in ipairs(handlers) do + handler(event) + end + else + for _, handler in ipairs(handlers) do + 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() + local handlers = event_handlers[init_event_name] + call_handlers(handlers) + + Public.runtime = true +end + +local function on_load() + local handlers = event_handlers[load_event_name] + call_handlers(handlers) + + Public.runtime = true +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 has 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) + end +end + +--- Do not use this function, use Event.on_init instead has 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) + end +end + +--- Do not use this function, use Event.on_load instead has 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) + end +end + +--- Do not use this function, use Event.on_nth_tick instead has 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) + 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 index d664ac6a..e173075a 100644 --- a/utils/global.lua +++ b/utils/global.lua @@ -1,4 +1,4 @@ -local Event = require 'utils.event' +local Event = require 'utils.event_core' local Token = require 'utils.token' local Global = {} diff --git a/utils/token.lua b/utils/token.lua index 2a41e1da..e0566ce0 100644 --- a/utils/token.lua +++ b/utils/token.lua @@ -1,10 +1,22 @@ +local EventCore = require 'utils.event_core' + 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 EventCore.runtime 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