Unpatch saves started with save patching disabled

Turning factorio.enable_save_patching off did nothing to a save that
was patched before. Its control.lua still loaded every module, so the
clusterio_api interface, player_sync's join handling and the other
module event handlers kept running with no plugin on the host side.

When patching is disabled the instance now restores the original
control.lua that patching kept as scenario.lua and removes the module
files, locale files included, as listed in clusterio.json. The patch
info stays with an empty module list so the patch number keeps
counting. Otherwise patching the save again would reuse the number
stored in the game and on_server_startup would not be raised. Data
the modules put in storage is left alone.

PatchInfo.fromSave dropped the version read from the save, so the
check against newer patch versions never fired. It now keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-11-04 00:16:52 +00:00
1 parent 6eaf48aefe
commit ca3dbab49d
6 files changed
+229 -49

No files matched your search

+86
View File
@@ -271,4 +271,90 @@ describe("host/patch", function() {
);
});
});
describe("unpatch()", function() {
const freeplayControl = "require('__base__/script/freeplay/control.lua')\n";
const zipPath = path.join("temp", "test", "unpatch.zip");
let testModule;
before(async function() {
testModule = new patch.SaveModule(new lib.ModuleInfo("test", "1.0.0"));
await testModule.loadFiles("test/file/modules/test");
});
async function readSave() {
const zip = await lib.ZipArchive.fromFile(zipPath);
try {
const files = new Map();
for (const name of zip.entries.keys()) {
files.set(name, (await zip.readFile(name)).toString("utf8"));
}
return files;
} finally {
zip.close();
}
}
it("should leave saves that are not patched untouched", async function() {
await fs.writeFile(zipPath, await createZipBuffer([["world/control.lua", freeplayControl]]));
assert.equal(await patch.unpatch(zipPath), false);
assert.deepEqual([...(await readSave()).keys()], ["world/control.lua"]);
});
it("should restore control.lua and remove module files", async function() {
await fs.writeFile(zipPath, await createZipBuffer([
["world/control.lua", freeplayControl],
["world/level.dat0", "level"],
]));
await patch.patch(zipPath, [testModule]);
assert.equal(await patch.unpatch(zipPath), true);
const files = await readSave();
assert.deepEqual(
[...files.keys()].sort(),
["world/clusterio.json", "world/control.lua", "world/level.dat0", "world/scenario.lua"],
);
assert.equal(files.get("world/control.lua"), freeplayControl);
const patchInfo = JSON.parse(files.get("world/clusterio.json"));
assert.equal(patchInfo.patch_number, 1);
assert.deepEqual(patchInfo.modules, []);
assert.equal(await patch.unpatch(zipPath), false, "unpatched twice");
});
it("should throw on a newer patch version", async function() {
const scenario = new lib.ModuleInfo("freeplay", "2.0.29", [], ["scenario"]);
const patchInfo = { version: 2, patch_number: 1, scenario, modules: [] };
await fs.writeFile(zipPath, await createZipBuffer([
["world/control.lua", freeplayControl],
["world/clusterio.json", JSON.stringify(patchInfo)],
]));
await assert.rejects(patch.unpatch(zipPath), /Save patch version 2 is newer/);
await assert.rejects(patch.patch(zipPath, []), /Save patch version 2 is newer/);
});
it("should keep counting the patch number when patched again", async function() {
await fs.writeFile(zipPath, await createZipBuffer([["world/control.lua", freeplayControl]]));
await patch.patch(zipPath, [testModule]);
await patch.unpatch(zipPath);
await patch.patch(zipPath, [testModule]);
const files = await readSave();
assert.equal(JSON.parse(files.get("world/clusterio.json")).patch_number, 2);
assert(files.has("world/modules/test/test.lua"));
});
it("should keep the loader of scenarios made for Clusterio", async function() {
const scenario = new lib.ModuleInfo("custom", "1.0.0", [], ["custom"]);
const patchInfo = new patch.PatchInfo(3, scenario, [testModule]);
await fs.writeFile(zipPath, await createZipBuffer([
["world/control.lua", patch._generateLoader(patchInfo)],
["world/clusterio.json", JSON.stringify(patchInfo)],
["world/custom.lua", "-- custom\n"],
...[...testModule.files].map(([name, content]) => [`world/${name}`, content]),
]));
assert.equal(await patch.unpatch(zipPath), true);
const files = await readSave();
assert.deepEqual(
[...files.keys()].sort(),
["world/clusterio.json", "world/control.lua", "world/custom.lua"],
);
assert.equal(files.get("world/control.lua"), patch._generateLoader(new patch.PatchInfo(3, scenario, [])));
});
});
});