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 + }); }); });