lib/api/Mod.js

const archiver = require('archiver');
const fsOld = require('fs');
const fs = require('fs').promises;
const YAML = require('json-to-pretty-yaml');
const mkdirp = require('mkdirp');
const path = require('path');
const rmfr = require('rmfr');
const Dialog = require('./Dialog');
const { safeName } = require('../utils');

class Mod {
	/**
	 * @constructor
	 * @param {Object} [data={}] - An object containing the attributes of the Mod
	 * @param {String} [data.name='Lucid Dream Default'] - The name of the Mod
	 * @param {String} [data.version='0.0.1'] - The version of the Mod (X.Y.Z)
	 * @param {String} [data.author='Lucid Dream Author'] - The author of the Mod
	 * @param {Array} [data.dependencies=[ { name: 'Everest', version: '1.0.0' } ]] - An Array containing the dependencies of the Mod
	 * @param {Array} [data.chapters=[]] - An Array of {@link Chapter} instances
	 * @param {Dialog} [data.dialog=new Dialog()] - An instance of {@link Dialog}
	 * @example
	 * // create a Mod
	 * const { Mod } = require('lucid-dream');
	 * const mod = new Mod({ name: 'My Mod', author: 'BSAC' });
	 */
	constructor(data = {}) {
		Object.assign(this, {
			name: 'Lucid Dream Default',
			version: '0.0.1',
			author: 'Lucid Dream Author',
			dependencies: [ { name: 'Everest', version: '1.0.0' } ],
			chapters: [],
			dialog: new Dialog()
		}, data);
	}

	/**
	 * Package the Mod to a format loadable by Celeste
	 * @param {String} outputDir - The directory to which to place the packaged Mod
	 * @example
	 * // package a Mod to the `output` directory
	 * const { Mod } = require('lucid-dream');
	 * const mod = new Mod({ name: 'Packaged Mod', author: 'iamdadbod' });
	 * // make any additional changes to the Mod
	 * await mod.package('output');
	 * @returns {null}
	 */
	async package(outputDir, opts = {}) {
		const { zip } = opts;
		const { name, version, dependencies } = this;
		const modDir = path.join(outputDir, safeName(this.name));
		const dialogDir = path.join(modDir, 'Dialog');
		const mapDir = path.join(modDir, 'Maps', safeName(this.name), safeName(this.author));

		// prep mod dir
		await rmfr(modDir);
		await mkdirp(modDir);
		await mkdirp(mapDir);
		await mkdirp(dialogDir);

		// write the everest.yaml file
		const everestYaml = YAML.stringify(capitalizeKeys({ name, version, dependencies }));
		await fs.writeFile(path.join(modDir, 'everest.yaml'), everestYaml);

		// write the Dialog/English.txt
		const englishTxt = this.dialog.encode(this);
		if (englishTxt !== '') {
			await fs.writeFile(path.join(dialogDir, 'English.txt'), englishTxt);
		}

		// encode all the chapters and sides
		for (let chapter of this.chapters) {
			await chapter.package(mapDir);
		}

		// zip the mod
		if (zip) {
			return new Promise((resolve, reject) => {
				const output = fsOld.createWriteStream(modDir + '.zip');
				const archive = archiver('zip', { zlib: { level: 9 } });
				output.on('close', resolve);
				output.on('error', reject);
				archive.pipe(output);
				archive.directory(modDir, false);
				archive.finalize();
			});
		}
	}
}

function capitalizeKeys(o) {
	if (!o || typeof o !== 'object') { return; }
	if (Array.isArray(o)) {
		o.forEach(el => capitalizeKeys(el));
	} else {
		Object.keys(o).forEach(key => {
			const newKey = key.charAt(0).toUpperCase() + key.slice(1);
			o[newKey] = o[key];
			delete o[key];
			capitalizeKeys(o[newKey]);
		});
	}
	return o;
}

module.exports = Mod;