--- Makes working with events in Factorio a lot more simple. --

By default, Factorio allows you to register **only one handler** to an event. --

This module lets you easily register **multiple handlers** to an event. --

Using this module is as simple as replacing @{LuaBootstrap.on_event|script.on_event} with @{Event.register}. --

-- 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. --
This module hooks into Factorio's event system, and using `script.on_event` for the same event will change which events are registered. --
--
-- This module does not have many of the multiplayer protections that `script.on_event` does. --
Due to this, great care should be taken when registering events conditionally. --
-- @module Event.Event -- @usage local Event = require('__stdlib__/stdlib/event/event') local config = require('__stdlib__/stdlib/config') config.control = true local Event = { __class = 'Event', registry = {}, -- Holds registered events custom_events = {}, -- Holds custom event ids stop_processing = {}, -- just has to be unique Filters = require('__stdlib__/stdlib/event/modules/event_filters'), __index = require('__stdlib__/stdlib/core') } setmetatable(Event, Event) Event.options = { protected_mode = false, skip_valid = false, force_crc = false -- Requires debug_mode to be true } local Event_options_meta = { __index = Event.options } Event.core_events = { on_init = 'on_init', on_load = 'on_load', on_configuration_changed = 'on_configuration_changed', init = 'on_init', load = 'on_load', configuration_changed = 'on_configuration_changed', init_and_config = { 'on_init', 'on_configuration_changed' }, init_and_load = { 'on_init', 'on_load' } } Event.script = { on_event = script.on_event, on_nth_tick = script.on_nth_tick, on_init = script.on_init, on_load = script.on_load, on_configuration_changed = script.on_configuration_changed, generate_event_name = script.generate_event_name, get_event_handler = script.get_event_handler } local Type = require('__stdlib__/stdlib/utils/type') local table = require('__stdlib__/stdlib/utils/table') local assert, type, tonumber = assert, type, tonumber local event_names = table.invert(defines.events) if not config.skip_script_protections then -- Protections for post and pre registrations for _, define in pairs(defines.events) do if Event.script.get_event_handler(define) then error('Detected attempt to add the STDLIB event module after using script.on_event') end end for name in pairs(Event.script) do _G.script[name] = function() error('Detected attempt to register an event using script.' .. name .. ' while using the STDLIB event system ') end end end local bootstrap_events = { 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) local id_type = type(id) return (id_type == 'number' or id_type == 'string'), 'Invalid Event Id, Must be string/int/defines.events, Passed in: ' .. type(id) end local function valid_event_id(id) return (tonumber(id) and id >= 0) or (Type.String(id) and not bootstrap_events[id]) end local function id_to_name(name) return event_names[name] or table.invert(Event.custom_events)[name] or name or 'unknown' end local stupid_events = { [defines.events.script_raised_revive] = 'entity', [defines.events.script_raised_built] = 'entity', [defines.events.on_entity_cloned] = 'destination' } --- Registers a handler for the given events. -- If a `nil` handler is passed, remove the given events and stop listening to them. --

Events dispatch in the order they are registered. --

An *event ID* can be obtained via @{defines.events}, -- @{LuaBootstrap.generate_event_name|script.generate_event_name} which is in @{int}, -- and can be a custom input name which is in @{string}. --

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) game.print(event.tick) end) -- -- Register something for Nth tick using negative numbers. -- Event.register(-120, function() game.print('Every 120 ticks') end -- -- Function call chaining -- Event.register(event1, handler1).register(event2, handler2) -- @param event_id (@{defines.events}, @{int}, @{string}, or {@{defines.events}, @{int}, @{string},...}) -- @tparam function handler the function to call when the given events are triggered -- @tparam[opt=nil] function filter 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 filter function, passed as the second parameter to your filter -- @tparam[opt=nil] table options a table of options that take precedence over the module options. -- @return (@{Event}) Event module object allowing for call chaining function Event.register(event_id, handler, filter, pattern, options) assert(event_id, 'missing event_id argument') assert(Type.Function(handler), 'handler function is missing, use Event.remove to un register events') assert(filter == nil or Type.Function(filter), 'filter must be a function when present') assert(options == nil or Type.Table(options), 'options must be a table when present') options = setmetatable(options or {}, Event_options_meta) --Recursively handle event id tables if Type.Table(event_id) then for _, id in pairs(event_id) do Event.register(id, handler) end return Event end assert(valid_id(event_id), 'event_id is invalid') -- 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 Type.String(event_id) then --String event ids will either be Bootstrap events or custom input events if bootstrap_events[event_id] then Event.script[event_id](bootstrap_events[event_id]) else Event.script.on_event(event_id, Event.dispatch) end elseif event_id >= 0 then --Positive values will be defines.events Event.script.on_event(event_id, Event.dispatch) elseif event_id < 0 then --Use negative values to register on_nth_tick Event.script.on_nth_tick(math.abs(event_id)--[[@as uint]] , 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.filter == filter then table.remove(registry, i) local output = { '__' .. script.mod_name .. '__', ' Duplicate handler registered for event ', event_id .. '(' .. (event_names[event_id] or ' ') .. ')', ' at position ' .. i, ', moving it to the bottom.' } log(table.concat(output)) break end end end --Finally insert the handler table.insert(registry, { handler = handler, filter = filter, pattern = pattern, options = options }) return Event end --- Removes a handler from the given events. --

When the last handler for an event is removed, stop listening to that event. --

An *event ID* can be obtained via @{defines.events}, -- @{LuaBootstrap.generate_event_name|script.generate_event_name} which is in @{int}, -- and can be a custom input name which is in @{string}. --

The `event_id` parameter takes in either a single, multiple, or mixture of @{defines.events}, @{int}, and @{string}. -- @param event_id (@{defines.events}, @{int}, @{string}, or {@{defines.events}, @{int}, @{string},...}) -- @tparam[opt] function handler the handler to remove, if not present remove all registered handlers for the event_id -- @tparam[opt] function filter -- @tparam[opt] mixed pattern -- @return (@{Event}) Event module object allowing for call chaining function Event.remove(event_id, handler, filter, pattern) assert(event_id, 'missing event_id argument') -- Handle recursion here if Type.Table(event_id) then for _, id in pairs(event_id) do Event.remove(id, handler) end return Event end assert(valid_id(event_id), 'event_id is invalid') 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 filter, possibly pattern if handler == registered.handler then if not filter and not pattern then table.remove(registry, i) found_something = true elseif filter then if filter == registered.filter 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 filter then -- no handler, filter, possibly pattern if filter == registered.filter 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 filter, pattern if pattern == registered.pattern then table.remove(registry, i) found_something = true end else -- no handler, filter, 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 Type.String(event_id) then -- String event ids will either be Bootstrap events or custom input events if bootstrap_events[event_id] then Event.script[event_id](nil) else Event.script.on_event(event_id, nil) end elseif event_id >= 0 then -- Positive values will be defines.events Event.script.on_event(event_id, nil) elseif event_id < 0 then -- Use negative values to remove on_nth_tick Event.script.on_nth_tick(math.abs(event_id)--[[@as uint]] , 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 --- Shortcut for `Event.register(Event.core_events.on_load, function)` -- @return (@{Event}) Event module object allowing for call chaining function Event.on_load(...) return Event.register(Event.core_events.on_load, ...) end function Event.on_load_if(truthy, ...) if truthy then return Event.on_load(...) end return Event end --- Shortcut for `Event.register(Event.core_events.on_configuration_changed, function)` -- @return (@{Event}) Event module object allowing for call chaining function Event.on_configuration_changed(...) return Event.register(Event.core_events.on_configuration_changed, ...) end --- Shortcut for `Event.register(Event.core_events.on_init, function)` -- @return (@{Event}) Event module object allowing for call chaining function Event.on_init(...) return Event.register(Event.core_events.on_init, ...) end function Event.on_init_if(truthy, ...) if truthy then return Event.on_init(...) end return Event end --- Shortcut for `Event.register(-nthTick, function)` -- @return (@{Event}) Event module object allowing for call chaining function Event.on_nth_tick(nth_tick, ...) return Event.register(-math.abs(nth_tick), ...) end --- Shortcut for `Event.register(defines.events, function)` -- @function Event.on_event -- @return (@{Event}) Event module object allowing for call chaining Event.on_event = Event.register function Event.register_if(truthy, id, ...) if truthy then return Event.register(id, ...) end return Event end Event.on_event_if = Event.register_if -- Used to replace pcall in un-protected events. local function no_pcall(handler, ...) return true, handler(...) end -- A dispatch helper function -- Call any filter and as applicable the event handler. -- protected errors are logged to game console if game is available, otherwise a real error -- is thrown. Bootstrap events are not protected from erroring no matter the option. local function dispatch_event(event, registered) local success, match_result, handler_result local protected = event.options.protected_mode local pcall = not bootstrap_events[event.name] and protected and pcall or no_pcall -- If we have a filter run it first passing event, and registered.pattern as parameters -- If the filter returns truthy call the handler passing event, and the result from the filter if registered.filter then success, match_result = pcall(registered.filter, event, registered.pattern) if success and match_result then success, handler_result = pcall(registered.handler, event, match_result) end else success, handler_result = pcall(registered.handler, event, registered.pattern) end -- If the handler errors lets make sure someone notices if not success and not Event.log_and_print(handler_result or match_result) then -- no players received the message, force a real error so someone notices error(handler_result or match_result) end return success and handler_result or nil end --- The user should create a table in this format, for a table that will be passed into @{Event.dispatch}. --

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. --

Abort calling remaining handlers if any one of them has invalid userdata. --

Handlers are dispatched in the order they were created. -- @param event (@{event_data}) the event data table -- @see https://forums.factorio.com/viewtopic.php?t=32039#p202158 Invalid Event Objects function Event.dispatch(event) if type(event) ~= 'table' then error('missing event table') end --get the registered handlers from name, input_name, or nth_tick in that priority. local registry if event.name and Event.registry[event.name] then registry = Event.registry[event.name] elseif event.input_name and Event.registry[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 --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 event.define_name = event_names[event.name or ''] event.options = event.options or {} -- Some events are just stupid and need more help if stupid_events[event.name] then event.created_entity = event.created_entity or event.entity or event.destination end for _, registered in ipairs(registry) do event.options = setmetatable(event.options, { __index = registered.options }) -- 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. if not event.options.skip_valid then for _, val in pairs(event) do if type(val) == 'table' and val.__self and not val.valid then return end end end -- Dispatch the event, if the event return Event.stop_processing don't process any more events if dispatch_event(event, registered) == Event.stop_processing then return end -- Force a crc check if option is enabled. This is a debug option and will hamper performance if enabled if game and event.options.force_crc then log('CRC check called for event [' .. event.name .. ']') game.force_crc() end end end end function Event.register_player(bool) require('__stdlib__/stdlib/event/player').register_events(bool) return Event end function Event.register_force(bool) require('__stdlib__/stdlib/event/force').register_events(bool) return Event end function Event.register_surface(bool) require('__stdlib__/stdlib/event/surface').register_events(bool) return Event 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) assert(Type.String(event_name), 'event_name must be a string.') local id if Type.Number(Event.custom_events[event_name]) then id = Event.custom_events[event_name] else id = Event.script.generate_event_name() Event.custom_events[event_name] = id end return id end function Event.set_event_name(event_name, id) assert(Type.String(event_name), 'event_name must be a string') assert(Type.Number(id)) Event.custom_events[event_name] = id return Event.custom_events[event_name] end function Event.get_event_name(event_name) assert(Type.String(event_name), 'event_name must be a string') return Event.custom_events[event_name] end ---@todo complete stub function Event.raise_event(...) script.raise_event(...) end --- Get event handler. function Event.get_event_handler(event_id) assert(valid_id(event_id), 'event_id is invalid') return { script = bootstrap_events[event_id] or (valid_event_id(event_id) and Event.script.get_event_handler(event_id)), handlers = Event.registry[event_id] } end --- Set protected mode. function Event.set_protected_mode(bool) Event.options.protected_mode = bool and true or false return Event end --- Set debug mode default for Event module. function Event.set_debug_mode(bool) Event.debug_mode = bool and true or false return Event end --- Set default options for the event module. function Event.set_option(option, bool) Event.options[option] = bool and true or false return Event end Event.dump_data = require('__stdlib__/stdlib/event/modules/dump_event_data')(Event, valid_event_id, id_to_name) return Event