diff --git a/packages/lib/data/ModPack.js b/packages/lib/data/ModPack.js index 286dcea7..8279cf03 100644 --- a/packages/lib/data/ModPack.js +++ b/packages/lib/data/ModPack.js @@ -316,6 +316,40 @@ class ModPack { propertyTreeBytes(this.toJSON().settings), ]); } + + /** + * Fill in missing 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. + * + * @param {Object} settingPrototypes - + * Setting prototypes exported from the game. + * @param {Logger} logger - Logger used to report warnings on. + */ + fillDefaultSettings(settingPrototypes, logger) { + const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting"]; + let prototypes = Object.entries(settingPrototypes) + .filter(([type, _]) => knownTypes.includes(type)) + .flatMap(([_, settings]) => Object.values(settings)) + ; + + for (let prototype of prototypes) { + if (!["startup", "runtime-global", "runtime-per-user"].includes(prototype.setting_type)) { + logger.warn(`Ignoring ${prototype.name} with unknown setting_type ${prototype.setting_type}`); + continue; + } + if (prototype.default_value === undefined) { + logger.warn(`Ignoring ${prototype.name} with missing default_value`); + continue; + } + if (this.settings[prototype.setting_type].get(prototype.name)) { + continue; + } + + this.settings[prototype.setting_type].set(prototype.name, { value: prototype.default_value }); + } + } } module.exports = ModPack; diff --git a/packages/master/src/routes.js b/packages/master/src/routes.js index 4f14fda3..4e48f1f5 100644 --- a/packages/master/src/routes.js +++ b/packages/master/src/routes.js @@ -220,12 +220,17 @@ async function uploadExport(req, res) { ]; let assets = {}; + let settingPrototypes = {}; for (let filePath of exportFiles) { let file = zip.file(filePath); if (!file) { continue; } + if (filePath === "export/settings.json") { + settingPrototypes = JSON.parse(await file.async("text")); + } + let { name, ext } = path.posix.parse(filePath); let hash = await libHash.hashStream(file.nodeStream()); assets[name] = `${name}.${hash}${ext}`; @@ -233,6 +238,7 @@ async function uploadExport(req, res) { } modPack.exportManifest = new libData.ExportManifest({ assets }); + modPack.fillDefaultSettings(settingPrototypes, logger); res.app.locals.master.modPackUpdated(modPack); res.sendStatus(200); diff --git a/test/lib/data/ModPack.js b/test/lib/data/ModPack.js index 15f94ee7..43cdc856 100644 --- a/test/lib/data/ModPack.js +++ b/test/lib/data/ModPack.js @@ -76,6 +76,99 @@ describe("lib/data/ModPack", function() { assert.deepEqual(factorioMods.map(mod => mod.factorioVersion), sortedVersions); }); + describe(".fillDefaultSettings()", function() { + const prototypes = { + "bool-setting": { + "bool": { + name: "bool", + type: "bool-setting", + setting_type: "startup", + default_value: true, + }, + }, + "double-setting": { + "number": { + name: "number", + type: "double-setting", + setting_type: "runtime-global", + default_value: 123, + }, + }, + "string-setting": { + "string": { + name: "string", + type: "string-setting", + setting_type: "runtime-per-user", + default_value: "a string", + }, + }, + }; + const mockLogger = { warn: () => {} }; + it("should fill in defaults for settings", function() { + const pack = new ModPack(); + pack.fillDefaultSettings(prototypes, mockLogger); + assert.deepEqual(pack.toJSON().settings, { + "startup": { + "bool": { "value": true }, + }, + "runtime-global": { + "number": { "value": 123 }, + }, + "runtime-per-user": { + "string": { "value": "a string" }, + }, + }); + }); + it("should not overwrite existing values", function() { + const pack = new ModPack({ settings: { + "startup": { + "bool": { "value": false }, + }, + "runtime-global": { + "number": { "value": 2 }, + }, + "runtime-per-user": { + "string": { "value": "spam" }, + }, + }}); + pack.fillDefaultSettings(prototypes, mockLogger); + assert.deepEqual(pack.toJSON().settings, { + "startup": { + "bool": { "value": false }, + }, + "runtime-global": { + "number": { "value": 2 }, + }, + "runtime-per-user": { + "string": { "value": "spam" }, + }, + }); + }); + it("should ignore unknown setting_type and missing default_value", function() { + const pack = new ModPack(); + pack.fillDefaultSettings({ + "string-setting": { + "foo": { + name: "foo", + type: "string-setting", + setting_type: "magic-that-does-not-exist", + default_value: "a string", + }, + "bar": { + name: "bar", + type: "string-setting", + setting_type: "startup", + }, + }, + }, mockLogger); + assert.deepEqual(pack.toJSON().settings, { + "startup": {}, + "runtime-global": {}, + "runtime-per-user": {}, + }); + }); + }); + describe(".fromModPackString()", function() { it("should handle malformed strings", function() { assert.throws(