mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Move all functions and classes currently available through lib/name and libName to be under a single flat namespace in lib. This make reorganising the lib namespace much simpler as the location of symbols can move without breaking the consumers of lib. The multi-entrypoint design was already problematic for the web plugin system which couldn't effectively use it due to how webpack's shared module system is awkward to set up with multiple entrypoints per package.
373 lines
9.7 KiB
JavaScript
373 lines
9.7 KiB
JavaScript
"use strict";
|
|
const zlib = require("zlib");
|
|
|
|
const libSchema = require("../schema");
|
|
|
|
const ExportManifest = require("./ExportManifest");
|
|
const { integerFactorioVersion } = require("./version");
|
|
|
|
|
|
/**
|
|
* A setting for a mod.
|
|
* @typedef {object} module:lib.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.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.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.ModPack~ModRecord>}
|
|
*/
|
|
mods = new Map();
|
|
|
|
/**
|
|
* Mod settings for this mod pack
|
|
* @type {Object<string, Map<string, module:lib.ModPack~ModSetting>>=}
|
|
*/
|
|
settings = {
|
|
"startup": new Map(),
|
|
"runtime-global": new Map(),
|
|
"runtime-per-user": new Map(),
|
|
};
|
|
|
|
/**
|
|
* Mapping to files containing exported data for this modpack
|
|
* @type {module:lib.ExportManifest|undefined}
|
|
*/
|
|
exportManifest;
|
|
|
|
/**
|
|
* True if this mod pack has been deleted from the list of mod packs.
|
|
* @type {boolean}
|
|
*/
|
|
isDeleted = false;
|
|
|
|
shallowClone() {
|
|
const clone = new this.constructor();
|
|
clone.id = this.id;
|
|
clone.name = this.name;
|
|
clone.description = this.description;
|
|
clone.factorioVersion = this.factorioVersion;
|
|
clone.mods = this.mods;
|
|
clone.settings = this.settings;
|
|
clone.exportManifest = this.exportManifest;
|
|
clone.isDeleted = this.isDeleted;
|
|
return clone;
|
|
}
|
|
|
|
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,
|
|
},
|
|
},
|
|
"export_manifest": ExportManifest.jsonSchema,
|
|
"is_deleted": { type: "boolean" },
|
|
},
|
|
};
|
|
|
|
static validate = libSchema.compile(this.jsonSchema);
|
|
|
|
static fromJSON(json) {
|
|
const modPack = new this();
|
|
|
|
if (json.id) {
|
|
modPack.id = json.id;
|
|
} else {
|
|
modPack.id = Math.random() * 2**31 | 0;
|
|
}
|
|
if (json.name) { modPack.name = json.name; }
|
|
if (json.description) { modPack.description = json.description; }
|
|
if (json.factorio_version) { modPack.factorioVersion = json.factorio_version; }
|
|
if (json.mods) { modPack.mods = new Map(json.mods.map(m => [m.name, m])); }
|
|
if (json.settings) {
|
|
modPack.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.export_manifest) { modPack.exportManifest = ExportManifest.fromJSON(json.export_manifest); }
|
|
if (json.is_deleted) { modPack.isDeleted = json.is_deleted; }
|
|
|
|
if (!modPack.mods.has("base")) {
|
|
modPack.mods.set("base", { name: "base", enabled: true, version: modPack.factorioVersion });
|
|
}
|
|
|
|
return modPack;
|
|
}
|
|
|
|
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.exportManifest) { json.export_manifest = this.exportManifest; }
|
|
if (this.isDeleted) { json.is_deleted = this.isDeleted; }
|
|
return json;
|
|
}
|
|
|
|
toModPackString() {
|
|
const json = this.toJSON();
|
|
delete json.id;
|
|
delete json.export_manifest;
|
|
|
|
// 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 this.fromJSON(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),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 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<string, 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;
|