Create lobbies on demand and only list servers on the player's version

When a player connects with a Factorio version and set of built in mods
no lobby runs, the lobby plugin now creates one on a connected host that
has the version installed or can download it, with a mod pack matching
the player's built in mods. This lets a cluster accept players on
several Factorio versions on the same port without any setup. It can be
turned off with lobby.auto_create.

To pick the mod pack the proxy now waits for the whole connection
confirm before routing and passes the mods listed in it to the
playerProxyRoute hook. Hosts answer the new HostFactorioVersionsRequest
with the versions they have installed.

The server list in lobbies only shows instances on the Factorio version
of the lobby, with a checkbox to also show the others greyed out.
This commit is contained in:
Danielv123 committed 2026-10-29 20:54:11 +01:00
1 parent a5bd43637e
commit 00823262d5
14 files changed
+582 -39

No files matched your search

+58 -7
View File
@@ -7,6 +7,11 @@ import { Controller, HostRecord, InstanceRecord, PlayerProxyServer, proxyProtoco
const { MessageType } = proxyProtocol;
const clientVersion = { major: 2, minor: 0, sub: 72, build: 84292 };
const defaultMods = [
{ name: "base", version: "2.0.72" },
{ name: "space-age", version: "2.0.72" },
{ name: "big-mod", version: "1.300.2" },
];
/**
* Encode a ConnectionRequestReplyConfirm the way a Factorio client does
@@ -14,18 +19,37 @@ const clientVersion = { major: 2, minor: 0, sub: 72, build: 84292 };
* @param {object} options - fields of the message.
* @returns {Buffer[]} the message split into the given number of fragments.
*/
function encodeConfirm({ clientRequestId, serverRequestId, username, messageId = 1, fragments = 1, padding = 40 }) {
function encodeConfirm({
clientRequestId, serverRequestId, username, messageId = 1, fragments = 1, mods = defaultMods,
}) {
const payload = new proxyProtocol.BufferWriter();
const writeString = text => {
const bytes = Buffer.from(text);
payload.writeUInt8(bytes.length);
for (const byte of bytes) {
payload.writeUInt8(byte);
}
};
payload.writeUInt32LE(clientRequestId);
payload.writeUInt32LE(serverRequestId);
payload.writeUInt32LE(1234); // instanceID
const name = Buffer.from(username);
payload.writeUInt8(name.length);
for (const byte of name) {
payload.writeUInt8(byte);
writeString(username);
writeString(""); // password
writeString("server-key");
writeString("1700000000");
payload.writeUInt32LE(0x11111111); // coreChecksum
payload.writeUInt32LE(0x22222222); // prototypeListChecksum
payload.writeUInt8(mods.length);
for (const mod of mods) {
writeString(mod.name);
for (const part of mod.version.split(".")) {
payload.writeSpaceOptimizedUInt16(Number(part));
}
payload.writeUInt32LE(0x33333333); // crc
}
for (let i = 0; i < padding; i++) {
payload.writeUInt8(i); // stands in for the password, keys, checksums and mods
// Stands in for the startup mod settings
for (let i = 0; i < 40; i++) {
payload.writeUInt8(i);
}
const body = payload.toBuffer();
@@ -193,6 +217,11 @@ describe("controller/proxy_protocol", function () {
assert.throws(() => proxyProtocol.parseConnectionRequestReplyConfirm(second), /first fragment/);
});
it("should read the mods from a fragmented confirm", function () {
const fragments = encodeConfirm({ clientRequestId: 1, serverRequestId: 2, username: "Player", fragments: 3 });
assert.deepEqual(proxyProtocol.parseConfirmMods(proxyProtocol.joinPayload(fragments)), defaultMods);
});
it("should reject truncated messages", function () {
assert.throws(() => proxyProtocol.parseConnectionRequest(Buffer.from([MessageType.ConnectionRequest, 0])));
});
@@ -267,6 +296,28 @@ describe("controller/PlayerProxyServer", function () {
assert.equal(proxy.serverVersions.get(1).build, clientVersion.build);
});
it("should route once the whole confirm has arrived", async function () {
const requests = [];
controller.hooks.playerProxyRoute.attach("test", request => {
requests.push(request);
return 1;
});
const client = await newClient();
client.send(proxyProtocol.encodeConnectionRequest(clientVersion, 7));
const reply = proxyProtocol.parseConnectionRequestReply(await client.receive());
const [first, second, third] = encodeConfirm({
clientRequestId: 7, serverRequestId: reply.serverRequestId, username: "Player", messageId: 7, fragments: 3,
});
client.send(third);
client.send(first);
await lib.wait(50);
assert.equal(requests.length, 0, "routed before all fragments arrived");
client.send(second);
assert.equal((await client.receive())[0], MessageType.ConnectionAcceptOrDeny);
assert.deepEqual(requests[0].mods, defaultMods);
assert.equal(servers[0].confirms().length, 3);
});
it("should route by the playerProxyRoute hook", async function () {
controller.hooks.playerProxyRoute.attach("test", request => {
assert.deepEqual(request.version, clientVersion);