Aleksei-bird 7c9c708c92 Первый фикс
Пачки некоторых позиций увеличены
2024-03-01 20:54:33 +03:00

530 lines
21 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.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.
-- <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) 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 (<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 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 (<span class="types">@{Event}</span>) 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.
-- <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 filter
-- @tparam[opt] mixed pattern
-- @return (<span class="types">@{Event}</span>) 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 (<span class="types">@{Event}</span>) 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 (<span class="types">@{Event}</span>) 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 (<span class="types">@{Event}</span>) 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 (<span class="types">@{Event}</span>) 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 (<span class="types">@{Event}</span>) 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}.
-- <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)
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