"use strict"; const zlib = require("zlib"); const libSchema = require("../schema"); const { integerFactorioVersion } = require("./version"); /** * A setting for a mod. * @typedef {object} module:lib/data.ModPack~ModSetting * @property {boolean|number|string} value - Value of the given mod setting. */ const ModSettingJsonSchema = { type: "object", additionalProperties: false, required: ["value"], properties: { "value": { type: ["boolean", "number", "string"] }, }, }; const ModSettingsJsonSchema = { type: "object", additionalProperties: ModSettingJsonSchema, }; /** * A mod in a mod pack * @typedef {object} module:lib/data.ModPack~ModRecord * @property {string} name - name of the mod. * @property {boolean} enabled - if mod is to be loaded. * @property {string} version - version of the mod. * @property {string=} sha1 - SHA1 hash of the zip file. */ const ModRecordJsonSchema = { type: "object", required: ["name", "enabled", "version"], properties: { "name": { type: "string" }, "enabled": { type: "boolean" }, "version": { type: "string" }, "sha1": { type: "string" }, }, }; /** * Factorio Mod Pack * * Tracks mods and settings for a collection of Factorio mods. * @alias module:lib/data.ModPack */ class ModPack { /** * Id of this mod pack * @type {number} */ id; /** * Name of this mod pack. * @type {string} */ name = ""; /** * Description for this modpcak. * @type {string} */ description = ""; /** * Version of Factorio this mod pack is for * @type {string} */ factorioVersion = "1.1.0"; /** * Integer representation of the factorioVersion * @type {number} */ get integerFactorioVersion() { return integerFactorioVersion(this.factorioVersion); } /** * Mods included in this mod pack * @type {Map} */ mods = new Map(); /** * Mod settings for this mod pack * @type {Object>=} */ settings = { "startup": new Map(), "runtime-global": new Map(), "runtime-per-user": new Map(), }; /** * True if this mod pack has been deleted from the list of mod packs. * @type {boolean} */ isDeleted = false; static jsonSchema = { type: "object", additionalProperties: false, required: ["name", "description", "factorio_version", "mods", "settings"], properties: { "id": { type: "integer" }, "name": { type: "string" }, "description": { type: "string" }, "factorio_version": { type: "string" }, "mods": { type: "array", items: ModRecordJsonSchema, }, "settings": { type: "object", additionalProperties: false, required: ["startup", "runtime-global", "runtime-per-user"], properties: { "startup": ModSettingsJsonSchema, "runtime-global": ModSettingsJsonSchema, "runtime-per-user": ModSettingsJsonSchema, }, }, "is_deleted": { type: "boolean" }, }, }; static validate = libSchema.compile(this.jsonSchema); constructor(json = {}) { if (json.id) { this.id = json.id; } else { this.id = Math.random() * 2**31 | 0; } if (json.name) { this.name = json.name; } if (json.description) { this.description = json.description; } if (json.factorio_version) { this.factorioVersion = json.factorio_version; } if (json.mods) { this.mods = new Map(json.mods.map(m => [m.name, m])); } if (json.settings) { this.settings = { "startup": new Map(Object.entries(json.settings["startup"])), "runtime-global": new Map(Object.entries(json.settings["runtime-global"])), "runtime-per-user": new Map(Object.entries(json.settings["runtime-per-user"])), }; } if (json.is_deleted) { this.isDeleted = json.is_deleted; } if (!this.mods.has("base")) { this.mods.set("base", { name: "base", enabled: true, version: this.factorioVersion }); } } toJSON() { let json = { id: this.id, name: this.name, description: this.description, factorio_version: this.factorioVersion, mods: [...this.mods.values()], settings: { "startup": Object.fromEntries(this.settings["startup"]), "runtime-global": Object.fromEntries(this.settings["runtime-global"]), "runtime-per-user": Object.fromEntries(this.settings["runtime-per-user"]), }, }; if (this.isDeleted) { json.is_deleted = this.isDeleted; } return json; } toModPackString() { const json = this.toJSON(); delete json.id; // eslint-disable-next-line node/no-sync let buf = zlib.deflateSync(JSON.stringify(json)); return buf.toString("base64"); } static fromModPackString(modPackString) { let buf = Buffer.from(modPackString, "base64"); try { // eslint-disable-next-line node/no-sync buf = zlib.inflateSync(buf); } catch (err) { if (err.code.startsWith("Z_")) { throw new Error("Malformed mod pack string: zlib inflate failed"); } } let json; try { json = JSON.parse(buf); } catch (err) { throw new Error(`Malformed mod pack string: ${err.message}`); } if (!this.validate(json)) { throw new Error("Malformed mod pack string: Schema validation failed"); } return new this(json); } toModSettingsDat() { function uint8Byte(int) { return Buffer.from(Uint8Array.from([int]).buffer); } function int16Bytes(int) { return Buffer.from(Int16Array.from([int]).buffer); } function uint32Bytes(int) { return Buffer.from(Uint32Array.from([int]).buffer); } function uint32SpaceOptimizedBytes(int) { if (0 <= int && int < 0xff) { return uint8Byte(int); } return Buffer.concat([ uint8Byte(0xff), uint32Bytes(int), ]); } function doubleBytes(double) { return Buffer.from(Float64Array.from([double]).buffer); } function stringBytes(str) { const buf = Buffer.from(str, "utf8"); return Buffer.concat([ uint32SpaceOptimizedBytes(buf.length), buf, ]); } function immutableStringBytes(str) { if (str === undefined) { return uint8Byte(1); // empty } return Buffer.concat([ uint8Byte(0), // empty stringBytes(str), ]); } function versionBytes(version) { const [main, major, minor] = version.split(".").map(n => Number.parseInt(n, 10)); return Buffer.concat([ int16Bytes(main), int16Bytes(major), int16Bytes(minor), Buffer.alloc(2), // developer Buffer.alloc(1), // reserved byte ]); } function propertyTreeListBytes(entries) { const sizeBytes = uint32Bytes(entries.length); const itemBytes = entries.flatMap(([key, item]) => [ immutableStringBytes(key), // eslint-disable-next-line no-use-before-define propertyTreeBytes(item), ]); return Buffer.concat([ sizeBytes, ...itemBytes, ]); } function propertyTreeBytes(element) { let type; let dataBytes; if (typeof element === null) { type = 1; dataBytes = Buffer.alloc(0); } else if (typeof element === "boolean") { type = 1; dataBytes = uint8Byte(element); } else if (typeof element === "number") { type = 2; dataBytes = doubleBytes(element); } else if (typeof element === "string") { type = 3; dataBytes = immutableStringBytes(element); } else if (element instanceof Array) { type = 4; dataBytes = propertyTreeListBytes(element.map(item => [undefined, item])); } else if (typeof element === "object") { type = 5; dataBytes = propertyTreeListBytes(Object.entries(element)); } else { throw new Error("Bad element passed to propertyTreeBytes"); } return Buffer.concat([ uint8Byte(type), Buffer.alloc(1), // anyTypeFlag dataBytes, ]); } return Buffer.concat([ versionBytes(this.factorioVersion), propertyTreeBytes(this.toJSON().settings), ]); } } module.exports = ModPack;