const Promise = require("bluebird");
const fs = require("fs-extra");
const path = require("path");
const tmp = require("tmp-promise");
const unzip = require("unzipper");
/**
* An array of Factorio Mods, useful for documenting return types inside Promises.
* Use `{Promise<ModArray>}` instead of `{Promise<Array<Mod>>}`.
* @typedef {Array<Mod>} ModArray
*/
/**
* The possible types of a module dependency.
* @enum {String}
* @readonly
*/
const DependencyType = {
EqualTo: "=",
GreaterThan: ">",
GreaterThanOrEqual: ">=",
};
/**
* The details of a mod dependency.
* @typedef {Object} ModDependency
* @property {Boolean} optional `true` if the mod dependency is optional.
* @property {String} name the name of the required mod.
* @property {DependencyType} type the type of dependency.
* @property {String} version the version dependency of the mod. Defaults to `"0.0.0.0"`.
*/
/**
* A Factorio mod.
*/
class Mod {
/**
* Loads an unpacked {@link Mod} by path.
* @param {String} dir the directory containing a manifest (`info.json`) file.
* directory.
* @return {Promise<Mod>} the loaded {@link Mod}.
*/
static loadFromManifest(dir) {
let mod = new Mod(require(path.join(dir, "info.json")), dir);
return Promise.resolve(mod);
}
/**
* Loads a Mod given the path to the mod's zipped data.
* @param {String} zip the patht to a zipped mod directory.
* @return {Promise<Mod>} the loaded {@link Mod}.
*/
static loadFromZip(zip) {
let tmpDir = null;
return tmp
.dir({ unsafeCleanup: true })
.then(o => tmpDir = o)
.then(tmpDir => {
return fs
.createReadStream(zip)
.pipe(unzip.Extract({ path: tmpDir.path }))
.promise();
})
.then(() => fs.readdir(tmpDir.path))
.then(files => files[0])
.then(dir => {
let mod = new Mod(require(path.join(tmpDir.path, dir, "info.json")), path.join(tmpDir.path, dir));
mod.addCleanup(tmpDir.cleanup);
return mod;
});
}
/**
* Checks that the version is larger than the test version.
* @param {String} version the version that should be larger.
* @param {String} test the version that should be smaller.
* @return {Boolean} `true` if `version` is larger than `test`.
*/
static versionGreater(version, test) {
version = version.split(".");
test = test.split(".");
for(let i = 0; i < test.length; i++) {
let v = parseInt(version[i], 10);
let t = parseInt(test[i], 10);
if(v > t) { return true; }
if(v < t) { return false; }
}
return false; // versions are equal.
}
/**
* @param {Object} manifest the manifest file for this mod.
* @param {String} directory the location for mod files.
*/
constructor(manifest, directory) {
this.manifest = manifest;
this.dir = directory;
/**
* Other mods that this mod is dependent on.
* @type {Array<ModDependency>}
*/
this._parsedDependencies = this.parseDependencies();
/**
* A list of cleanup tasks that will remove temporary files. Tasks can return a `Promise` for longer-running tasks.
* @type {Array<Function>}
*/
this._cleanupTasks = [];
}
get name() { return this.manifest.name; }
get version() { return this.manifest.version || "0.0.0.0"; }
get dependencies() {
let extra = [];
if(this.name === "base") { extra.push("core"); }
return this.manifest.dependencies ? this.manifest.dependencies.concat(extra) : extra;
}
/**
* Add a task to execute when cleaning up this {@link Mod}.
* @param {Function} task the additional task to run during cleanup. Can return a `Promise` for longer tasks.
* @return {void}
*/
addCleanup(task) {
this._cleanupTasks.push(task);
}
/**
* Run all cleanup tasks to remove temporary files used when creating the mod.
* @return {Promise} resolves when all temporary files have been cleaned up.
*/
cleanup() {
return Promise.all(this._cleanupTasks.map(task => task()));
}
/**
* Determines if this mod satisfies the given dependency selection.
* @param {ModDependency} dep the dependency to check against.
* @return {Boolean} `true` if the mod satisfies the dependency.
*/
satisfiesDependency(dep) {
return this.name === dep.name && (
typeof dep.version !== "string" ||
(
(dep.type === DependencyType.EqualTo || dep.type === DependencyType.GreaterThanOrEqual) &&
dep.version === this.version
) ||
(
(dep.type === DependencyType.GreaterThan || dep.type === DependencyType.GreaterThanOrEqual) &&
this.constructor.versionGreater(this.version, dep.version)
)
);
}
/**
* Determines if this mod depends on the given mod.
* @param {Mod} mod the mod to compare
* @param {Boolean} ignoreOptional if `true`, optional dependencies are not compared. Defaults to `false`.
* @return {Boolean} `true` if this mod depends on the given mod.
*/
dependsOn(mod, ignoreOptional) {
let deps = ignoreOptional ? this._parsedDependencies.filter(dep => !dep.optional) : this._parsedDependencies;
return deps.some(dep => mod.satisfiesDependency(dep));
}
/**
* Parse the dependencies listed for this mod.
* @return {Array<ModDependency>} the dependencies for this mod.
* @private
*/
parseDependencies() {
return this.dependencies.map(d => {
let dep = d.split(" ");
let optional = false;
let name, type, version;
if(dep[0] === "?") {
optional = true;
dep.shift();
}
[ name, type, version ] = dep;
if(!version) { version = "0.0.0.0"; }
return {optional, name, type, version};
});
}
}
module.exports = Mod;