Files
factorio-clusterio/packages/host/lua/clusterio_lib/api.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

83 lines
2.0 KiB
Lua

-- Important: Keep this API in sync with modules/clusterio/api.lua
local compat = require("compat")
local api = {}
local initialized = false
function api.init()
if initialized then
return
end
initialized = true
if remote.interfaces.clusterio_api then
api.events = remote.call("clusterio_api", "get_events")
else
local null_event = script.generate_event_name()
api.events = {
on_instance_updated = null_event,
on_server_startup = null_event,
}
end
end
local function call_api(fn, ...)
if remote.interfaces.clusterio_api and remote.interfaces.clusterio_api[fn] then
return remote.call("clusterio_api", fn, ...)
else
return nil
end
end
function api.get_instance_name()
return call_api("get_instance_name")
end
function api.get_instance_id()
return call_api("get_instance_id")
end
local function escape_channel(channel)
-- Escape bad characters. The question mark is used for separating the
-- channel name from the payload.
return channel:gsub("([\x00-\x1f?\\])", function(match)
return "\\x" .. string.format("%02x", match:byte())
end)
end
function api.send_json(channel, data)
if not remote.interfaces.clusterio_api then
return
end
channel = escape_channel(channel)
data = compat.table_to_json(data)
-- If there's more than about 4kB of data users running with the Windows
-- console open will start to experience stuttering, otherwise we could
-- output about 1 MB of data at a time through stdout.
if #data < 4000 then
print("\f$ipc:" .. channel .. "?j" .. data)
else
local file_no = remote.call("clusterio_api", "get_file_no")
local file_name = "clst_" .. file_no .. ".json"
compat.write_file(file_name, data, false, 0)
print("\f$ipc:" .. channel .. "?f" .. file_name)
end
end
function api.send_udp(channel, data)
local port = call_api("get_host_udp_port")
if not port then
return -- Lua UDP is not enabled for this instance
end
channel = escape_channel(channel)
data = compat.table_to_json(data)
helpers.send_udp(port, "\f$ipc:" .. channel .. "?j" .. data, 0)
end
return api