Files
factorio-clusterio/packages/lib/data/ModPack.js
T
Hornwitser 53a5d7ea6f Add ModPack.toModSettingsDat
Add utility function to convert the settings representation of a ModPack
into the mod-settings.dat format which Factorio reads.
2022-10-15 18:01:54 +02:00

311 lines
7.6 KiB
JavaScript

"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<string, module:lib/data.ModPack~ModRecord>}
*/
mods = new Map();
/**
* Mod settings for this mod pack
* @type {Object<string, Map<string, module:lib/data.ModPack~ModSetting>>=}
*/
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;