mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
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.
173 lines
4.2 KiB
JavaScript
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,
|
|
}
|