mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Merge pull request #1014 from bbassie/feature/patched-scenario
Add create-scenario command to clusteriohost
This commit is contained in:
11 files changed
+496
-36
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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}`)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user