diff --git a/README.md b/README.md index 5955e302..52ac01bc 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/plugins/research_sync/README.md b/plugins/research_sync/README.md index 46315762..0c0889a9 100644 --- a/plugins/research_sync/README.md +++ b/plugins/research_sync/README.md @@ -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 diff --git a/plugins/research_sync/instance.ts b/plugins/research_sync/instance.ts index c79617c9..38457c03 100644 --- a/plugins/research_sync/instance.ts +++ b/plugins/research_sync/instance.ts @@ -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)); }); diff --git a/plugins/research_sync/module/sync.lua b/plugins/research_sync/module/sync.lua index b3da483f..81ab132d 100644 --- a/plugins/research_sync/module/sync.lua +++ b/plugins/research_sync/module/sync.lua @@ -40,7 +40,7 @@ end --- @class ResearchSyncData --- @field technologies table --- @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 diff --git a/plugins/research_sync/test/controller.js b/plugins/research_sync/test/controller.js index dcbd2dfb..b3cf0a9d 100644 --- a/plugins/research_sync/test/controller.js +++ b/plugins/research_sync/test/controller.js @@ -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"]); diff --git a/plugins/research_sync/test/missing.js b/plugins/research_sync/test/missing.js index 4d585e2b..579cf2ed 100644 --- a/plugins/research_sync/test/missing.js +++ b/plugins/research_sync/test/missing.js @@ -2,4 +2,3 @@ // required here in order to track coverage for them. import "../dist/node/instance.js"; -import "../dist/node/controller.js";