From 790c2834f2436d20596f7ea4e243af67a28ca215 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:36:19 +0000 Subject: [PATCH 1/3] Add validator to load_plugin --- packages/lib/src/config/definitions.ts | 81 ++++++++++++----- packages/lib/src/plugin.ts | 10 +++ test/lib/config/validators.test.js | 120 ++++++++++++++++++++----- 3 files changed, 165 insertions(+), 46 deletions(-) diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index 37eb17fa..b8257ecd 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -716,36 +716,73 @@ function validateFieldNames( } /** - * Add config fields defined by the provided plugin infos + * Add the config fields defined by the by a specific plugin infos + */ +export function addPluginFieldDefinitions( + pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo, + kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields", + Config: typeof classes.Config +) { + // Define plugin.load_plugin + const fieldDef: classes.FieldDefinition = { + title: "Load Plugin", + type: "boolean", + initialValue: true, + restartRequired: true, + }; + + // The validator is dynamic based on the feature requirements of the plugin + if (kind === "instanceConfigFields" && pluginInfo.features) { + fieldDef.dependsOn = []; + + const savePatching = pluginInfo.features.includes("SavePatching"); + if (savePatching) { + fieldDef.dependsOn.push("factorio.enable_save_patching"); + } + + const scriptCommands = pluginInfo.features.includes("ScriptCommands"); + if (scriptCommands) { + fieldDef.dependsOn.push("factorio.enable_script_commands"); + } + + if (scriptCommands || savePatching) { + fieldDef.validator = (enabled, config) => { + if (!enabled) { + return; + } + if (savePatching && !config.get("factorio.enable_save_patching")) { + throw new Error(`Plugin ${pluginInfo.name} requires save patching`); + } + if (scriptCommands && !config.get("factorio.enable_script_commands")) { + throw new Error(`Plugin ${pluginInfo.name} requires script commands`); + } + }; + } + } + + Config.fieldDefinitions[`${pluginInfo.name}.load_plugin`] = fieldDef; + + // Merge in other fields from the plugin + const fields = pluginInfo[kind]; + if (fields) { + validateFieldNames(pluginInfo.name, fields); + Object.assign(Config.fieldDefinitions, fields); + } +} + +/** + * Add all config fields defined by the provided plugin infos * * @param {Array} pluginInfos - Array of plugin info objects. */ export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) { - function pluginConfig( - pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo, - kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields", - Config: typeof ControllerConfig | typeof HostConfig | typeof InstanceConfig, - ) { - (Config.fieldDefinitions as any)[`${pluginInfo.name}.load_plugin`] = { - title: "Load Plugin", - restartRequired: true, - type: "boolean", - initialValue: true, - }; - - const fields = pluginInfo[kind]; - if (fields) { - validateFieldNames(pluginInfo.name, fields); - Object.assign(Config.fieldDefinitions, fields); - } - } for (let pluginInfo of pluginInfos) { - pluginConfig(pluginInfo, "controllerConfigFields", ControllerConfig); + addPluginFieldDefinitions(pluginInfo, "controllerConfigFields", ControllerConfig); if (pluginInfo.hostEntrypoint || pluginInfo.instanceEntrypoint) { - pluginConfig(pluginInfo, "hostConfigFields", HostConfig); + addPluginFieldDefinitions(pluginInfo, "hostConfigFields", HostConfig); } if (pluginInfo.instanceEntrypoint) { - pluginConfig(pluginInfo, "instanceConfigFields", InstanceConfig); + addPluginFieldDefinitions(pluginInfo, "instanceConfigFields", InstanceConfig); } } } diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index ae09788e..08a03ed3 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -7,6 +7,14 @@ import type { Logger } from "./logging"; import type { FieldDefinition } from "./config"; import type { PlayerStats } from "./data"; + +export const PluginFeatureFlags = [ + /** The plugin requires module code to be patched into the save */ + "SavePatching", + /** The plugin requires access to script commands over rcon */ + "ScriptCommands", +] as const; + /** Used to define the plugin export in plugins */ export type PluginDeclaration = { name: string; @@ -26,6 +34,8 @@ export type PluginDeclaration = { webEntrypoint?: string; controlConfigFields?: Record; + features?: (typeof PluginFeatureFlags)[number][]; + messages?: any[]; routes?: string[]; } diff --git a/test/lib/config/validators.test.js b/test/lib/config/validators.test.js index 876d95c5..29869fc9 100644 --- a/test/lib/config/validators.test.js +++ b/test/lib/config/validators.test.js @@ -207,35 +207,107 @@ describe("lib/config/definitions/validators", function() { ); }); - it("should validate factorio.player_online_autosave_slots against autosave slots", function() { - const config = new InstanceConfig("controller", { - "factorio.settings": { - autosave_slots: 5, - }, + describe("factorio.player_online_autosave_slots", function() { + it("should validate against autosave slots", function() { + const config = new InstanceConfig("controller", { + "factorio.settings": { + autosave_slots: 5, + }, + }); + + // valid: equal + assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 5)); + + // valid: greater + assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 6)); + + // invalid: less than autosave_slots + assert.throws( + () => config.set("factorio.player_online_autosave_slots", 4), + /Value cannot be less than the number of autosave slots/ + ); }); + it("should allow when autosave_slots is not a number", function() { + const config = new InstanceConfig("controller", { + "factorio.settings": { + autosave_slots: null, + }, + }); - // valid: equal - assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 5)); - - // valid: greater - assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 6)); - - // invalid: less than autosave_slots - assert.throws( - () => config.set("factorio.player_online_autosave_slots", 4), - /Value cannot be less than the number of autosave slots/ - ); + assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 0)); + assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 1)); + }); }); - it("should allow player_online_autosave_slots when autosave_slots is not a number", function() { - const config = new InstanceConfig("controller", { - "factorio.settings": { - autosave_slots: null, - }, - }); + describe("plugin.load_plugin", function() { + class TestInstanceConfig extends InstanceConfig { + static fieldDefinitions = { ...InstanceConfig.fieldDefinitions }; + } + it("should validate against factorio.enable_save_patching", function() { + lib.addPluginFieldDefinitions( + {name: "save_patching", features: ["SavePatching"] }, + "instanceConfigFields", TestInstanceConfig + ); - assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 0)); - assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 1)); + const config = new TestInstanceConfig("controller"); + + // valid: does not require script commands + assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false)); + + // valid: can have load disabled + assert.doesNotThrow(() => config.set("save_patching.load_plugin", false)); + + // valid: does not require save patching when not loaded + assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false)); + + // invalid: requires save patching when loaded + assert.throws( + () => config.set("save_patching.load_plugin", true), + /requires save patching/ + ); + }); + it("should validate against factorio.enable_script_commands", function() { + lib.addPluginFieldDefinitions( + { name: "script_commands", features: ["ScriptCommands"] }, + "instanceConfigFields", TestInstanceConfig + ); + + const config = new TestInstanceConfig("controller"); + + // valid: does not require save patching + assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false)); + + // valid: can have load disabled + assert.doesNotThrow(() => config.set("script_commands.load_plugin", false)); + + // valid: does not require script commands when not loaded + assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false)); + + // invalid: requires script commands when loaded + assert.throws( + () => config.set("script_commands.load_plugin", true), + /requires script commands/ + ); + }); + it("should allow when no feature flags are present", function() { + lib.addPluginFieldDefinitions( + { name: "no_features", features: [] }, + "instanceConfigFields", TestInstanceConfig + ); + + const config = new TestInstanceConfig("controller"); + + // valid: does not require save patching + assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false)); + + // valid: does not require script commands + assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false)); + + // valid: can have load disabled + assert.doesNotThrow(() => config.set("script_commands.load_plugin", false)); + + // no invalid case + }); }); }); From 622d3727763135fc83f7f7079cb26b0c1c629efc Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:44:30 +0000 Subject: [PATCH 2/3] Add feature flags to first party plugins --- plugins/global_chat/index.ts | 4 ++++ plugins/global_chat/instance.ts | 4 ---- plugins/inventory_sync/index.ts | 5 +++++ plugins/inventory_sync/instance.ts | 6 ------ plugins/player_auth/index.ts | 4 ++++ plugins/player_auth/instance.ts | 4 ---- plugins/research_sync/index.ts | 5 +++++ plugins/research_sync/instance.ts | 7 ------- plugins/statistics_exporter/index.ts | 5 +++++ plugins/statistics_exporter/instance.ts | 9 --------- plugins/subspace_storage/index.ts | 4 ++++ plugins/subspace_storage/instance.ts | 4 ---- 12 files changed, 27 insertions(+), 34 deletions(-) diff --git a/plugins/global_chat/index.ts b/plugins/global_chat/index.ts index 97c0985a..a02d5cfd 100644 --- a/plugins/global_chat/index.ts +++ b/plugins/global_chat/index.ts @@ -9,6 +9,10 @@ export const plugin: lib.PluginDeclaration = { instanceEntrypoint: "dist/node/instance", ctlEntrypoint: "dist/node/control", + features: [ + "ScriptCommands", + ], + messages: [ ChatEvent, ], diff --git a/plugins/global_chat/instance.ts b/plugins/global_chat/instance.ts index 10c0401c..2cfb2c6e 100644 --- a/plugins/global_chat/instance.ts +++ b/plugins/global_chat/instance.ts @@ -16,10 +16,6 @@ export class InstancePlugin extends BaseInstancePlugin { messageQueue: string[] = []; async init() { - if (!this.instance.config.get("factorio.enable_script_commands")) { - throw new Error("global_chat plugin requires script commands."); - } - this.instance.handle(ChatEvent, this.handleChatEvent.bind(this)); } diff --git a/plugins/inventory_sync/index.ts b/plugins/inventory_sync/index.ts index a796ee97..f38ec669 100644 --- a/plugins/inventory_sync/index.ts +++ b/plugins/inventory_sync/index.ts @@ -45,6 +45,11 @@ export const plugin: lib.PluginDeclaration = { }, }, + features: [ + "SavePatching", + "ScriptCommands", + ], + messages: [ messages.AcquireRequest, messages.ReleaseRequest, diff --git a/plugins/inventory_sync/instance.ts b/plugins/inventory_sync/instance.ts index 275d033e..c831e8fd 100644 --- a/plugins/inventory_sync/instance.ts +++ b/plugins/inventory_sync/instance.ts @@ -31,12 +31,6 @@ export class InstancePlugin extends BaseInstancePlugin { disconnecting!: boolean; async init() { - if (!this.instance.config.get("factorio.enable_save_patching")) { - throw new Error("inventory_sync plugin requires save patching."); - } - if (!this.instance.config.get("factorio.enable_script_commands")) { - throw new Error("inventory_sync plugin requires script commands."); - } this.playersToRelease = new Set(); this.disconnecting = false; diff --git a/plugins/player_auth/index.ts b/plugins/player_auth/index.ts index 62e7b109..79fa909d 100644 --- a/plugins/player_auth/index.ts +++ b/plugins/player_auth/index.ts @@ -33,6 +33,10 @@ export const plugin: lib.PluginDeclaration = { }, }, + features: [ + "SavePatching", + ], + messages: [ messages.FetchPlayerCodeRequest, messages.SetVerifyCodeRequest, diff --git a/plugins/player_auth/instance.ts b/plugins/player_auth/instance.ts index c6d90a4c..780dd1a3 100644 --- a/plugins/player_auth/instance.ts +++ b/plugins/player_auth/instance.ts @@ -13,10 +13,6 @@ type IpcPlayerAuth = { export class InstancePlugin extends BaseInstancePlugin { async init() { - if (!this.instance.config.get("factorio.enable_save_patching")) { - throw new Error("player_auth plugin requires save patching."); - } - this.instance.server.on( "ipc-player_auth", (ipcPlayerAuth: IpcPlayerAuth) => this.handleEvent(ipcPlayerAuth).catch( diff --git a/plugins/research_sync/index.ts b/plugins/research_sync/index.ts index 42f162d5..85afabdc 100644 --- a/plugins/research_sync/index.ts +++ b/plugins/research_sync/index.ts @@ -9,6 +9,11 @@ export const plugin: lib.PluginDeclaration = { instanceEntrypoint: "dist/node/instance", controllerEntrypoint: "dist/node/controller", + features: [ + "SavePatching", + "ScriptCommands", + ], + messages: [ messages.ContributionEvent, messages.ProgressEvent, diff --git a/plugins/research_sync/instance.ts b/plugins/research_sync/instance.ts index c905ff34..5ce28907 100644 --- a/plugins/research_sync/instance.ts +++ b/plugins/research_sync/instance.ts @@ -27,13 +27,6 @@ export class InstancePlugin extends BaseInstancePlugin { } async init() { - if (!this.instance.config.get("factorio.enable_save_patching")) { - throw new Error("research_sync plugin requires save patching."); - } - if (!this.instance.config.get("factorio.enable_script_commands")) { - throw new Error("research_sync plugin requires script commands."); - } - this.instance.server.on("ipc-research_sync:contribution", (tech: IpcContribution) => { this.researchContribution(tech).catch(err => this.unexpectedError(err)); }); diff --git a/plugins/statistics_exporter/index.ts b/plugins/statistics_exporter/index.ts index 5de5fc80..6e769973 100644 --- a/plugins/statistics_exporter/index.ts +++ b/plugins/statistics_exporter/index.ts @@ -22,4 +22,9 @@ export const plugin: lib.PluginDeclaration = { initialValue: 1, }, }, + + features: [ + "SavePatching", + "ScriptCommands", + ], }; diff --git a/plugins/statistics_exporter/instance.ts b/plugins/statistics_exporter/instance.ts index 2557089c..c76bf1f0 100644 --- a/plugins/statistics_exporter/instance.ts +++ b/plugins/statistics_exporter/instance.ts @@ -104,15 +104,6 @@ function setForceFlowStatistic( export class InstancePlugin extends BaseInstancePlugin { - async init() { - if (!this.instance.config.get("factorio.enable_save_patching")) { - throw new Error("statistics_exporter plugin requires save patching."); - } - if (!this.instance.config.get("factorio.enable_script_commands")) { - throw new Error("statistics_exporter plugin requires script commands."); - } - } - async gatherMetrics() { let string = await this.sendRcon("/sc statistics_exporter.export()"); let stats: IpcStats; diff --git a/plugins/subspace_storage/index.ts b/plugins/subspace_storage/index.ts index 1909ef29..300c8895 100644 --- a/plugins/subspace_storage/index.ts +++ b/plugins/subspace_storage/index.ts @@ -49,6 +49,10 @@ export const plugin: lib.PluginDeclaration = { }, }, + features: [ + "ScriptCommands", + ], + messages: [ messages.PlaceEvent, messages.RemoveRequest, diff --git a/plugins/subspace_storage/instance.ts b/plugins/subspace_storage/instance.ts index 77cfd818..4de15afb 100644 --- a/plugins/subspace_storage/instance.ts +++ b/plugins/subspace_storage/instance.ts @@ -22,10 +22,6 @@ export class InstancePlugin extends BaseInstancePlugin { } async init() { - if (!this.instance.config.get("factorio.enable_script_commands")) { - throw new Error("subspace_storage plugin requires script commands."); - } - this.pendingTasks = new Set(); this.instance.server.on("ipc-subspace_storage:output", (output: IpcItems) => { this.logger.info("Received output items:"); From 2afd7aab387bdafb0a2a1274158f4a108797d15a Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:25:53 +0000 Subject: [PATCH 3/3] Fix failing tests --- test/integration/clusterio.js | 18 ++++++++++++++++- test/lib/config/validators.test.js | 31 +++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index be234991..6bc7e9b8 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -763,9 +763,25 @@ describe("Integration of Clusterio", function() { describe("instance send-rcon", function() { requiresFactorio(this); + const plugins = [ + "global_chat", "inventory_sync", "player_auth", + "research_sync", "statistics_exporter", "subspace_storage", + ]; + this.beforeAll(async function() { + this.timeout(10000); + // Disable plugins as they interfere with enable_script_commands test + for (const plugin of plugins) { + await execCtl(`instance config set test ${plugin}.load_plugin false`); + } + }); this.afterAll(async function() { + this.timeout(10000); // Prevents cascading failure where enable_script_commands is expected to be true await execCtl("instance config set test factorio.enable_script_commands true"); + // Enable plugins after they were previously disabled + for (const plugin of plugins) { + await execCtl(`instance config set test ${plugin}.load_plugin true`); + } }); it("sends the command", async function() { slowTest(this); @@ -1097,12 +1113,12 @@ describe("Integration of Clusterio", function() { it("starts the instance with the given settings", async function() { slowTest(this); requiresFactorio(this); - await execCtl("instance config set 44 factorio.enable_save_patching false"); await execCtl("instance config set 44 player_auth.load_plugin false"); await execCtl("instance config set 44 research_sync.load_plugin false"); await execCtl("instance config set 44 statistics_exporter.load_plugin false"); await execCtl("instance config set 44 subspace_storage.load_plugin false"); await execCtl("instance config set 44 inventory_sync.load_plugin false"); + await execCtl("instance config set 44 factorio.enable_save_patching false"); await execCtl("instance start 44"); const instance = (await getInstances()).get(44); diff --git a/test/lib/config/validators.test.js b/test/lib/config/validators.test.js index 29869fc9..85bce149 100644 --- a/test/lib/config/validators.test.js +++ b/test/lib/config/validators.test.js @@ -240,16 +240,25 @@ describe("lib/config/definitions/validators", function() { }); describe("plugin.load_plugin", function() { - class TestInstanceConfig extends InstanceConfig { - static fieldDefinitions = { ...InstanceConfig.fieldDefinitions }; - } + const initialConfigFields = { + "global_chat.load_plugin": false, + "inventory_sync.load_plugin": false, + "player_auth.load_plugin": false, + "research_sync.load_plugin": false, + "statistics_exporter.load_plugin": false, + "subspace_storage.load_plugin": false, + }; it("should validate against factorio.enable_save_patching", function() { + class TestInstanceConfig extends InstanceConfig { + static fieldDefinitions = { ...InstanceConfig.fieldDefinitions }; + } + lib.addPluginFieldDefinitions( {name: "save_patching", features: ["SavePatching"] }, "instanceConfigFields", TestInstanceConfig ); - const config = new TestInstanceConfig("controller"); + const config = new TestInstanceConfig("controller", initialConfigFields); // valid: does not require script commands assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false)); @@ -267,12 +276,16 @@ describe("lib/config/definitions/validators", function() { ); }); it("should validate against factorio.enable_script_commands", function() { + class TestInstanceConfig extends InstanceConfig { + static fieldDefinitions = { ...InstanceConfig.fieldDefinitions }; + } + lib.addPluginFieldDefinitions( { name: "script_commands", features: ["ScriptCommands"] }, "instanceConfigFields", TestInstanceConfig ); - const config = new TestInstanceConfig("controller"); + const config = new TestInstanceConfig("controller", initialConfigFields); // valid: does not require save patching assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false)); @@ -290,12 +303,16 @@ describe("lib/config/definitions/validators", function() { ); }); it("should allow when no feature flags are present", function() { + class TestInstanceConfig extends InstanceConfig { + static fieldDefinitions = { ...InstanceConfig.fieldDefinitions }; + } + lib.addPluginFieldDefinitions( { name: "no_features", features: [] }, "instanceConfigFields", TestInstanceConfig ); - const config = new TestInstanceConfig("controller"); + const config = new TestInstanceConfig("controller", initialConfigFields); // valid: does not require save patching assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false)); @@ -304,7 +321,7 @@ describe("lib/config/definitions/validators", function() { assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false)); // valid: can have load disabled - assert.doesNotThrow(() => config.set("script_commands.load_plugin", false)); + assert.doesNotThrow(() => config.set("no_features.load_plugin", false)); // no invalid case });