diff --git a/CHANGELOG.md b/CHANGELOG.md index 31908653..9dd176b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,14 +28,22 @@ Many thanks to the following for contributing to this release: ### Features - Added logging of HTTP requests and errors on the controller [#556](https://github.com/clusterio/clusterio/pull/556). +- Added caching of data parsed from mods to speed up startup time [#606](https://github.com/clusterio/clusterio/pull/606). +- Added command to installer to generate the code for making a new plugin [#570](https://github.com/clusterio/clusterio/pull/570). ### Changes -- Fixed Inventory Sync reporting a negative database size [ยค598](https://github.com/clusterio/clusterio/pull/598). +- Fixed Inventory Sync reporting a negative database size [#598](https://github.com/clusterio/clusterio/pull/598). - Fixed plugin events being invoked while a host connection was in an invalid state. +- Suppresed bogus warning about event listener leak in the browser's console [#600](https://github.com/clusterio/clusterio/pull/600). +- Fixed installer breaking on Windows after Node.js released a security fix [#614](https://github.com/clusterio/clusterio/pull/614). +- During a data export the mod settings in mod packs will now be corrected to the type of the settings prototype if the type is incorrect. +- Fixed missing color setting causing the Mod Pack view in the Web UI to show an error [#609](https://github.com/clusterio/clusterio/issues/609). Many thanks to the following for contributing to this release: [@Cooldude2606](https://github.com/Cooldude2606) +[@Danielv123](https://github.com/Danielv123) +[@Hornwitser](https://github.com/Hornwitser) ## Version 2.0.0-alpha.17 diff --git a/package.json b/package.json index a5f4a506..e62b1c7f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "tsc --build", "watch": "tsc --build --watch", - "test": "pnpm run build && mocha --check-leaks -R spec --recursive test \"plugins/*/test/**\"", + "test": "pnpm run build && mocha --check-leaks -R spec --recursive test \"plugins/*/test/**\" --exclude \"test/manual/**\"", "fast-test": "FAST_TEST=y pnpm test", "cover": "nyc pnpm test", "fast-cover": "FAST_TEST=y nyc pnpm test", diff --git a/packages/controller/package.json b/packages/controller/package.json index 61a728e4..70238955 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -3,7 +3,7 @@ "description": "Implementation of Clusterio controller", "author": "Hornwitser", "license": "MIT", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --env production", @@ -19,7 +19,7 @@ "node": ">=18" }, "dependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", "@sinclair/typebox": "^0.30.4", "busboy": "^1.6.0", "compression": "^1.7.4", @@ -36,7 +36,7 @@ "yargs": "^17.7.2" }, "devDependencies": { - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@swc/core": "^1.4.0", "@types/busboy": "^1.5.0", "@types/compression": "^1.7.2", diff --git a/packages/create/create.js b/packages/create/create.js index 66e4084a..3126f0dc 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -108,7 +108,7 @@ async function safeOutputFile(file, data, options={}) { async function execFile(cmd, args) { logger.verbose(`executing ${cmd} ${args.join(" ")}`); return new Promise((resolve, reject) => { - let child = child_process.execFile(cmd, args, (err, stdout, stderr) => { + let child = child_process.execFile(cmd, args, { shell: true }, (err, stdout, stderr) => { if (err) { reject(err); } else { diff --git a/packages/create/package.json b/packages/create/package.json index 7929cbb1..814e26f0 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -3,7 +3,7 @@ "description": "Installer for clusterio", "author": "Hornwitser", "license": "MIT", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "main": "create.js", "bin": { "clusteriocreate": "./create.js" diff --git a/packages/ctl/package.json b/packages/ctl/package.json index 879afe37..4cfa6b84 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -3,7 +3,7 @@ "description": "Clusterio command line management tool", "author": "Hornwitser", "license": "MIT", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build", @@ -19,7 +19,7 @@ "node": ">=18" }, "dependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", "as-table": "^1.0.55", "fs-extra": "^11.1.1", "phin": "^3.7.0", diff --git a/packages/host/package.json b/packages/host/package.json index b339d0a0..613d40fd 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -3,7 +3,7 @@ "description": "Implementation of Clusterio host server", "author": "Hornwitser", "license": "MIT", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build", @@ -19,7 +19,7 @@ "node": ">=18" }, "dependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", "@sinclair/typebox": "^0.30.4", "fs-extra": "^11.1.1", "jimp": "^0.22.8", diff --git a/packages/host/src/patch.ts b/packages/host/src/patch.ts index 05497637..c0047667 100644 --- a/packages/host/src/patch.ts +++ b/packages/host/src/patch.ts @@ -77,9 +77,16 @@ export class SaveModule { static async fromSave(json: Static, root: JSZip) { const module = new this(lib.ModuleInfo.fromJSON(json)); - module.files = new Map( - await Promise.all(json.files.map(async f => [f, await root.file(f)!.async("nodebuffer")] as const)) - ); + module.files = new Map(await Promise.all(json.files + .map(filename => ({filename, file: root.file(filename)})) + .filter(({filename, file}) => { + if (file === null) { + lib.logger.warn(`Missing file ${filename} in save`); + } + return file !== null; + }) + .map(async ({filename, file}) => [filename, await file!.async("nodebuffer")] as const) + )); return module; } @@ -395,8 +402,11 @@ export async function patch(savePath: string, modules: SaveModule[]) { } } else { for (let module of patchInfo.modules) { - for (let file of module.files.keys()) { - zip.remove(root.file(file)!.name); + for (let filepath of module.files.keys()) { + const file = root.file(filepath); + if (file !== null) { + zip.remove(file.name); + } } } patchInfo.modules = []; diff --git a/packages/lib/package.json b/packages/lib/package.json index 82afa633..bf27858b 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -1,7 +1,7 @@ { "name": "@clusterio/lib", "description": "Shared library for Clusterio", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "repository": "https://github.com/clusterio/clusterio", "license": "MIT", "scripts": { diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 85dba1fb..d8fa5b3a 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -334,10 +334,11 @@ export default class ModPack { } /** - * Fill in missing settings with their default values + * Fill in missing and incorrect settings with their default values * - * Uses the provided setting prototypes to add any missing mod settings - * in the mod pack with the default value from the prototype. + * Uses the provided setting prototypes to correct any missing and or + * incorrect mod settings in the mod pack with the default value from + * the prototype. * * @param settingPrototypes - * Setting prototypes exported from the game. @@ -360,13 +361,20 @@ export default class ModPack { logger.warn(`Ignoring ${prototype.name} with missing default_value`); continue; } - if (this.settings[settingType].get(prototype.name)) { - continue; + let value = this.settings[settingType].get(prototype.name)?.value ?? prototype.default_value; + const type = prototype.type; + if ( + type === "bool-setting" && typeof value !== "boolean" + || type === "color-setting" && typeof value !== "object" + || (type === "int-setting" || type === "double-setting") && typeof value !== "number" + ) { + value = prototype.default_value; } - - let value = prototype.default_value; - if (prototype.type === "color-setting") { - value = libLuaTools.normalizeColor(value); + if (type === "string-setting" && typeof value !== "string") { + value = typeof value === "object" ? prototype.default_value : String(value ?? ""); + } + if (type === "color-setting") { + value = libLuaTools.normalizeColor(value as object); } this.settings[settingType].set(prototype.name, { value }); } diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index 0c7c4cc8..97c6aab5 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -3,7 +3,7 @@ "description": "Clusterio web interface implementation", "author": "Hornwitser", "license": "MIT", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "browser": "src/index.ts", "main": "dist/browser/src/index.js", "scripts": { @@ -17,7 +17,7 @@ }, "dependencies": { "@ant-design/icons": "^5.1.4", - "@clusterio/lib": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", "@swc/core": "^1.4.0", "antd": "^5.13.0", "assert": "^2.0.0", diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 840155f2..93f07889 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -424,7 +424,15 @@ function InputColor(props: { value: lib.ModSettingColor, onChange: (value: lib.ModSettingColor) => void, }) { - const value = props.value; + // In certain edge cases it's possible a value for this mod setting does not exist + // or is of the wrong type. Fallback to black if this is the case. + function isColor(input: unknown) { + if (typeof input !== "object" || input === null) { + return false; + } + return "r" in input && "g" in input && "b" in input && "a" in input; + } + const value = isColor(props.value) ? props.value : { r: 0, g: 0, b: 0, a: 1 }; return ", "homepage": "https://github.com/clusterio/clusterio#readme", @@ -21,8 +21,8 @@ "@clusterio/lib": "^2.0.0-alpha.0" }, "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@types/node": "^20.4.5", "@types/react": "^18.2.21", "typescript": "^5.1.6", diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index 566e4006..d98b9ea7 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -1,6 +1,6 @@ { "name": "@clusterio/plugin-inventory_sync", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "description": "Clusterio plugin forwarding between Factorio servers", "author": "Danielv ", "homepage": "https://github.com/clusterio/factorioClusterio#readme", @@ -25,8 +25,8 @@ "fs-extra": "^8.1.0" }, "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "@types/react": "^18.2.21", diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index b65f9186..c2e35762 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -1,6 +1,6 @@ { "name": "@clusterio/plugin-player_auth", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "description": "Clusterio plugin authenticating logged in players to the web interface", "author": "Hornwitser ", "homepage": "https://github.com/clusterio/clusterio#readme", @@ -26,8 +26,8 @@ "jsonwebtoken": "^9.0.1" }, "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.4.5", diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index ed6c298a..d1bf14fa 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -1,6 +1,6 @@ { "name": "@clusterio/plugin-research_sync", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "description": "Clusterio plugin for synchronizing research between Factorio servers", "author": "Hornwitser ", "homepage": "https://github.com/clusterio/clusterio#readme", @@ -25,8 +25,8 @@ "fs-extra": "^8.1.0" }, "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "@types/react": "^18.2.21", diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index c1bb9490..68a4d8f1 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -1,6 +1,6 @@ { "name": "@clusterio/plugin-statistics_exporter", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "description": "Clusterio plugin for gathering statistics from Factorio servers", "author": "Hornwitser ", "homepage": "https://github.com/clusterio/clusterio#readme", @@ -21,8 +21,8 @@ "@clusterio/lib": "^2.0.0-alpha.0" }, "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@types/node": "^20.4.5", "@types/react": "^18.2.21", "typescript": "^5.1.6", diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index 22fd77a8..288b4743 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -1,6 +1,6 @@ { "name": "@clusterio/plugin-subspace_storage", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "description": "Clusterio plugin for sharing storage between Factorio servers", "author": "Hornwitser ", "homepage": "https://github.com/clusterio/clusterio#readme", @@ -25,8 +25,8 @@ "fs-extra": "^8.1.0" }, "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.17", - "@clusterio/web_ui": "^2.0.0-alpha.17", + "@clusterio/lib": "^2.0.0-alpha.18", + "@clusterio/web_ui": "^2.0.0-alpha.18", "@types/express": "^4.17.17", "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", diff --git a/test/lib/data/ModPack.js b/test/lib/data/ModPack.js index 14616da3..d1fc713e 100644 --- a/test/lib/data/ModPack.js +++ b/test/lib/data/ModPack.js @@ -179,6 +179,84 @@ describe("lib/data/ModPack", function() { "runtime-per-user": {}, }); }); + it("should coerce values of the incorrect type into the correct type", function() { + const boolDefault = true; + const intDefault = 123; + const doubleDefault = 1234.5; + const stringDefault = "default"; + const colorDefault = { r: 1, g: 0, b: 1, a: 1}; + const settingDefaultValues = { + "bool-setting": boolDefault, + "int-setting": intDefault, + "double-setting": doubleDefault, + "string-setting": stringDefault, + "color-setting": colorDefault, + }; + const possibleValues = ["bool", "int", "double", "string", "color"]; + const settingPrototypes = { + "bool-setting": {}, + "int-setting": {}, + "double-setting": {}, + "string-setting": {}, + "color-setting": {}, + }; + for (const [type, default_value] of Object.entries(settingDefaultValues)) { + for (const value of possibleValues) { + const settingName = `${type}-with-${value}-value`; + settingPrototypes[type][settingName] = { + type, + name: settingName, + setting_type: "startup", + default_value, + }; + } + }; + + const pack = new ModPack(); + const settingValues = { + "bool": false, + "int": 1, + "double": 2.5, + "string": "str", + "color": { r: 0, g: 1, b: 0, a: 1 }, + }; + for (const type of Object.keys(settingDefaultValues)) { + for (const [valueType, value] of Object.entries(settingValues)) { + pack.settings.startup.set(`${type}-with-${valueType}-value`, { value }); + } + } + pack.fillDefaultSettings(settingPrototypes); + assert.deepEqual( + pack.settings["startup"], + new Map([ + ["bool-setting-with-bool-value", false], + ["bool-setting-with-int-value", boolDefault], + ["bool-setting-with-double-value", boolDefault], + ["bool-setting-with-string-value", boolDefault], + ["bool-setting-with-color-value", boolDefault], + ["int-setting-with-bool-value", intDefault], + ["int-setting-with-int-value", 1], + ["int-setting-with-double-value", 2.5], + ["int-setting-with-string-value", intDefault], + ["int-setting-with-color-value", intDefault], + ["double-setting-with-bool-value", doubleDefault], + ["double-setting-with-int-value", 1], + ["double-setting-with-double-value", 2.5], + ["double-setting-with-string-value", doubleDefault], + ["double-setting-with-color-value", doubleDefault], + ["string-setting-with-bool-value", "false"], + ["string-setting-with-int-value", "1"], + ["string-setting-with-double-value", "2.5"], + ["string-setting-with-string-value", "str"], + ["string-setting-with-color-value", stringDefault], + ["color-setting-with-bool-value", colorDefault], + ["color-setting-with-int-value", colorDefault], + ["color-setting-with-double-value", colorDefault], + ["color-setting-with-string-value", colorDefault], + ["color-setting-with-color-value", { r: 0, g: 1, b: 0, a: 1}], + ].map(([k, v]) => [k, { value: v }])), + ); + }); }); describe(".fromModPackString()", function() { diff --git a/test/manual/README.md b/test/manual/README.md new file mode 100644 index 00000000..3b24dc23 --- /dev/null +++ b/test/manual/README.md @@ -0,0 +1,35 @@ +# Manual Tests + +This folder contains files used for performing manual tests that are non-trivial to automate. + +## Setup test mod + +Run `node packages/lib/dist/node/build_mod.js --source-dir test/manual/test_mod/ --output-dir mods/` to create the test mod. + +## Test handling of bad setting values + +1. Create a ModPack with missmatched values with `node test/manual/test_settings_mod_pack.js` and import it. +2. Run a data export on an instance with the mod pack assigned, the values will get coerced to the correct types. +3. Use ctl to set the settings back to their wrong values: + + ```bash + SETTING_TYPES=(bool-setting int-setting double-setting string-setting color-setting) + VALUE_TYPES=(bool int double string color missing) + VALUES=( + --bool-setting false + --int-setting 1 + --double-setting 0.5 + --string-setting str + --color-setting '{"r":0,"g":1,"b":0,"a":1}' + --remove-setting + ) + node packages/ctl mod-pack edit test-settings $( + for TYPE in "${SETTING_TYPES[@]}" ; do + for (( I = 0; I < ${#VALUE_TYPES[@]}; I++ )); do + printf "%s startup %s-with-%s-value %s " ${VALUES[((I * 2))]} $TYPE ${VALUE_TYPES[I]} ${VALUES[((I * 2 + 1))]} + done + done + ) + ``` + +4. Verify that the Web UI gracefully handles all combinations of wrong settings types. diff --git a/test/manual/test_mod/info.json b/test/manual/test_mod/info.json new file mode 100644 index 00000000..7d3cf54d --- /dev/null +++ b/test/manual/test_mod/info.json @@ -0,0 +1,7 @@ +{ + "name": "test_mod", + "version": "0.0.0", + "title": "test_mod", + "author": "", + "factorio_version": "1.1" +} diff --git a/test/manual/test_mod/settings.lua b/test/manual/test_mod/settings.lua new file mode 100644 index 00000000..efcdd49c --- /dev/null +++ b/test/manual/test_mod/settings.lua @@ -0,0 +1,22 @@ +local settings = { + ["bool-setting"] = true, + ["int-setting"] = 123, + ["double-setting"] = 1234.5, + ["string-setting"] = "string", + ["color-setting"] = { r = 1, g = 0, b = 1, a = 1 }, +} + +local possible_values = { "missing", "bool", "int", "double", "string", "color" } + +for name, default_value in pairs(settings) do + for _, value in ipairs(possible_values) do + data:extend({ + { + type = name, + name = name .. "-with-" .. value .. "-value", + setting_type = "startup", + default_value = default_value, + } + }) + end +end diff --git a/test/manual/test_settings_mod_pack.js b/test/manual/test_settings_mod_pack.js new file mode 100644 index 00000000..fe2a699f --- /dev/null +++ b/test/manual/test_settings_mod_pack.js @@ -0,0 +1,28 @@ +"use strict"; +const { ModPack } = require("@clusterio/lib"); + +const pack = new ModPack(); +pack.name = "test-settings"; +pack.mods.set("test_mod", { name: "test_mod", enabled: true, version: "0.0.0" }); +const settingTypes = [ + "bool-setting", + "int-setting", + "double-setting", + "string-setting", + "color-setting", +]; +const settingValues = { + "bool": false, + "int": 1, + "double": 0.5, + "string": "str", + "color": { r: 0, g: 1, b: 0, a: 1 }, + // "missing": +}; +for (const type of settingTypes) { + for (const [valueType, value] of Object.entries(settingValues)) { + pack.settings.startup.set(`${type}-with-${valueType}-value`, { value }); + } +} +/* eslint-disable no-console */ +console.log(pack.toModPackString());