411 lines
17 KiB
Lua
411 lines
17 KiB
Lua
--- Makes working with events in Factorio a lot more simple.
|
|
-- <p>By default, Factorio allows you to register **only one handler** to an event.
|
|
-- <p>This module lets you easily register **multiple handlers** to an event.
|
|
-- <p>Using this module is as simple as replacing @{LuaBootstrap.on_event|script.on_event} with @{Event.register}.
|
|
-- <blockquote>
|
|
-- Due to the way that Factorio's event system works, it is not recommended to intermingle `script.on_event` and `Event.register` in a mod.
|
|
-- <br>This module hooks into Factorio's event system, and using `script.on_event` for the same event will change which events are registered.
|
|
-- </blockquote>
|
|
-- <blockquote>
|
|
-- This module does not have many of the multiplayer protections that `script.on_event` does.
|
|
-- <br>Due to this, great care should be taken when registering events conditionally.
|
|
-- </blockquote>
|
|
-- @module Event
|
|
-- @usage local Event = require('stdlib/event/event')
|
|
|
|
--Holds the event registry
|
|
local event_registry = {}
|
|
|
|
local Event = {
|
|
_module_name = 'Event',
|
|
core_events = {
|
|
init = 'on_init',
|
|
load = 'on_load',
|
|
configuration_changed = 'on_configuration_changed',
|
|
init_and_config = {'on_init', 'on_configuration_changed'}
|
|
},
|
|
custom_events = {}, -- Holds custom event ids
|
|
protected_mode = false,
|
|
force_crc = false,
|
|
stop_processing = {} -- just has to be unique
|
|
}
|
|
setmetatable(Event, {__index = require('stdlib/core')})
|
|
|
|
local Is = require('stdlib/utils/is')
|
|
|
|
local bootstrap_register = {
|
|
on_init = function()
|
|
Event.dispatch({name = 'on_init'})
|
|
end,
|
|
on_load = function()
|
|
Event.dispatch({name = 'on_load', tick = -1})
|
|
end,
|
|
on_configuration_changed = function(event)
|
|
event.name = 'on_configuration_changed'
|
|
Event.dispatch(event)
|
|
end
|
|
}
|
|
|
|
local function valid_id(id)
|
|
return (Is.Number(id) or Is.String(id)), 'Invalid Event Id, Must be string/int/defines.events, Passed in: ' .. type(id)
|
|
end
|
|
|
|
--- Registers a handler for the given events.
|
|
-- If a `nil` handler is passed, remove the given events and stop listening to them.
|
|
-- <p>Events dispatch in the order they are registered.
|
|
-- <p>An *event ID* can be obtained via @{defines.events},
|
|
-- @{LuaBootstrap.generate_event_name|script.generate_event_name} which is in <span class="types">@{int}</span>,
|
|
-- and can be a custom input name which is in <span class="types">@{string}</span>.
|
|
-- <p>The `event_id` parameter takes in either a single, multiple, or mixture of @{defines.events}, @{int}, and @{string}.
|
|
-- @usage
|
|
-- -- Create an event that prints the current tick every tick.
|
|
-- Event.register(defines.events.on_tick, function(event) print event.tick end)
|
|
-- -- Create an event that prints the new ID of a train.
|
|
-- Event.register(Trains.on_train_id_changed, function(event) print(event.new_id) end)
|
|
-- -- Function call chaining
|
|
-- Event.register(event1, handler1).register(event2, handler2)
|
|
-- @param event_id (<span class="types">@{defines.events}, @{int}, @{string}, or {@{defines.events}, @{int}, @{string},...}</span>)
|
|
-- @tparam function handler the function to call when the given events are triggered
|
|
-- @tparam[opt=nil] function matcher a function whose return determines if the handler is executed. event and pattern are passed into this
|
|
-- @tparam[opt=nil] mixed pattern an invariant that can be used in the matcher function, passed as the second parameter to your matcher
|
|
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
|
|
function Event.register(event_id, handler, matcher, pattern)
|
|
Is.Assert(event_id, 'missing event_id argument')
|
|
Is.Assert(Is.Function(handler), 'handler function is missing, use Event.remove to un register events')
|
|
Is.Assert(Is.Nil(matcher) or Is.Function(matcher), 'matcher must be a function when present')
|
|
|
|
--Recursively handle event id tables
|
|
if Is.Table(event_id) then
|
|
for _, id in pairs(event_id) do
|
|
Event.register(id, handler)
|
|
end
|
|
return Event
|
|
end
|
|
|
|
Is.Assert(valid_id(event_id))
|
|
|
|
-- If the event_id has never been registered before make sure we call the correct script action to register
|
|
-- our Event handler with factorio
|
|
if not event_registry[event_id] then
|
|
event_registry[event_id] = {}
|
|
|
|
if Is.String(event_id) then
|
|
--String event ids will either be Bootstrap events or custom input events
|
|
if bootstrap_register[event_id] then
|
|
script[event_id](bootstrap_register[event_id])
|
|
else
|
|
script.on_event(event_id, Event.dispatch)
|
|
end
|
|
elseif event_id >= 0 then
|
|
--Positive values will be defines.events
|
|
script.on_event(event_id, Event.dispatch)
|
|
elseif event_id < 0 then
|
|
--Use negative values to register on_nth_tick
|
|
script.on_nth_tick(math.abs(event_id), Event.dispatch)
|
|
end
|
|
end
|
|
|
|
local registry = event_registry[event_id]
|
|
|
|
--If handler is already registered for this event: remove it for re-insertion at the end.
|
|
if #registry > 0 then
|
|
for i, registered in ipairs(registry) do
|
|
if registered.handler == handler and registered.pattern == pattern and registered.matcher == matcher then
|
|
table.remove(registry, i)
|
|
log('Same handler already registered for event ' .. event_id .. ' at position ' .. i .. ', moving it to the bottom')
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--Finally insert the handler
|
|
table.insert(registry, {handler = handler, matcher = matcher, pattern = pattern})
|
|
return Event
|
|
end
|
|
|
|
--- Removes a handler from the given events.
|
|
-- <p>When the last handler for an event is removed, stop listening to that event.
|
|
-- <p>An *event ID* can be obtained via @{defines.events},
|
|
-- @{LuaBootstrap.generate_event_name|script.generate_event_name} which is in <span class="types">@{int}</span>,
|
|
-- and can be a custom input name which is in <span class="types">@{string}</span>.
|
|
-- <p>The `event_id` parameter takes in either a single, multiple, or mixture of @{defines.events}, @{int}, and @{string}.
|
|
-- @param event_id (<span class="types">@{defines.events}, @{int}, @{string}, or {@{defines.events}, @{int}, @{string},...}</span>)
|
|
-- @tparam[opt] function handler the handler to remove, if not present remove all registered handlers for the event_id
|
|
-- @tparam[opt] function matcher
|
|
-- @tparam[opt] mixed pattern
|
|
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
|
|
function Event.remove(event_id, handler, matcher, pattern)
|
|
Is.Assert(event_id, 'missing event_id argument')
|
|
|
|
-- Handle recursion here
|
|
if Is.Table(event_id) then
|
|
for _, id in pairs(event_id) do
|
|
Event.remove(id, handler)
|
|
end
|
|
return Event
|
|
end
|
|
|
|
Is.Assert(valid_id(event_id))
|
|
|
|
local registry = event_registry[event_id]
|
|
if registry then
|
|
local found_something = false
|
|
for i = #registry, 1, -1 do
|
|
local registered = registry[i]
|
|
if handler then -- handler, possibly matcher, possibly pattern
|
|
if handler == registered.handler then
|
|
if not matcher and not pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
elseif matcher then
|
|
if matcher == registered.matcher then
|
|
if not pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
elseif pattern and pattern == registered.pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
end
|
|
end
|
|
elseif pattern and pattern == registered.pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
end
|
|
end
|
|
elseif matcher then -- no handler, matcher, possibly pattern
|
|
if matcher == registered.matcher then
|
|
if not pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
elseif pattern and pattern == registered.pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
end
|
|
end
|
|
elseif pattern then -- no handler, no matcher, pattern
|
|
if pattern == registered.pattern then
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
end
|
|
else -- no handler, matcher, or pattern
|
|
table.remove(registry, i)
|
|
found_something = true
|
|
end
|
|
end
|
|
|
|
if found_something and table.size(registry) == 0 then
|
|
-- Clear the registry data and un subscribe if there are no registered handlers left
|
|
event_registry[event_id] = nil
|
|
|
|
if Is.String(event_id) then
|
|
-- String event ids will either be Bootstrap events or custom input events
|
|
if bootstrap_register[event_id] then
|
|
script[event_id](nil)
|
|
else
|
|
script.on_event(event_id, nil)
|
|
end
|
|
elseif event_id >= 0 then
|
|
-- Positive values will be defines.events
|
|
script.on_event(event_id, nil)
|
|
elseif event_id < 0 then
|
|
-- Use negative values to remove on_nth_tick
|
|
script.on_nth_tick(math.abs(event_id), nil)
|
|
end
|
|
elseif not found_something then
|
|
log('Attempt to deregister already non-registered listener from event: ' .. event_id)
|
|
end
|
|
else
|
|
log('Attempt to deregister already non-registered listener from event: ' .. event_id)
|
|
end
|
|
return Event
|
|
end
|
|
|
|
-- A dispatch helper function
|
|
--
|
|
-- Call any matcher and, as applicable, the event handler, in protected mode. Errors are
|
|
-- caught and logged to stdout but event processing proceeds thereafter; errors are suppressed.
|
|
local function run_protected(registered, event)
|
|
local success, err
|
|
if registered.matcher then
|
|
success, err = pcall(registered.matcher, event, registered.pattern)
|
|
if success and err then
|
|
success, err = pcall(registered.handler, event)
|
|
end
|
|
else
|
|
success, err = pcall(registered.handler, event)
|
|
end
|
|
|
|
-- If the handler errors lets make sure someone notices
|
|
if not success then
|
|
if not Event.log_and_print(err) then
|
|
-- no players received the message, force a real error so someone notices
|
|
error(err)
|
|
end
|
|
end
|
|
|
|
-- force a crc check if option is enabled. This is a debug option and will hamper performance if enabled
|
|
if (Event.force_crc or event.force_crc) and game then
|
|
log('CRC check called for event [' .. event.name .. ']')
|
|
game.force_crc()
|
|
end
|
|
|
|
return success and err or nil
|
|
end
|
|
|
|
--- The user should create a table in this format, for a table that will be passed into @{Event.dispatch}.
|
|
-- <p>In general, the user should create an event data table that is in a similar format as the one that Factorio returns.
|
|
--> The event data table **MUST** have either `name` or `input_name`.
|
|
-- @tfield[opt] int|defines.events name unique event ID generated with @{LuaBootstrap.generate_event_name|script.generate_event_name} ***OR*** @{defines.events}
|
|
-- @tfield[opt] string input_name custom input name of an event
|
|
-- @field[opt] ... any # of additional fields with extra data, which are passed into the handler registered to an event that this table represents
|
|
-- @usage
|
|
-- -- below code is from Trains module.
|
|
-- -- old_id & new_id are additional fields passed into the handler that's registered to Trains.on_train_id_changed event.
|
|
-- local event_data = {
|
|
-- old_id = renaming.old_id,
|
|
-- new_id = renaming.new_id,
|
|
-- name = Trains.on_train_id_changed
|
|
-- }
|
|
-- Event.dispatch(event_data)
|
|
-- @table event_data
|
|
|
|
--- Calls the handlers that are registered to the given event.
|
|
-- <p>Abort calling remaining handlers if any one of them has invalid userdata.
|
|
-- <p>Handlers are dispatched in the order they were created.
|
|
-- @param event (<span class="types">@{event_data}</span>) the event data table
|
|
-- @see https://forums.factorio.com/viewtopic.php?t=32039#p202158 Invalid Event Objects
|
|
function Event.dispatch(event)
|
|
Is.Assert.Table(event, 'missing event table')
|
|
--get the registered handlers from name, input_name, or nth_tick in that priority.
|
|
local registry
|
|
if event.name then
|
|
registry = event_registry[event.name]
|
|
elseif event.input_name then
|
|
registry = event_registry[event.input_name]
|
|
elseif event.nth_tick then
|
|
registry = event_registry[-event.nth_tick]
|
|
end
|
|
|
|
if registry then
|
|
-- protected_mode runs the handler and matcher in pcall, additionaly forcing a crc can only be
|
|
-- accomplished in protected_mode
|
|
local protected = Event.protected_mode or event.protected_mode
|
|
|
|
--add the tick if it is not present, this only affects calling Event.dispatch manually
|
|
--doing the check up here as it will faster than checking every iteration for a constant value
|
|
event.tick = event.tick or game and game.tick or 0
|
|
|
|
for _, registered in ipairs(registry) do
|
|
-- Check for userdata and stop processing this and further handlers if not valid
|
|
-- This is the same behavior as factorio events.
|
|
-- This is done inside the loop as other events can modify the event.
|
|
for _, val in pairs(event) do
|
|
if Is.Object(val) and not val.valid then
|
|
return
|
|
end
|
|
end
|
|
|
|
if protected then
|
|
if run_protected(registered, event) == Event.stop_processing then
|
|
return
|
|
end
|
|
elseif registered.matcher then
|
|
if registered.matcher(event, registered.pattern) then
|
|
if registered.handler(event) == Event.stop_processing then
|
|
return
|
|
end
|
|
end
|
|
else
|
|
if registered.handler(event) == Event.stop_processing then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Retrieve or Generate an event_name and store it in Event.custom_events
|
|
-- @tparam string event_name the custom name for your event.
|
|
-- @treturn int the id associated with the event.
|
|
-- @usage
|
|
-- Event.register(Event.generate_event_name("my_custom_event"), handler)
|
|
function Event.generate_event_name(event_name)
|
|
Is.Assert.String(event_name, 'event_name must be a string.')
|
|
|
|
local id
|
|
if Is.Number(Event.custom_events[event_name]) then
|
|
id = Event.custom_events[event_name]
|
|
else
|
|
id = script.generate_event_name()
|
|
Event.custom[event_name] = id
|
|
end
|
|
return id
|
|
end
|
|
|
|
-- TODO complete stub
|
|
function Event.raise_event(...)
|
|
script.raise_event(...)
|
|
end
|
|
|
|
function Event.get_event_handler(event_id)
|
|
Is.Assert(valid_id(event_id))
|
|
return {
|
|
script = (tonumber(event_id) or 0 >= 0 or Is.String(event_id)) and script.get_event_handler(event_id),
|
|
handlers = event_registry[event_id]
|
|
}
|
|
end
|
|
|
|
--- Retrieve the event_registry
|
|
-- @treturn table event_registry
|
|
function Event.get_registry()
|
|
return event_registry
|
|
end
|
|
|
|
function Event.dump(reg_type)
|
|
local init, config, load, events, nth = 0, 0, 0, 0, 0
|
|
for id, registry in pairs(event_registry) do
|
|
if tonumber(id) then
|
|
if id < 0 then
|
|
nth = nth + #registry
|
|
else
|
|
events = events + #registry
|
|
end
|
|
else
|
|
if id == "on_init" then
|
|
init = init + #registry
|
|
elseif id == "on_configuration_changed" then
|
|
config = config + #registry
|
|
elseif id == "load" then
|
|
load = load + #registry
|
|
else
|
|
events = events + #registry
|
|
end
|
|
end
|
|
end
|
|
local all = {
|
|
core = init + load + config,
|
|
init = init,
|
|
config = config,
|
|
load = load,
|
|
events = events,
|
|
nth = nth,
|
|
all = init + config + load + events + nth
|
|
}
|
|
return reg_type and all[reg_type] or all
|
|
end
|
|
|
|
--- Filters events related to entity_type.
|
|
-- DEPRECATED
|
|
-- @tparam string event_parameter The event parameter to look inside to find the entity type
|
|
-- @tparam string entity_type The entity type to filter events for
|
|
-- @tparam callable matcher The matcher to invoke if the filter passes. The object defined in the event parameter is passed
|
|
function Event.filter_entity(event_parameter, entity_type, matcher)
|
|
return function(evt)
|
|
if (evt[event_parameter].type == entity_type) then
|
|
matcher(evt[event_parameter])
|
|
end
|
|
end
|
|
end
|
|
|
|
return Event
|