Files
factorio-clusterio/packages/host/modules/clusterio/impl.lua
T
bbassieandClaude Fable 5.1 788ae17657 Add UDP communication support for instances
Add opt-in UDP messaging between the host and the Factorio server using
the --enable-lua-udp option of Factorio 2.0.59+.  When
factorio.enable_lua_udp is set the instance binds a localhost UDP socket,
starts Factorio with --enable-lua-udp on factorio.lua_udp_port (random
if null, like the RCON port) and tells the clusterio module the host port
via clusterio_private.update_instance.

Data from Factorio is sent with clusterio_api.send_udp(channel, data) in
the same IPC format as send_json, so plugins receive it through the
existing ipc-<channel> events.  Packets not sent from the Lua UDP port
are ignored, as that is the only unauthenticated way into the IPC
handler.  Data to Factorio is sent with the new sendUdp method on
instances and instance plugins, and is dispatched in-game as
on_udp_packet_received events by polling helpers.recv_udp every tick
from the clusterio module.

Plugins requiring UDP declare the new LuaUdp feature flag, which like
SavePatching and ScriptCommands refuses to load the plugin on instances
without the option enabled.

Verified against a headless 2.1.17 server: send_udp and recv_udp are
silent no-ops when the option is off, so a stale port in a save that is
moved to an instance without UDP does no harm.  The integration test
matrix now starts the instance with UDP enabled on Factorio 2.0.59+ and
round trips a player event over it.

Closes #969

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:38:28 +00:00

130 lines
3.6 KiB
Lua

local api = require('modules/clusterio/api')
local compat = require("modules/clusterio/compat")
--- @class (exact) EventData.on_server_startup:EventData
local function check_patch()
if compat.script_data.clusterio_patch_number ~= clusterio_patch_number then
compat.script_data.clusterio_patch_number = clusterio_patch_number
script.raise_event(api.events.on_server_startup, {
name = api.events.on_server_startup, tick = game.tick
})
end
end
local impl = {}
impl.events = {}
impl.events[defines.events.on_tick] = function()
check_patch()
-- Dispatch on_udp_packet_received for packets sent by the host
local clusterio = compat.script_data.clusterio
if clusterio and clusterio.host_udp_port then
helpers.recv_udp(0)
end
end
impl.events[api.events.on_server_startup] = function()
if not compat.script_data.clusterio then
compat.script_data.clusterio = {
instance_id = nil,
instance_name = nil,
host_udp_port = nil,
}
end
-- Replay will just desync after the save is patched
game.disable_replay()
end
impl.events[defines.events.on_player_joined_game] = function(event)
local player = game.players[event.player_index]
api.send_json("player_event", { type = "join", name = player.name })
end
local disconnect_reason_name = {}
if defines.disconnect_reason then
for name, i in pairs(defines.disconnect_reason) do
disconnect_reason_name[i] = name
end
end
impl.events[defines.events.on_player_left_game] = function(event)
local player = game.players[event.player_index]
api.send_json("player_event", {
type = "leave",
name = player.name,
reason = disconnect_reason_name[event.reason] or "quit",
})
end
impl.events[defines.events.on_player_banned] = function(event)
api.send_json("player_event", {
type = "BAN",
name = event.player_name,
reason = event.reason,
})
end
impl.events[defines.events.on_player_unbanned] = function(event)
api.send_json("player_event", { type = "UNBANNED", name = event.player_name })
end
impl.events[defines.events.on_player_promoted] = function(event)
local player = game.players[event.player_index]
api.send_json("player_event", { type = "PROMOTE", name = player.name })
end
impl.events[defines.events.on_player_demoted] = function(event)
local player = game.players[event.player_index]
api.send_json("player_event", { type = "DEMOTE", name = player.name })
end
-- Internal API
clusterio_private = {}
function clusterio_private.update_instance(new_id, new_name, host_udp_port)
check_patch()
local script_data = compat.script_data
script_data.clusterio.instance_id = new_id
script_data.clusterio.instance_name = new_name
script_data.clusterio.host_udp_port = host_udp_port
script.raise_event(api.events.on_instance_updated, {
instance_id = new_id,
instance_name = new_name,
})
end
-- This is not part of the add_remote_interface callback to ensure it is
-- available when the clusterio_lib mod is loaded. The reason this is
-- neccessary is that on_init for newly added mods happen before on_load
-- for existing mods, and the add_remote_interface callback is done in
-- on_load. See https://forums.factorio.com/viewtopic.php?f=25&t=81552 for
-- more details.
remote.add_interface('clusterio_api', {
get_events = function()
return api.events
end,
get_instance_id = function()
return compat.script_data.clusterio.instance_id
end,
get_instance_name = function()
return compat.script_data.clusterio.instance_name
end,
get_host_udp_port = function()
return compat.script_data.clusterio.host_udp_port
end,
get_file_no = function()
local script_data = compat.script_data
script_data.clusterio_file_no = (script_data.clusterio_file_no or 0) + 1
return script_data.clusterio_file_no
end,
})
return impl