diff --git a/client.js b/client.js index 444d6c0d..d0e6f96d 100644 --- a/client.js +++ b/client.js @@ -155,37 +155,56 @@ class Instance extends link.Link{ // Patch save with lua modules from plugins console.log("Clusterio | Patching save"); - // For now it's assumed that all files in the lua folder of a plugin is - // to be patched in under the name of the plugin and loaded for all - // plugins that are not disabled. This will most likely change in the - // future when the plugin refactor is done. - let modules = []; - for (let pluginName of await fs.readdir("plugins")) { - let pluginDir = path.join("plugins", pluginName); - if (await fs.pathExists(path.join(pluginDir, "DISABLED"))) { + // Find plugin modules to patch in + let modules = new Map(); + for (let pluginName of this.plugins.keys()) { + let modulePath = path.join('plugins', pluginName, 'module'); + if (!await fs.pathExists(modulePath)) { continue; } - if (!await fs.pathExists(path.join(pluginDir, "lua"))) { - continue; + let moduleJsonPath = path.join(modulePath, 'module.json'); + if (!await fs.pathExists(moduleJsonPath)) { + throw new Error(`Module for plugin ${pluginName} is missing module.json`); } - let module = { - "name": pluginName, - "files": [], - }; - - for (let fileName of await fs.readdir(path.join(pluginDir, "lua"))) { - module["files"].push({ - path: pluginName+"/"+fileName, - content: await fs.readFile(path.join(pluginDir, "lua", fileName)), - load: true, - }); + let module = JSON.parse(await fs.readFile(moduleJsonPath)); + if (module.name !== pluginName) { + throw new Error(`Expected name of module for plugin ${pluginName} to match the plugin name`); } - modules.push(module); + module = Object.assign({ + path: modulePath, + load: [], + }, module); + modules.set(module.name, module); } - await factorio.patch(this.path("saves", saveName), modules); + + for (let entry of await fs.readdir('modules', { withFileTypes: true })) { + if (entry.isDirectory()) { + if (modules.has(entry.name)) { + throw new Error(`Module with name ${entry.name} already exists in a plugin`); + } + + let moduleJsonPath = path.join('modules', entry.name, 'module.json'); + if (!await fs.pathExists(moduleJsonPath)) { + throw new Error(`Module ${entry.name} is missing module.json`); + } + + let module = JSON.parse(await fs.readFile(moduleJsonPath)); + if (module.name !== entry.name) { + throw new Error(`Expected name of module ${entry.name} to match the directory name`); + } + + module = Object.assign({ + path: path.join('modules', entry.name), + load: [], + }, module); + modules.set(module.name, module); + } + } + + await factorio.patch(this.path("saves", saveName), modules.values()); this.server.on('rcon-ready', () => { console.log("Clusterio | RCON connection established"); @@ -805,6 +824,7 @@ async function startClient() { await fs.ensureDir(slaveConfig.instanceDirectory); await fs.ensureDir("sharedMods"); + await fs.ensureDir("modules"); // Set the process title, shows up as the title of the CMD window on windows // and as the process name in ps/top on linux. diff --git a/docs/developing-for-clusterio.md b/docs/developing-for-clusterio.md index 5cf4958f..6083285e 100644 --- a/docs/developing-for-clusterio.md +++ b/docs/developing-for-clusterio.md @@ -180,13 +180,40 @@ for some pointers if you get stuck. Clusterio Modules ----------------- -Modules are primarily used by plugins to inject code into Factorio -games with the save patcher. The save patcher puts modules into the -`modules` folder of the Factorio save and adds code to `control.lua` to +Modules are primarily used by plugins to inject code into Factorio games +with the save patcher, though it's also possible to make stand alone +modules that are loaded into the game if you don't need the capabilites +of the plugin system. The save patcher puts modules into the `modules` +folder of the Factorio save and adds code to `control.lua` to load the modules by requiring it and passing the result of the require call to the `add_lib` function of the event_handler lib. See the section on the [event_handler interface](#event_handler-interface) for a -detailed description on how this works. +detailed description on how the event_handler lib works. + +Stand alone modules are placed into the modules folder of Clusterio, +plugin modules are located in the module folder of the plugin. In +either case a `module.json` file is required and has the following +structure: + +```json +{ + "name": "my_module", + "load": ["foo.lua"] +} +``` + +The following entries are supported in the module.json file: + +- `name`: + Name of the module, must match the folder the module is located in + for stand alone modules or the name of the plugin the module is + located in. +- `load`: + Lua files to load with the `event_handler` lib. They should be + specified as paths that's relative to the module and uses forward + slashes as directory sepparators. Thus foo.lua will require a file + by that name and feed the result of that require to the event + handler lib. Because modules can be patched into an existing game you cannot rely on the `on_init` callback to be called in Clusterio Modules. Nor can you @@ -194,11 +221,6 @@ rely on the `on_configuration_changed` callback, as this is not called when level code changes. Instead you will have to initialize whatever global variable you need when you first use them. -Currently any files in a folder named `lua` in a Clusterio plugin is -assumed to be a module and will be patched into the save before starting -up Factorio. This will most likely change with the planned plugin -restructuring. - Clusterio Plugins ----------------- diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index 86b450d6..9926e845 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -18,7 +18,8 @@ The basic file structure of a plugin is the following. +- instance.js +- test/ | +- plugin.js - +- lua/ + +- module/ + +- module.json +- plugin.lua The most important file is the `info.js` file. Without it the plugin @@ -70,6 +71,12 @@ The following properties are recognized: Object with link messages definitions for this plugin. See guide for [defining link messages](#defining-link-messages) below. +The optional module folder contains a Clusterio module that will be +patched into the save when the plugin is enabled. See the section on +[Clusterio Modules](developing-for-clusterio.md) in the Developing for +Clusterio document. The only restriction imposed on modules embedded +into plugins is that they must be named the same as the plugin. + While there is no standard for how to organize a plugin it's recommended to put the MasterPlugin class definition into master.js and the InstancePlugin class definition into instance.js. You can put them into diff --git a/lib/factorio/patch.js b/lib/factorio/patch.js index 235d53b5..41ebf1c0 100644 --- a/lib/factorio/patch.js +++ b/lib/factorio/patch.js @@ -7,6 +7,7 @@ const events = require("events"); const fs = require("fs-extra"); const jszip = require("jszip"); +const path = require("path"); const hash = require("lib/hash"); @@ -81,7 +82,7 @@ function generateLoader(patchInfo) { for (let module of patchInfo["modules"]) { for (let file of module["files"]) { if (file["load"]) { - let requirePath = "modules/" + file["path"].slice(0, -4); + let requirePath = `modules/${module.name}/${file["path"].slice(0, -4)}`; lines.push('event_handler.add_lib(require("'+requirePath+'"))'); } } @@ -135,19 +136,31 @@ async function patch(savePath, modules) { } // Add the modules to the save. - let modulesDir = root.folder("modules"); for (let module of modules) { + let moduleDir = root.folder(`modules/${module.name}`); 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), - }); + let dirs = [[module.path, '']]; + 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()) { + moduleDir.file(relativePath, await fs.readFile(fsPath)); + moduleInfo["files"].push({ + "path": relativePath, + "load": module.load.includes(relativePath), + }); + + } else if (entry.isDirectory()) { + dirs.push([fsPath, relativePath]); + } + } } patchInfo["modules"].push(moduleInfo); diff --git a/plugins/statistics_exporter/lua/export.lua b/plugins/statistics_exporter/module/export.lua similarity index 100% rename from plugins/statistics_exporter/lua/export.lua rename to plugins/statistics_exporter/module/export.lua diff --git a/plugins/statistics_exporter/module/module.json b/plugins/statistics_exporter/module/module.json new file mode 100644 index 00000000..253cd328 --- /dev/null +++ b/plugins/statistics_exporter/module/module.json @@ -0,0 +1,4 @@ +{ + "name": "statistics_exporter", + "load": ["export.lua"] +} diff --git a/plugins/statistics_exporter/slave.js b/plugins/statistics_exporter/slave.js deleted file mode 100644 index e69de29b..00000000 diff --git a/test/file/modules/subdir/dir/test.lua b/test/file/modules/subdir/dir/test.lua new file mode 100644 index 00000000..181e93bd --- /dev/null +++ b/test/file/modules/subdir/dir/test.lua @@ -0,0 +1 @@ +-- test diff --git a/test/file/modules/test/test.lua b/test/file/modules/test/test.lua new file mode 100644 index 00000000..181e93bd --- /dev/null +++ b/test/file/modules/test/test.lua @@ -0,0 +1 @@ +-- test diff --git a/test/integration/patch.js b/test/integration/patch.js index 25e8b1f8..9897bc28 100644 --- a/test/integration/patch.js +++ b/test/integration/patch.js @@ -23,18 +23,23 @@ describe("Integration of lib/factorio/patch", function() { slowTest(this); await factorio.patch(savePath, [{ name: "test", - files: [{ path: "test.lua", content: "-- test" }] + path: "test/file/modules/test", + load: [], + }, { + name: "subdir", + path: "test/file/modules/subdir", + load: [], }]); let zip = await jszip.loadAsync(await fs.readFile(savePath)); - let content = await zip.file("test/modules/test.lua").async("string"); - assert.equal(content, "-- test"); + assert.equal(await zip.file("test/modules/test/test.lua").async("string"), "-- test\n"); + assert.equal(await zip.file("test/modules/subdir/dir/test.lua").async("string"), "-- test\n"); }); it("should remove old modules in a save", async function() { slowTest(this); - await factorio.patch(savePath, [{ name: "test", files: [] }]); + await factorio.patch(savePath, []); let zip = await jszip.loadAsync(await fs.readFile(savePath)); - assert.equal(zip.file("test/modules/test.lua"), null); + assert.equal(zip.file("test/modules/test/test.lua"), null); }); }); }); diff --git a/test/lib/factorio/patch.js b/test/lib/factorio/patch.js index c9f3900f..3815a852 100644 --- a/test/lib/factorio/patch.js +++ b/test/lib/factorio/patch.js @@ -59,7 +59,7 @@ describe("lib/factorio", function() { 'event_handler.add_lib(require("foo"))', '', '-- Clusterio modules', - 'event_handler.add_lib(require("modules/bar"))', + 'event_handler.add_lib(require("modules/spam/bar"))', '', ].join('\n'); let patchInfo = {