Merge pull request #1014 from bbassie/feature/patched-scenario

Add create-scenario command to clusteriohost
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-10-06 16:37:16 +01:00
commit b07c25aa7e
11 files changed
+496 -36

No files matched your search

+2
View File
@@ -5,6 +5,8 @@ The save patching is based on the built in [event_handler](https://github.com/wu
Mods on the other hand work without modifications.
This can be disabled with the `factorio.enable_save_patching` config on a per instance basis, however disabling it means that modules are not loaded into the game and most plugins will not function.
To use the modules outside of Clusterio the `npx clusteriohost create-scenario` command creates a scenario with the modules patched into it.
Note that the IPC between the game and the host is not available outside of Clusterio, so modules that depend on it will not work as expected in such a scenario.
<sub>1: Before version 2.0 of Clusterio this was done at runtime with a patcher called Hotpatch, but due to the difficulty in supporting run time patching and the lack of documentation it was replace it with save patching instead.</sub>
+25
View File
@@ -99,6 +99,31 @@ See docs/configuration.md in the main repositiory for the available configuratio
Shows the value for a single config entry.
### `create-scenario <output>`
Create a scenario with the Clusterio modules patched into it and write it to `output`.
This is the same patching that is applied to saves when an instance starts, see docs/how-it-works.md in the main repository.
The result can be used as a regular scenario outside of Clusterio by copying it to the `scenarios` folder of Factorio or into a mod.
The IPC between the game and the host is not available outside of Clusterio, so modules that depend on it will not work as expected.
By default the freeplay scenario from the latest Factorio install in `host.factorio_directory` is patched with the modules from all plugins that are enabled in the host config.
* `--scenario <path>` Scenario directory, zipped scenario or save to patch instead of freeplay.
This has the same restrictions as save patching, the scenario must either use the event_handler library and have been seen by Clusterio before, or already have been patched by Clusterio.
* `--factorio-version <version>` Factorio version to take the freeplay scenario from.
Defaults to `latest`.
* `--plugins <name>...` Only patch in modules from the given plugins.
* `--zip` Write the scenario as a zip file instead of a directory.
Factorio does not load zipped scenarios, extract it before use.
For example, creating a scenario with only the inventory_sync module:
npx clusteriohost create-scenario ~/.factorio/scenarios/inventory_sync --plugins inventory_sync
#### `config list`
Lists up all configuration entries with their currently configured values.
+11
View File
@@ -24,6 +24,7 @@ import * as lib from "@clusterio/lib";
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
import Host from "./src/Host.js";
import { createScenarioCommand, handleCreateScenarioCommand } from "./src/create_scenario.js";
let host: Host | undefined;
@@ -119,6 +120,11 @@ async function startHost() {
.option("bypass-lock-file", { hidden: true, type: "boolean", nargs: 0, default: false })
.command("plugin", "Manage available plugins", lib.pluginCommand)
.command("config", "Manage Host config", lib.configCommand)
.command(
"create-scenario <output>",
"Create a scenario patched with modules for use outside Clusterio",
createScenarioCommand,
)
.command("run", "Run host", yargs => {
yargs.option("can-restart", {
type: "boolean", nargs: 0, default: false,
@@ -185,6 +191,11 @@ async function startHost() {
return;
}
if (command === "create-scenario") {
await handleCreateScenarioCommand(args, hostConfig, pluginInfos);
return;
}
// From this point on the host should run
await hostConfigLock.acquire();
+2 -24
View File
@@ -10,7 +10,7 @@ const execAsync = util.promisify(exec);
import * as lib from "@clusterio/lib";
import { FactorioServer } from "./server.js";
import { SaveModule, patch } from "./patch.js";
import { loadModules, patch } from "./patch.js";
import { exportData } from "./export.js";
import type Host from "./Host.js";
import { BaseInstancePlugin, InstanceHooks } from "./BaseInstancePlugin.js";
@@ -1007,29 +1007,7 @@ end`.replace(/\r?\n/g, " ");
// Patch save with lua modules from plugins
this.logger.verbose("Patching save");
// Find plugin modules to patch in
let modules: Map<string, SaveModule> = new Map();
for (const pluginInfo of this.loadedPlugins) {
let module = await SaveModule.fromPluginInfo(pluginInfo);
if (!module) {
continue;
}
modules.set(module.info.name, module);
}
// Find stand alone modules to load
// XXX for now only the included clusterio module is loaded
let modulesDirectory = path.join(import.meta.dirname, "..", "..", "..", "modules");
for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (modules.has(entry.name)) {
throw new Error(`Module with name ${entry.name} already exists in a plugin`);
}
let module = await SaveModule.fromDirectory(path.join(modulesDirectory, entry.name));
modules.set(module.info.name, module);
}
}
let modules = await loadModules(this.loadedPlugins);
await patch(this.path("saves", saveName), [...modules.values()]);
return saveName;
}
+171
View File
@@ -0,0 +1,171 @@
// Implementation of the create-scenario command
import fs from "node:fs/promises";
import os from "node:os";
import JSZip from "jszip";
import path from "path";
import * as lib from "@clusterio/lib";
import { logger } from "@clusterio/lib";
import { loadModules, patch, SaveModule } from "./patch.js";
import { findVersion } from "./server.js";
/**
* Create a scenario patched with the given modules
*
* Runs the save patching on the scenario at scenarioPath and writes the
* result to outputPath. The scenario can be a directory, a zipped
* scenario or a save.
*
* @param scenarioPath - Path to the scenario to patch.
* @param outputPath - Path to write the patched scenario to.
* @param modules - Description of the modules to patch.
* @param zip - Write a zip file instead of a directory.
*/
export async function createScenario(
scenarioPath: string,
outputPath: string,
modules: SaveModule[],
zip = false,
) {
try {
await fs.stat(outputPath);
throw new Error(`${outputPath} already exists`);
} catch (err: any) {
if (err.code !== "ENOENT") {
throw err;
}
}
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clusterio-scenario-"));
try {
const tempPath = path.join(tempDir, "scenario.zip");
if ((await fs.stat(scenarioPath)).isDirectory()) {
await zipDirectory(scenarioPath, tempPath);
} else {
await fs.copyFile(scenarioPath, tempPath);
}
await patch(tempPath, modules);
if (zip) {
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.copyFile(tempPath, outputPath);
} else {
await extractZip(tempPath, outputPath);
}
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
async function zipDirectory(dirPath: string, zipPath: string) {
const root = path.basename(path.resolve(dirPath));
const zip = new JSZip();
let dirs: [string, string][] = [[dirPath, root]];
while (dirs.length) {
let [dir, relativeDir] = dirs.pop()!;
for (let entry of await fs.readdir(dir, { withFileTypes: true })) {
let fsPath = path.join(dir, entry.name);
let relativePath = path.posix.join(relativeDir, entry.name);
if (entry.isFile()) {
zip.file(relativePath, await fs.readFile(fsPath));
} else if (entry.isDirectory()) {
dirs.push([fsPath, relativePath]);
}
}
}
await fs.writeFile(zipPath, await zip.generateAsync({ type: "nodebuffer" }));
}
async function extractZip(zipPath: string, dirPath: string) {
const zip = await JSZip.loadAsync(await fs.readFile(zipPath));
const root = lib.findRoot(zip);
for (let file of Object.values(zip.files)) {
if (file.dir || !file.name.startsWith(`${root}/`)) {
continue;
}
const fsPath = path.join(dirPath, file.name.slice(root.length + 1));
await fs.mkdir(path.dirname(fsPath), { recursive: true });
await fs.writeFile(fsPath, await file.async("nodebuffer"));
}
}
/**
* Yargs create-scenario command
*
* @param yargs - yargs command builder.
*/
export function createScenarioCommand(yargs: any) {
yargs
.positional("output", {
describe: "Path to write the scenario to",
type: "string",
})
.option("zip", {
type: "boolean", nargs: 0, default: false,
describe: "Write the scenario as a zip file instead of a directory",
})
.option("scenario", {
nargs: 1,
describe:
"Path to a scenario directory, zipped scenario or save to patch. " +
"Defaults to the freeplay scenario of the Factorio install in host.factorio_directory",
type: "string",
})
.option("factorio-version", {
nargs: 1,
describe: "Factorio version to take the freeplay scenario from",
default: "latest",
type: "string",
})
.option("plugins", {
describe: "Plugins to patch modules from, defaults to all plugins enabled in the host config",
type: "array",
})
;
}
/**
* Handle yargs create-scenario command
*
* @param args - yargs args object.
* @param hostConfig - Host config.
* @param pluginInfos - Plugins available to the host.
*/
export async function handleCreateScenarioCommand(
args: Record<string, unknown>,
hostConfig: lib.HostConfig,
pluginInfos: lib.PluginNodeEnvInfo[],
) {
let selected: lib.PluginNodeEnvInfo[];
if (args.plugins) {
selected = (args.plugins as string[]).map(name => {
const pluginInfo = pluginInfos.find(info => info.name === name);
if (!pluginInfo) {
throw new Error(`Plugin ${name} is not in the plugin list`);
}
return pluginInfo;
});
} else {
selected = pluginInfos.filter(
info => info.instanceEntrypoint && hostConfig.get(`${info.name}.load_plugin`)
);
}
let scenarioPath = args.scenario as string | undefined;
if (scenarioPath === undefined) {
const factorioDir = hostConfig.get("host.factorio_directory");
const version = args.factorioVersion as lib.TargetVersion;
if (!lib.isTargetVersion(version)) {
throw new Error(`Invalid Factorio version ${version}`);
}
const [dataDir] = await findVersion(factorioDir, version);
scenarioPath = path.join(dataDir, "base", "scenarios", "freeplay");
}
const modules = await loadModules(selected);
logger.info(`Patching ${scenarioPath} with modules ${[...modules.keys()].join(", ")}`);
await createScenario(scenarioPath, args.output as string, [...modules.values()], Boolean(args.zip));
logger.info(`Created scenario ${args.output}`);
}
+34
View File
@@ -145,6 +145,40 @@ export class SaveModule {
}
}
/**
* Load the modules to patch into a save
*
* Collects the modules provided by the given plugins together with the
* modules bundled with the host package.
*
* @param pluginInfos - Plugins to load modules from.
* @returns Map of module name to module.
*/
export async function loadModules(pluginInfos: Iterable<lib.PluginNodeEnvInfo>) {
let modules: Map<string, SaveModule> = new Map();
for (let pluginInfo of pluginInfos) {
let module = await SaveModule.fromPluginInfo(pluginInfo);
if (!module) {
continue;
}
modules.set(module.info.name, module);
}
// Find stand alone modules to load
// XXX for now only the included clusterio module is loaded
let modulesDirectory = path.join(import.meta.dirname, "..", "..", "..", "modules");
for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (modules.has(entry.name)) {
throw new Error(`Module with name ${entry.name} already exists in a plugin`);
}
let module = await SaveModule.fromDirectory(path.join(modulesDirectory, entry.name));
modules.set(module.info.name, module);
}
}
return modules;
}
export class PatchInfo {
static currentVersion = 1;
+4 -3
View File
@@ -181,9 +181,11 @@ async function downloadAndExtractTar(url: string, targetDir: string) {
* Version to look for, supports the special value "latest" for the
* latest version available.
* @returns Array with path to data dir and version found.
* @internal
*/
async function findVersion(factorioDir: string, targetVersion: lib.TargetVersion): Promise<[string, lib.FullVersion]> {
export async function findVersion(
factorioDir: string,
targetVersion: lib.TargetVersion,
): Promise<[string, lib.FullVersion]> {
// Check if this is a direct installation dir
const directVersion = await getFactorioVersion(factorioDir);
if (directVersion !== null) {
@@ -1609,7 +1611,6 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
// For testing only
export const _getFactorioVersion = getFactorioVersion;
export const _versionOrder = versionOrder;
export const _findVersion = findVersion;
export const _listFactorioVersions = listFactorioVersions;
export const _downloadAndExtractZip = downloadAndExtractZip;
export const _downloadAndExtractTar = downloadAndExtractTar;
+190
View File
@@ -0,0 +1,190 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import JSZip from "jszip";
import path from "node:path";
import * as lib from "@clusterio/lib";
import {
createScenario, createScenarioCommand, handleCreateScenarioCommand,
} from "@clusterio/host/dist/node/src/create_scenario.js";
import { SaveModule, PatchInfo } from "@clusterio/host/dist/node/src/patch.js";
describe("host/create_scenario", function() {
const tempDir = path.join("temp", "test", "create_scenario");
const patchInfo = new PatchInfo(0, new lib.ModuleInfo("test", "1.0.0", [], ["scenario"]), []);
const scenarioFiles = new Map([
["scenario.lua", "-- scenario\n"],
["description.json", "{}\n"],
["locale/en/test.cfg", "test=Test\n"],
["clusterio.json", JSON.stringify(patchInfo)],
]);
const module = new SaveModule(
new lib.ModuleInfo("spam", "1.0.0", ["spam.lua"]),
new Map([["modules/spam/spam.lua", Buffer.from("return {}\n")]]),
);
async function readDir(dirPath) {
const files = new Map();
for (const entry of await fs.readdir(dirPath, { recursive: true, withFileTypes: true })) {
if (entry.isFile()) {
const fsPath = path.join(entry.parentPath, entry.name);
files.set(path.relative(dirPath, fsPath).split(path.sep).join("/"), await fs.readFile(fsPath, "utf8"));
}
}
return files;
}
function checkOutput(files) {
assert(files.get("control.lua").includes('event_handler.add_lib(require("modules/spam/spam"))'));
assert.equal(JSON.parse(files.get("clusterio.json")).patch_number, 1);
assert.equal(files.get("modules/spam/spam.lua"), "return {}\n");
for (const name of ["scenario.lua", "description.json", "locale/en/test.cfg"]) {
assert.equal(files.get(name), scenarioFiles.get(name));
}
assert.equal(files.size, scenarioFiles.size + 2);
}
before(async function() {
await fs.rm(tempDir, { recursive: true, force: true });
await fs.mkdir(path.join(tempDir, "scenario"), { recursive: true });
const zip = new JSZip();
for (const [name, content] of scenarioFiles) {
const fsPath = path.join(tempDir, "scenario", name);
await fs.mkdir(path.dirname(fsPath), { recursive: true });
await fs.writeFile(fsPath, content);
zip.file(`scenario/${name}`, content);
}
await fs.writeFile(path.join(tempDir, "scenario.zip"), await zip.generateAsync({ type: "nodebuffer" }));
});
it("should patch a scenario directory", async function() {
const output = path.join(tempDir, "from_dir");
await createScenario(path.join(tempDir, "scenario"), output, [module]);
checkOutput(await readDir(output));
});
it("should patch a zipped scenario", async function() {
const output = path.join(tempDir, "from_zip");
await createScenario(path.join(tempDir, "scenario.zip"), output, [module]);
checkOutput(await readDir(output));
});
it("should write a zip file when asked to", async function() {
const output = path.join(tempDir, "zipped.zip");
await createScenario(path.join(tempDir, "scenario"), output, [module], true);
const zip = await JSZip.loadAsync(await fs.readFile(output));
const files = new Map();
for (const file of Object.values(zip.files)) {
if (!file.dir) {
files.set(file.name.replace(/^scenario\//, ""), await file.async("string"));
}
}
checkOutput(files);
});
it("should not modify the input scenario", async function() {
const files = await readDir(path.join(tempDir, "scenario"));
assert.deepEqual(files, scenarioFiles);
});
it("should throw if the output already exists", async function() {
const output = path.join(tempDir, "from_dir");
await assert.rejects(
createScenario(path.join(tempDir, "scenario"), output, [module]),
new Error(`${output} already exists`),
);
});
it("should throw on unknown scenario", async function() {
await fs.mkdir(path.join(tempDir, "unknown"));
await fs.writeFile(path.join(tempDir, "unknown", "control.lua"), "-- unknown\n");
await assert.rejects(
createScenario(path.join(tempDir, "unknown"), path.join(tempDir, "from_unknown"), [module]),
new Error("Unable to patch save, unknown scenario (3acc3be3861144e55604f5ac2f2555071885ebc4)")
);
});
describe("createScenarioCommand()", function() {
it("should define the output positional and options", function() {
const positionals = [];
const options = [];
const yargs = {
positional(name) { positionals.push(name); return yargs; },
option(name) { options.push(name); return yargs; },
};
createScenarioCommand(yargs);
assert.deepEqual(positionals, ["output"]);
assert.deepEqual(options, ["zip", "scenario", "factorio-version", "plugins"]);
});
});
describe("handleCreateScenarioCommand()", function() {
const factorioDir = path.join(tempDir, "factorio");
const scenario = path.join(tempDir, "scenario");
let hostConfig;
let pluginInfos;
before(async function() {
// Fake install with the test scenario as freeplay
await fs.cp(path.join("test", "file", "factorio", "0.1.2"), factorioDir, { recursive: true });
const freeplay = path.join(factorioDir, "data", "base", "scenarios", "freeplay");
await fs.cp(scenario, freeplay, { recursive: true });
pluginInfos = await lib.loadPluginInfos(new Map([
["research_sync", path.resolve("plugins/research_sync")],
["test_plugin", path.resolve("test/file/test_plugin")],
]));
lib.addPluginConfigFields(pluginInfos);
hostConfig = new lib.HostConfig("host", { "host.factorio_directory": factorioDir });
});
it("should patch freeplay with the enabled plugins by default", async function() {
const output = path.join(tempDir, "default");
await handleCreateScenarioCommand({ output, factorioVersion: "latest" }, hostConfig, pluginInfos);
const files = await readDir(output);
assert.equal(files.get("scenario.lua"), scenarioFiles.get("scenario.lua"));
const info = JSON.parse(files.get("clusterio.json"));
assert.deepEqual(info.modules.map(m => m.name), ["clusterio", "research_sync"]);
});
it("should skip plugins disabled in the host config", async function() {
const output = path.join(tempDir, "disabled");
hostConfig.set("research_sync.load_plugin", false);
try {
await handleCreateScenarioCommand({ output, factorioVersion: "latest" }, hostConfig, pluginInfos);
} finally {
hostConfig.set("research_sync.load_plugin", true);
}
const info = JSON.parse((await readDir(output)).get("clusterio.json"));
assert.deepEqual(info.modules.map(m => m.name), ["clusterio"]);
});
it("should use the given scenario and plugins", async function() {
const output = path.join(tempDir, "selected");
await handleCreateScenarioCommand(
{ output, scenario, plugins: ["research_sync"] }, hostConfig, pluginInfos
);
const info = JSON.parse((await readDir(output)).get("clusterio.json"));
assert.deepEqual(info.modules.map(m => m.name), ["clusterio", "research_sync"]);
});
it("should throw on unknown plugin", async function() {
await assert.rejects(
handleCreateScenarioCommand(
{ output: path.join(tempDir, "unknown_plugin"), scenario, plugins: ["nope"] },
hostConfig,
pluginInfos,
),
new Error("Plugin nope is not in the plugin list")
);
});
it("should throw on invalid Factorio version", async function() {
await assert.rejects(
handleCreateScenarioCommand(
{ output: path.join(tempDir, "bad_version"), factorioVersion: "1..2" }, hostConfig, pluginInfos
),
new Error("Invalid Factorio version 1..2")
);
});
});
});
+35
View File
@@ -225,6 +225,41 @@ describe("host/patch", function() {
});
});
describe("loadModules()", function() {
it("should load the bundled clusterio module", async function() {
const modules = await patch.loadModules([]);
assert.deepEqual([...modules.keys()], ["clusterio"]);
assert(modules.get("clusterio").files.has("modules/clusterio/api.lua"));
});
it("should load modules from plugins", async function() {
const pluginInfos = await lib.loadPluginInfos(
new Map([["research_sync", path.resolve("plugins/research_sync")]])
);
const modules = await patch.loadModules(pluginInfos);
assert.deepEqual([...modules.keys()], ["research_sync", "clusterio"]);
assert(modules.get("research_sync").files.has("modules/research_sync/sync.lua"));
});
it("should skip plugins without a module", async function() {
const pluginInfos = await lib.loadPluginInfos(
new Map([["test_plugin", path.resolve("test/file/test_plugin")]])
);
const modules = await patch.loadModules(pluginInfos);
assert.deepEqual([...modules.keys()], ["clusterio"]);
});
it("should throw on a plugin module named like a bundled module", async function() {
const pluginDir = path.join("temp", "test", "clusterio_plugin");
await fs.mkdir(path.join(pluginDir, "module"), { recursive: true });
await fs.writeFile(path.join(pluginDir, "package.json"), "{}");
await fs.writeFile(path.join(pluginDir, "module", "module.json"), "{}");
await assert.rejects(
patch.loadModules([{
name: "clusterio", version: "1.0.0", packagePath: path.resolve(pluginDir, "package.json"),
}]),
new Error("Module with name clusterio already exists in a plugin")
);
});
});
describe("patch()", function() {
it("should throw on unknown scenario", async function() {
let zip = new JSZip();
+9 -9
View File
@@ -41,26 +41,26 @@ describe("host/server", function() {
describe("direct install", function() {
it("should search given directory for latest Factorio install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.2");
const [dir, version] = await hostServer._findVersion(installDir, "latest");
const [dir, version] = await hostServer.findVersion(installDir, "latest");
assert.equal(dir, path.join(installDir, "data"));
assert.equal(version, "0.1.2");
});
it("should search given directory for given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.1");
const [dir, version] = await hostServer._findVersion(installDir, "0.1.1");
const [dir, version] = await hostServer.findVersion(installDir, "0.1.1");
assert.equal(dir, path.join(installDir, "data"));
assert.equal(version, "0.1.1");
});
it("should search given directory for partly given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.2");
const [dir, version] = await hostServer._findVersion(installDir, "0.1");
const [dir, version] = await hostServer.findVersion(installDir, "0.1");
assert.equal(dir, path.join(installDir, "data"));
assert.equal(version, "0.1.2");
});
it("should reject if the version does not match", async function() {
let installDir = path.join("test", "file", "factorio", "0.1.1");
await assert.rejects(
hostServer._findVersion(installDir, "0.1.2"),
hostServer.findVersion(installDir, "0.1.2"),
new Error(
`Unable to find Factorio version 0.1.2: ${installDir} is a direct (single-version) ` +
`install of 0.1.1. Use a versioned layout, where ${installDir} contains a subdirectory ` +
@@ -73,33 +73,33 @@ describe("host/server", function() {
it("should reject if no factorio install with the given version was found", async function() {
let installDir = path.join("test", "file", "factorio");
await assert.rejects(
hostServer._findVersion(installDir, "0.1.3"),
hostServer.findVersion(installDir, "0.1.3"),
new Error("Unable to find Factorio version 0.1.3")
);
});
it("should search given directory for given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio");
const [dir, version] = await hostServer._findVersion(installDir, "0.1.1");
const [dir, version] = await hostServer.findVersion(installDir, "0.1.1");
assert.equal(dir, path.join(installDir, "0.1.1", "data"));
assert.equal(version, "0.1.1");
});
it("should search given directory for partly given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio");
const [dir, version] = await hostServer._findVersion(installDir, "0.1");
const [dir, version] = await hostServer.findVersion(installDir, "0.1");
assert.equal(dir, path.join(installDir, "0.1.2", "data"));
assert.equal(version, "0.1.2");
});
it("should reject if no factorio install with the given version was found", async function() {
let installDir = path.join("test", "file", "factorio");
await assert.rejects(
hostServer._findVersion(installDir, "0.1.3"),
hostServer.findVersion(installDir, "0.1.3"),
new Error("Unable to find Factorio version 0.1.3")
);
});
it("should reject if no factorio install was found", async function() {
let installDir = path.join("test", "file");
await assert.rejects(
hostServer._findVersion(installDir, "0.0.0"),
hostServer.findVersion(installDir, "0.0.0"),
new Error(`Unable to find any Factorio install in ${installDir}`)
);
});
+13
View File
@@ -263,6 +263,19 @@ describe("Integration of Clusterio", function() {
});
describe("clusteriohost", function() {
describe("create-scenario", function() {
it("should create a patched scenario", async function() {
slowTest(this);
requiresFactorio(this);
const output = path.join("temp", "test", "patched-scenario");
await fs.rm(output, { recursive: true, force: true });
await execHost("create-scenario patched-scenario --plugins research_sync");
const control = await fs.readFile(path.join(output, "control.lua"), "utf8");
assert(control.includes('require("modules/research_sync/sync")'));
await fs.access(path.join(output, "scenario.lua"));
});
});
describe("hostUpdateEventHandler()", function() {
it("should trigger when a new host is added", async function() {
slowTest(this);