research_sync: README for the cluster research queue, review fixes

Document the cluster research queue in the plugin README and the root
README. Review fixes: ipairs for queue order, event annotations on the new
Lua handlers, an IpcQueue type, a test for clearing the queue, and drop the
controller import from test/missing.js.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILLISISandClaude Opus 5.5 committed 2026-10-18 21:54:10 +11:00
1 parent 7175571d5b
commit 1f6e0519bd
6 files changed
+41 -7

No files matched your search

+1 -1
View File
@@ -73,7 +73,7 @@ Plugins add functionality to Factorio servers, Clusterio itself or both.
These are the plugins supported and maintained by the Clusterio developers:
- [Global Chat](/plugins/global_chat/README.md): share the in-game chat between servers.
- [Research Sync](/plugins/research_sync/README.md): synchronize research progress and technologies unlocked between servers.
- [Research Sync](/plugins/research_sync/README.md): synchronize research progress and technologies unlocked and the research queue between servers.
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect in-game statistics from all the servers and makes it available to the Prometheus endpoint on the controller.
- [Subspace Storage](https://github.com/clusterio/subspace_storage): Provide shared storage that can transport items between servers via teleport chests.
- [Player Auth](/plugins/player_auth/README.md): Provides authentication to the cluster via logging into a Factorio server.
+21
View File
@@ -2,6 +2,27 @@
Plugin synchronizing research progress between instances and the cluster.
## Research queue
Each server can follow one cluster research queue.
It is set from the normal research screen: queueing, moving or cancelling a technology there changes it for every following server.
So does a lab whose circuit "set research" picks a technology, and so can another mod.
The "Follow the cluster research queue" checkbox on the lab window turns this on or off per server.
Turning it on replaces the server's queue with the cluster's, turning it off clears it.
It is on by default.
On the first start after installing or upgrading, the first server to start shares its queue as the cluster queue, and later servers take it.
Every change prints a chat line on every server.
It needs Factorio 2.0.56 or later, older servers ignore the queue.
To turn it off without the GUI, run `/sc research_sync.set_follow(false)` on that server.
Known gaps:
- A script that assigns the queue directly to `{}` isn't seen.
- Two following servers whose labs want different research keep flipping the cluster queue, as two labs do on one server.
- A technology a server can't queue yet is dropped there, and an edit on that server then drops it for everyone.
## Installation
+3 -2
View File
@@ -21,6 +21,7 @@ type IpcFinished = {
name: string,
level: number,
};
type IpcQueue = { queue: unknown, player?: string };
export default async function(context: InstancePluginContext) {
const { instance, logger, plugin } = context;
@@ -68,14 +69,14 @@ export default async function(context: InstancePluginContext) {
researchFinished(tech).catch(err => unexpectedError(err));
});
instance.server.on("ipc-research_sync:queue", (data: { queue?: unknown, player?: string }) => {
instance.server.on("ipc-research_sync:queue", (data: IpcQueue) => {
try {
instance.sendTo("controller", new QueueUpdateEvent(asArray(data.queue), data.player ?? null));
} catch (err: any) {
unexpectedError(err);
}
});
instance.server.on("ipc-research_sync:follow", (data: { queue?: unknown, player?: string }) => {
instance.server.on("ipc-research_sync:follow", (data: IpcQueue) => {
followQueue(asArray(data.queue), data.player ?? null).catch(err => unexpectedError(err));
});
+7 -3
View File
@@ -40,7 +40,7 @@ end
--- @class ResearchSyncData
--- @field technologies table<string, ResearchSyncTechnology>
--- @field ignore_research_finished boolean
--- @field follow boolean? nil until first set; nil counts as following
--- @field follow boolean? nil until the first queue_state sets it; the checkbox shows ticked but nil does not send or apply
--- @field ignore_queue_events boolean
--- @field queue_dirty boolean
--- @field queue_player string?
@@ -159,7 +159,7 @@ end
--- @return string[]
local function queue_names(force)
local names = {}
for _, tech in pairs(force.research_queue) do
for _, tech in ipairs(force.research_queue) do
table.insert(names, tech.name)
end
return names
@@ -217,11 +217,13 @@ local function ensure_gui(player)
end
if queue_sync then
--- @param event EventData.on_player_created
sync.events[defines.events.on_player_created] = function(event)
ensure_gui(game.get_player(event.player_index) --[[@as LuaPlayer]])
end
sync.events[defines.events.on_player_joined_game] = sync.events[defines.events.on_player_created]
--- @param event EventData.on_gui_checked_state_changed
sync.events[defines.events.on_gui_checked_state_changed] = function(event)
if event.element.name == "research_sync_follow" then
local player = game.get_player(event.player_index) --[[@as LuaPlayer]]
@@ -229,6 +231,7 @@ if queue_sync then
end
end
--- @param event EventData.on_research_queued|EventData.on_research_moved|EventData.on_research_cancelled
local function on_queue_event(event)
if event.force.name ~= "player" then
return
@@ -247,7 +250,8 @@ if queue_sync then
sync.events[defines.events.on_research_cancelled] = on_queue_event
-- One send per tick however many events fired (a lab raises several at once).
sync.events[defines.events.on_tick] = function()
--- @param event EventData.on_tick
sync.events[defines.events.on_tick] = function(event)
local script_data = get_script_data()
if not script_data.queue_dirty then
return
+9
View File
@@ -63,6 +63,15 @@ describe("research_sync controller", function() {
await update(["a", "b"]);
assert.equal(queueEvents().length, 1);
});
it("should broadcast a clear to an empty queue", async function() {
await update(["a"], "Bob");
sent.length = 0;
await update([], null);
assert.equal(queueEvents().length, 1);
assert.deepEqual(queueEvents()[0][1].queue, []);
assert.equal(queueEvents()[0][1].player, null);
assert.deepEqual(await sync(null, null), []);
});
it("should cap the queue at 7", async function() {
await update(["1", "2", "3", "4", "5", "6", "7", "8", "9"]);
assert.deepEqual(queueEvents()[0][1].queue, ["1", "2", "3", "4", "5", "6", "7"]);
-1
View File
@@ -2,4 +2,3 @@
// required here in order to track coverage for them.
import "../dist/node/instance.js";
import "../dist/node/controller.js";