Files
factorio-clusterio/lib/factorio/patch.js
T
Hornwitser 1a035e849e Replace Hotpatch with save patching
Remove Hotpatch and implement a save patcher in its place.  The save
patcher takes a normal Factorio save and adds modules to it that's
loaded by a generated control.lua file.  This greatly reduces the
difficulty of adding an existing game to Clusterio and using scenario
code in plugins.

The mechanism for selecting what code to patch into the saves from
plugins is temorary and expected to change with #220.
2019-10-24 22:49:16 +02:00

173 lines
4.2 KiB
JavaScript

/**
* Library for patching Factorio saves with scenario code.
* @author Hornwitser
*/
"use strict";
const events = require("events");
const fs = require("fs-extra");
const jszip = require("jszip");
const hash = require("lib/hash");
const knownScenarios = {
// First seen in 0.17.63
"4e866186ebe297f1038fd325b09df1a1f5e2fdd1": {
"name": "freeplay",
"modules": [
"freeplay",
"silo-script",
],
},
};
/**
* Returns the root folder in the zip file
*
* Returns the name of the folder that all files in the zip file is
* contained in. Throws an error if there are multiple such folders.
*
* @returns {string} name of the root folder.
*/
function findRoot(zip) {
let root = null;
zip.forEach((relativePath, file) => {
let index = relativePath.indexOf("/");
if (index === -1) {
throw new Error(`Zip contains file '${relativePath}' in root dir`);
}
let pathRoot = relativePath.slice(0, index);
if (root === null) {
root = pathRoot;
} else if (root !== pathRoot) {
throw new Error(`Zip contains multiple root folders`);
}
});
if (root === null) {
throw new Error(`Empty zip file`);
}
return root;
}
/**
* Generates control.lua code for loading the Clusterio modules
*
* @param {Object} patchInfo - The patch info files's json content
*/
function generateLoader(patchInfo) {
let lines = [
'-- Auto generated scenario module loader created by Clusterio',
'-- Modifications to this file will be lost when loaded in Clusterio',
'',
'local event_handler = require("event_handler")',
'',
'-- Scenario modules',
];
for (let moduleName of patchInfo["scenario"]["modules"]) {
lines.push('event_handler.add_lib(require("'+moduleName+'"))');
}
lines.push(...[
'',
'-- Clusterio modules',
]);
for (let module of patchInfo["modules"]) {
for (let file of module["files"]) {
if (file["load"]) {
let requirePath = "modules/" + file["path"].slice(0, -4);
lines.push('event_handler.add_lib(require("'+requirePath+'"))');
}
}
}
// End last line with a newline
lines.push('');
return lines.join('\n');
}
/**
* Patch a save with the given modules
*
* Adds the modules given by the modules parameter to the save located
* at savePath and rewrites the control.lua in the save to load the
* modules that were added. Will also remove any previous module
* located in the save.
*
* @param {string} savePath - Path to the Factorio save to patch.
* @param {Array<Object>} modules - Description of the modules to patch.
*/
async function patch(savePath, modules) {
let zip = await jszip.loadAsync(await fs.readFile(savePath));
let root = zip.folder(findRoot(zip));
let patchInfoFile = root.file("clusterio.json");
let patchInfo;
if (patchInfoFile !== null) {
let content = await patchInfoFile.async("string");
patchInfo = JSON.parse(content);
// No info file present, try to detect if it's a known compatible scenario.
} else {
let controlStream = root.file("control.lua").nodeStream("nodebuffer");
let controlHash = await hash.hashStream(controlStream);
if (controlHash in knownScenarios) {
patchInfo = {
"scenario": knownScenarios[controlHash],
};
} else {
throw new Error(`Unable to patch save, unknown scenario (${controlHash})`);
}
}
// Remove any existing modules from the save
patchInfo["modules"] = [];
for (let file of root.file(/^modules\//)) {
zip.remove(file.name);
}
// Add the modules to the save.
let modulesDir = root.folder("modules");
for (let module of modules) {
let moduleInfo = {
"name": module.name,
"files": [],
}
for (let file of module.files) {
modulesDir.file(file.path, file.content);
moduleInfo["files"].push({
"path": file.path,
"load": Boolean(file.load),
});
}
patchInfo["modules"].push(moduleInfo);
}
// Add loading code and patch info
root.file("control.lua", generateLoader(patchInfo));
root.file("clusterio.json", JSON.stringify(patchInfo, null, 4));
// Write back the save
let stream = zip.generateNodeStream({compression: "DEFLATE"});
let pipe = stream.pipe(fs.createWriteStream(savePath));
await events.once(pipe, "finish");
}
module.exports = {
patch,
// For testing only
_findRoot: findRoot,
_generateLoader: generateLoader,
}