Require Factorio 2.1.12 for Lua UDP

Factorio 2.0.59 through 2.1.11 crash with "Trying to add invalid input
action to the closure" when helpers.recv_udp dispatches a received
packet, whether polled from on_tick or called from a command and
whether or not the server is paused.  Reproduced on headless 2.0.77 and
2.1.8, works from 2.1.12 on.  FactorioServer now refuses to start with
Lua UDP enabled on older versions, the docs say 2.1.12, and the
integration tests only enable it on 2.1.12 or later.

The to-game integration test polls recv_udp from on_tick like the
clusterio module does and sends commands to make the empty server tick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Fable 5.1 committed 2026-10-15 16:38:34 +00:00
1 parent 7a64706c82
commit 1f37905be0
8 files changed
+37 -10

No files matched your search

+1 -1
View File
@@ -610,7 +610,7 @@ Defaults to true.
Enable UDP messaging between the host and the Factorio server by passing `--enable-lua-udp` to Factorio.
This lets plugins send UDP packets to the server and receive data sent with `clusterio_api.send_udp` in-game.
Requires Factorio 2.0.59 or later.
Requires Factorio 2.1.12 or later, earlier versions crash when a packet is received while the server is paused.
Defaults to false.
+1 -1
View File
@@ -255,7 +255,7 @@ Parameters:
Send data to Clusterio over the given channel using UDP instead of stdout.
Clusterio plugins receive it as `udp-channel_name` events, see the [Communicating over UDP section](writing-plugins.md#communicating-over-udp) in the Writing Plugins document.
Requires `factorio.enable_lua_udp` to be enabled on the instance and Factorio 2.0.59 or later, otherwise the data is silently dropped.
Requires `factorio.enable_lua_udp` to be enabled on the instance and Factorio 2.1.12 or later, otherwise the data is silently dropped.
The port to send to is passed to the module with a script command on startup, so `factorio.enable_script_commands` must be enabled as well.
**Note**: Delivery is not guaranteed and payloads should be kept well below 32 kB.
+2 -1
View File
@@ -484,7 +484,8 @@ For RCON, commands longer than 50 characters may end up being executed after sho
### Communicating over UDP
Instances can optionally exchange UDP packets with the Factorio server for low latency fire-and-forget communication, using the `--enable-lua-udp` option of Factorio 2.0.59 and later.
Instances can optionally exchange UDP packets with the Factorio server for low latency fire-and-forget communication, using the `--enable-lua-udp` option of Factorio.
This needs Factorio 2.1.12 or later, earlier versions crash when a packet is received while the server is paused.
This is disabled by default and enabled by setting `factorio.enable_lua_udp` on the instance.
Sending data out of Factorio also needs `factorio.enable_script_commands`, as the port to send to is passed to the module with a script command on startup.
Plugins requiring it should declare the `LuaUdp` feature.
+9
View File
@@ -865,6 +865,13 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
this._udpSocket = socket;
}
// Factorio before 2.1.12 crashes when a packet is received while paused
_checkLuaUdpVersion() {
if (this.enableLuaUdp && lib.integerFullVersion(this._version!) < lib.integerFullVersion("2.1.12")) {
throw new Error("Lua UDP requires Factorio 2.1.12 or later");
}
}
_stopUdp() {
if (this._udpSocket) {
this._udpSocket.close();
@@ -1317,6 +1324,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
*/
async start(save: string) {
this._check(["init"]);
this._checkLuaUdpVersion();
this._state = "running";
try {
@@ -1370,6 +1378,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
*/
async startScenario(scenario: string, seed?: number, mapGenSettings?: object, mapSettings?: object) {
this._check(["init"]);
this._checkLuaUdpVersion();
this._state = "running";
try {
+1 -1
View File
@@ -611,7 +611,7 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
"factorio.enable_lua_udp": {
description:
"Enable UDP messaging between the Factorio server and the host. " +
"Required by plugins using UDP. Requires Factorio 2.0.59 or later.",
"Required by plugins using UDP. Requires Factorio 2.1.12 or later.",
restartRequired: true, // Because of the cli option "--enable-lua-udp"
type: "boolean",
initialValue: false,
+14
View File
@@ -365,6 +365,20 @@ describe("host/server", function() {
return { lines, restore: () => { server._logger = logger; } };
}
it("should refuse to start on Factorio before 2.1.12", async function() {
const version = server._version;
server._version = "2.1.11";
server.enableLuaUdp = true;
try {
const error = new Error("Lua UDP requires Factorio 2.1.12 or later");
await assert.rejects(server.start("save.zip"), error);
await assert.rejects(server.startScenario("test"), error);
assert.equal(server._state, "init");
} finally {
server._version = version;
server.enableLuaUdp = false;
}
});
it("should not have a host port when not running", function() {
assert.equal(server.hostUdpPort, undefined);
});
+2 -2
View File
@@ -21,9 +21,9 @@ const requireApi = [
"or package.loaded['__level__/modules/clusterio/api.lua']", // 2.0.0
].join(" ");
// --enable-lua-udp was added in Factorio 2.0.59
// Lua UDP needs Factorio 2.1.12, see docs/configuration.md
function hasLuaUdp() {
return lib.integerFullVersion(getFactorioVersion()) >= lib.integerFullVersion("2.0.59");
return lib.integerFullVersion(getFactorioVersion()) >= lib.integerFullVersion("2.1.12");
}
function getUser(name) {
+7 -4
View File
@@ -125,8 +125,8 @@ describe("Integration of host/src/server", function() {
let mapPath = server.writePath("saves", "test.zip");
await assert.doesNotReject(fs.access(mapPath), "save is missing");
// --enable-lua-udp was added in Factorio 2.0.59
server.enableLuaUdp = lib.integerFullVersion(server.version) >= lib.integerFullVersion("2.0.59");
// Lua UDP needs Factorio 2.1.12, see docs/configuration.md
server.enableLuaUdp = lib.integerFullVersion(server.version) >= lib.integerFullVersion("2.1.12");
await server.start("test.zip");
});
});
@@ -182,9 +182,11 @@ describe("Integration of host/src/server", function() {
}
log("Lua UDP to game");
// Poll from on_tick like the clusterio module does
await server.sendRcon(
"/sc script.on_event(defines.events.on_udp_packet_received, " +
"function(event) print('udp:' .. event.payload) end)"
"function(event) print('udp:' .. event.payload) end) " +
"script.on_nth_tick(1, function() helpers.recv_udp(0) end)"
);
let pass = false;
function filter(output) {
@@ -195,8 +197,9 @@ describe("Integration of host/src/server", function() {
server.on("output", filter);
await server.sendUdp("hello");
// The server does not tick without players, commands make it tick
for (let i = 0; i < 10; i++) {
await server.sendRcon("/sc helpers.recv_udp(0)");
await server.sendRcon("/sc rcon.print('tick')");
await lib.wait(10);
if (pass) {
break;