const Promise = require("bluebird");
const path = require("path");
const glob = require("glob-promise");
const Mod = require("./Mod");
/**
* Options to configure finding modules.
* @typedef {Object} ModuleLoaderOptions
* @property {String} factorioPath **Required** The directory where Factorio is installed.
* @property {String} factorioDataPath the directory containing Factorio's core mods.
* Defaults to `factorioPath + "/data"`.
* @property {String} factorioModPath the directory containing user-added mods. Defaults to `factorioPath + "/mods"`
* @property {String} factorioModList the JSON file listing which mods are enabled. Defaults to
* `factorioModPath + "/mod-list.json"`
* @property {Boolean} vanilla if `true`, the vanilla game components will be added to the output. Defaults to `true`.
* @property {Boolean} added if `true`, user-added mods will be added to the output. Defaults to `true`.
*/
/**
* Locates Factorio modules, determines dependencies, and filters by enabled mods.
*/
class ModuleLoader {
/**
* The `Mod` class to use when creating new `Mod` instances. Can be overridden for testing, etc.
*/
get Mod() {
return this._Mod || Mod;
}
set Mod(_Mod) { this._Mod = _Mod; }
/**
* The default loading options.
* @type {ModuleLoaderOptions}
* @private
*/
static get defaultLoadOptions() {
return {
vanilla: true,
added: true,
};
}
/**
* @param {ModuleLoaderOptions} loadOpts default options to use when searching for mods.
*/
constructor(loadOpts) {
/**
* The default options to use when searching for mods.
* @type {ModuleLoaderOptions}
* @private
*/
this._loadOpts = loadOpts;
}
/**
* Merge the temporary options with the default options for this class instance and the global default options,
* including processing dependent options (like `factorioDataPath` defaulting to `factorioPath + "/data"`)
* @param {ModuleLoaderOptions} opts the current instance options
* @return {ModuleLoaderOptions} the merged options
* @private
*/
parseOpts(opts) {
/** @type {ModuleLoaderOptions} */
opts = Object.assign({}, this.constructor.defaultLoadOptions, this._loadOpts, opts);
/*if(!opts.factorioPath) {
throw new ReferenceError("'factorioPath' is a required option for the module loader, yet was not provided.");
}*/
if(!opts.factorioDataPath && opts.factorioPath) {
opts.factorioDataPath = opts.factorioPath + "/data";
}
if(!opts.factorioModPath && opts.factorioPath) {
opts.factorioModPath = opts.factorioPath + "/mods";
}
if(!opts.factorioModList && opts.factorioModPath) {
opts.factorioModList = opts.factorioModPath + "/mod-list.json";
}
return opts;
}
/**
* Find all Factorio Mods installed.
* @param {ModuleLoaderOptions} opts options to control searching for mods.
* @return {Promise<ModArray>} all mods installed.
* @todo Test returning only `vanilla` or only `added`
*/
all(opts) {
opts = this.parseOpts(opts);
return Promise
.all([
opts.vanilla ? this.vanilla(opts.factorioDataPath) : [],
opts.added ? this.added(opts.factorioModPath) : [],
])
.then((...args) => {
let groups = [].concat(...args);
return [].concat(...groups);
});
}
/**
* Find all Factorio Mods that are enabled.
* @param {ModArray} [mods] **Optional** The mods to filter. Defaults to searching for all mods.
* @param {ModuleLoaderOptions} [opts] options to control searching for mods.
* @return {Promise<ModArray>} all mods installed and enabled.
*/
enabled(mods, opts) {
opts = this.parseOpts(opts);
if(!mods) { mods = this.all(opts); }
let modList = require(opts.factorioModList);
let enabled = modList.mods.filter(mod => mod.enabled).map(mod => mod.name);
return Promise
.resolve(mods)
.then((mods) => mods.filter(mod => enabled.indexOf(mod.name) > -1));
}
/**
* Sort Factorio mods, so mods with dependencies are loaded after the dependencies.
* @param {ModArray} [mods] **Optional** The mods to sort. Defaults to searching for all mods.
* @return {Promise<ModArray>} resolves to a sorted list of the given mods.
* @see {@link https://bitbucket.org/Nicksaurus/foreman/src/46053df/Foreman/DependencyGraph.cs#DependencyGraph.cs-43}
*/
sortedDependencies(mods) {
if(!mods) { mods = this.all(); }
return Promise
.resolve(mods)
.then((mods) => {
let sorted = [];
let adjacencyMatrix = mods.map(i => mods.map(j => i.dependsOn(j) ? 1 : 0));
// Get all mods with no incoming dependencies
let s = mods.filter((mod, i) => mods.every((mod2, j) => adjacencyMatrix[j][i] !== 1 ));
/**
* Loop through mods that have no dependencies, or have dependencies already resolved.
* Add each to a sorted array, and see if any dependencies in the matrix now have resolved dependencies.
*/
while(s.length > 0) {
let mod = s.shift();
sorted.push(mod);
let modIndex = mods.indexOf(mod);
/**
* Loop through all mods. If they were dependent on us, we've already been added to the sorted list and they
* should no longer be waiting for us.
* So, remove that dependency if we find one. Now that they are waiting for one less item, see if all of the
* dependencies have been added. If so, they can be added to the list of resolved mods (`s`) and added to the
* output in a later round.
*/
for(let m = 0; m < mods.length; m++) {
if(adjacencyMatrix[modIndex][m] === 0) { continue; }
adjacencyMatrix[modIndex][m] = 0;
let resolved = mods.every((_, i) => adjacencyMatrix[i][m] === 0);
if(resolved) { s.push(mods[m]); }
}
}
return sorted.reverse();
});
}
/**
* Finds the vanilla game components.
* @param {String} dataPath the path to the `data` directory inside a Factorio install, where internal mods are
* located.
* @return {Promise<ModArray>} The "mods" shipped with the vanilla game.
* @private
*/
vanilla(dataPath) {
return glob
.promise("./*/info.json", { cwd: dataPath })
.then(manifests =>
Promise.all( manifests.map(manifest => this.Mod.loadFromManifest(path.dirname(path.join(dataPath, manifest)))) )
);
}
/**
* Finds user-added game components.
* @param {String} modPath the path to the `mods` directory inside a Factorio install, where user-added mods are
* located.
* @return {Promise<ModArray>} The mods installed by the user.
* @private
*/
added(modPath) {
return glob
.promise("./*.zip", { cwd: modPath })
.then(zips =>
Promise.all( zips.map(zip => this.Mod.loadFromZip(path.join(modPath, zip))) )
);
}
}
module.exports = ModuleLoader;