diff --git a/packages/create/create.js b/packages/create/create.js index c6c0d094..eda9364e 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -699,6 +699,7 @@ async function inquirerMissingArgs(args) { { name: "Command Line", value: "ctl" }, { name: "Web UI", value: "web" }, { name: "No Typescript", value: "js" }, + { name: "No Config", value: "no_config" }, ], }, { diff --git a/packages/create/template.js b/packages/create/template.js index 71ce6734..02549f7d 100644 --- a/packages/create/template.js +++ b/packages/create/template.js @@ -76,8 +76,15 @@ async function copyPluginTemplates(pluginName, templates) { const files = new Map(); const prepare = []; + // Some basic flags for selecting files to include const javascriptOnly = templates.includes("js"); - const webpack = templates.includes("controller") || templates.includes("web"); + const config = !templates.includes("no_config") && templates.some(value => ( + ["controller", "host", "instance", "ctl"].includes(value) + )); + const webpack = config || templates.includes("controller") || templates.includes("web"); + + // A count of the number of isolated contexts the plugin runs under + let pluginContexts = 0; // Get the file extension and path to the templates const ext = javascriptOnly ? "js" : "ts"; @@ -88,7 +95,6 @@ async function copyPluginTemplates(pluginName, templates) { files.set(".gitignore", path.join(commonPath, ".gitignore")); files.set(".npmignore", path.join(commonPath, ".npmignore")); files.set("package.json", path.join(commonPath, "package.json")); - files.set(`messages.${ext}`, path.join(templatePath, `messages.${ext}`)); files.set(`index.${ext}`, path.join(templatePath, `index.${ext}`)); // Files and dependencies to support typescript @@ -105,24 +111,30 @@ async function copyPluginTemplates(pluginName, templates) { if (webpack) { prepare.push("webpack-cli --env production"); files.set("webpack.config.js", path.join(commonPath, "webpack.config.js")); - files.set(`web/index.${ext}x`, path.join(templatePath, - templates.includes("web") ? `web/plugin.${ext}x` : `web/no_plugin.${ext}x` - )); + if (templates.includes("web")) { + files.set(`web/index.${ext}x`, path.join(templatePath, `web/plugin.${ext}x`)); + pluginContexts += 1; + } else { + files.set(`web/index.${ext}x`, path.join(templatePath, `web/no_plugin.${ext}x`)); + } } // Files for the controller if (templates.includes("controller")) { files.set(`controller.${ext}`, path.join(templatePath, `controller.${ext}`)); + pluginContexts += 1; } // Files for hosts if (templates.includes("host")) { files.set(`host.${ext}`, path.join(templatePath, `host.${ext}`)); + pluginContexts += 1; } // Files for instances if (templates.includes("instance")) { files.set(`instance.${ext}`, path.join(templatePath, `instance.${ext}`)); + pluginContexts += 1; } // Files for lua modules @@ -133,16 +145,26 @@ async function copyPluginTemplates(pluginName, templates) { files.set("module/module_exports.lua", path.join(commonPath, "module/module_exports.lua")); if (templates.includes("instance")) { files.set("module/globals.lua", path.join(commonPath, "module/globals.lua")); + } else { + files.set(`instance.${ext}`, path.join(templatePath, `instance_empty.${ext}`)); + pluginContexts += 1; } } // Files for the control tool if (templates.includes("ctl")) { files.set(`ctl.${ext}`, path.join(templatePath, `ctl.${ext}`)); + pluginContexts += 1; + } + + // If there are more than one contexts then include the message file + if (pluginContexts > 1) { + files.set(`messages.${ext}`, path.join(templatePath, `messages.${ext}`)); } // Properties that will control the replacements in the templates const properties = { + config: config, typescript: !javascriptOnly, controller: templates.includes("controller"), host: templates.includes("host"), @@ -150,6 +172,7 @@ async function copyPluginTemplates(pluginName, templates) { module: templates.includes("module"), ctl: templates.includes("ctl"), web: templates.includes("web"), + multi_context: pluginContexts > 1, plugin_name: pluginName, prepare: prepare.join(" && "), ext: ext, diff --git a/packages/create/templates/common/module/control.lua b/packages/create/templates/common/module/control.lua index 2747b83b..458f5cf3 100644 --- a/packages/create/templates/common/module/control.lua +++ b/packages/create/templates/common/module/control.lua @@ -6,8 +6,10 @@ local MyModule = { on_nth_tick = {}, } ---- global is 'synced' between players, you should use your plugin name to avoid conflicts --- setupGlobalData should either be removed or called during clusterio_api.events.on_server_startup +--- global is 'synced' between players, you should use your plugin name to avoid name conflicts +-- The following helper function is desync safe and avoids name conflicts after being called during on_server_startup +-- There are edge cases in the following events: pre player left, player left, console command, and config changed +-- If you use any of these events and access globalData then make sure to call this function first local globalData = {} local function setupGlobalData() if global["__plugin_name__"] == nil then @@ -15,6 +17,12 @@ local function setupGlobalData() -- starting values go here } end + MyModule.on_load() +end + +--- The on_load function is called independently for each client when they first load the map +-- It should be used to restore global aliases and metatables not registered with script.register_metatable +function MyModule.on_load() globalData = global["__plugin_name__"] end @@ -28,7 +36,7 @@ local function bar() game.print("bar") end ---- Clusterio provides a few custom events, on_server_startup is the most useful and should be used in place of on_load +--- Clusterio provides a few custom events, on_server_startup is the most useful and should be used in place of on_init MyModule.events[clusterio_api.events.on_server_startup] = function(event) setupGlobalData() game.print(game.table_to_json(event)) diff --git a/packages/create/templates/plugin-js/controller.js b/packages/create/templates/plugin-js/controller.js index 266e06fa..b578d248 100644 --- a/packages/create/templates/plugin-js/controller.js +++ b/packages/create/templates/plugin-js/controller.js @@ -1,13 +1,17 @@ "use strict"; const lib = require("@clusterio/lib"); const { BaseControllerPlugin } = require("@clusterio/controller"); +//%if multi_context // Subscribing requires multi context const { PluginExampleEvent, PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif +//%if multi_context // Subscribing requires multi context } = require("./messages"); +//%endif class ControllerPlugin extends BaseControllerPlugin { //%if controller & web // Subscribing requires web content and the controller @@ -15,14 +19,18 @@ class ControllerPlugin extends BaseControllerPlugin { storageDirty = false; //%endif +//%if multi_context // Subscribing requires multi context async init() { this.controller.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.controller.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if controller & web // Subscribing requires web content and the controller this.controller.subscriptions.handle(ExampleSubscribableUpdate, this.handleExampleSubscription.bind(this)); this.exampleDatabase = new Map(); // If needed, replace with loading from database file //%endif +//%if multi_context // Subscribing requires multi context } +//%endif async onControllerConfigFieldChanged(field, curr, prev) { this.logger.info(`controller::onControllerConfigFieldChanged ${field}`); @@ -45,6 +53,7 @@ class ControllerPlugin extends BaseControllerPlugin { async onPlayerEvent(instance, event) { this.logger.info(`controller::onPlayerEvent ${instance.id} ${JSON.stringify(event)}`); } +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); @@ -57,6 +66,7 @@ class ControllerPlugin extends BaseControllerPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if controller & web // Subscribing requires web content and the controller async handleExampleSubscription(request) { diff --git a/packages/create/templates/plugin-js/ctl.js b/packages/create/templates/plugin-js/ctl.js index ed1d1a32..95e886b9 100644 --- a/packages/create/templates/plugin-js/ctl.js +++ b/packages/create/templates/plugin-js/ctl.js @@ -1,6 +1,7 @@ "use strict"; const { BaseCtlPlugin } = require("@clusterio/ctl"); const { CommandTree, Command } = require("@clusterio/lib"); +//%// We do not check for multi context here because it doesn't make sense to have a ctl without messages const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); /* eslint-disable no-console */ diff --git a/packages/create/templates/plugin-js/host.js b/packages/create/templates/plugin-js/host.js index 2aed38b9..c1e1468d 100644 --- a/packages/create/templates/plugin-js/host.js +++ b/packages/create/templates/plugin-js/host.js @@ -1,13 +1,17 @@ "use strict"; const lib = require("@clusterio/lib"); const { BaseHostPlugin } = require("@clusterio/host"); +//%if multi_context const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +//%endif class HostPlugin extends BaseHostPlugin { +//%if multi_context async init() { this.host.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.host.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); } +//%endif async onHostConfigFieldChanged(field, curr, prev) { this.logger.info(`host::onInstanceConfigFieldChanged ${field}`); @@ -16,6 +20,7 @@ class HostPlugin extends BaseHostPlugin { async onShutdown() { this.logger.info("host::onShutdown"); } +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); @@ -28,6 +33,7 @@ class HostPlugin extends BaseHostPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } module.exports = { diff --git a/packages/create/templates/plugin-js/index.js b/packages/create/templates/plugin-js/index.js index 67d5df0d..26c5387d 100644 --- a/packages/create/templates/plugin-js/index.js +++ b/packages/create/templates/plugin-js/index.js @@ -1,5 +1,6 @@ "use strict"; const lib = require("@clusterio/lib"); +//%if multi_context const Messages = require("./messages"); lib.definePermission({ @@ -13,6 +14,7 @@ lib.definePermission({ title: "Example permission request", description: "Example Description. Request. Change me in index.js", }); +//%endif //%if controller & web // Subscribing requires web content and the controller lib.definePermission({ @@ -34,9 +36,13 @@ const plugin = { name: "__plugin_name__", title: "__plugin_name__", description: "Example Description. Plugin. Change me in index.js", -//%if controller +//%if controller | host & !config | instance & !config | ctl & !config // Blank line for formatting - controllerEntrypoint: "./controller", +//%endif +//%if controller +controllerEntrypoint: "./dist/node/controller", +//%endif +//%if controller & config controllerConfigFields: { "__plugin_name__.myControllerField": { title: "My Controller Field", @@ -46,9 +52,13 @@ const plugin = { }, }, //%endif -//%if host +//%if host & config // Blank line for formatting - hostEntrypoint: "./host", +//%endif +//%if host + hostEntrypoint: "./dist/node/host", +//%endif +//%if host & config hostConfigFields: { "__plugin_name__.myHostField": { title: "My Host Field", @@ -58,9 +68,13 @@ const plugin = { }, }, //%endif -//%if instance +//%if instance & config | module & config // Blank line for formatting - instanceEntrypoint: "./instance", +//%endif +//%if instance | module // Modules load an empty instance plugin + instanceEntrypoint: "./dist/node/instance", +//%endif +//%if instance & config instanceConfigFields: { "__plugin_name__.myInstanceField": { title: "My Instance Field", @@ -70,9 +84,13 @@ const plugin = { }, }, //%endif -//%if ctl +//%if ctl & config // Blank line for formatting - ctlEntrypoint: "./ctl", +//%endif +//%if ctl + ctlEntrypoint: "./dist/node/ctl", +//%endif +//%if ctl & config controlConfigFields: { "__plugin_name__.myControlField": { title: "My Control Field", @@ -82,14 +100,18 @@ const plugin = { }, }, //%endif +//%if multi_context // Subscribing requires multi context messages: [ Messages.PluginExampleEvent, Messages.PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller Messages.ExampleSubscribableUpdate, //%endif +//%if multi_context // Subscribing requires multi context ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", @@ -97,11 +119,6 @@ const plugin = { "/__plugin_name__", ], //%endif -//%if controller & !web // The controller always includes web entry even if there is no content - - webEntrypoint: "./web", - routes: [], -//%endif }; module.exports = { diff --git a/packages/create/templates/plugin-js/instance.js b/packages/create/templates/plugin-js/instance.js index d3bfa189..6993aa1d 100644 --- a/packages/create/templates/plugin-js/instance.js +++ b/packages/create/templates/plugin-js/instance.js @@ -1,16 +1,23 @@ "use strict"; const lib = require("@clusterio/lib"); const { BaseInstancePlugin } = require("@clusterio/host"); +//%if multi_context const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +//%endif class InstancePlugin extends BaseInstancePlugin { +//%if multi_context | module async init() { +//%if multi_context this.instance.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.instance.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if module this.instance.server.handle("__plugin_name__-plugin_example_ipc", this.handlePluginExampleIPC.bind(this)); //%endif +//%if multi_context | module } +//%endif async onInstanceConfigFieldChanged(field, curr, prev) { this.logger.info(`instance::onInstanceConfigFieldChanged ${field}`); @@ -26,13 +33,16 @@ class InstancePlugin extends BaseInstancePlugin { async onPlayerEvent(event) { this.logger.info(`onPlayerEvent::onPlayerEvent ${JSON.stringify(event)}`); +//%if module + if (this.instance.status === "running") { + this.sendRcon("/sc __plugin_name__.foo()"); + } +//%endif } +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); -//%if module - this.sendRcon("/sc __plugin_name__.foo()"); -//%endif } async handlePluginExampleRequest(request) { @@ -42,6 +52,7 @@ class InstancePlugin extends BaseInstancePlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if module async handlePluginExampleIPC(event) { diff --git a/packages/create/templates/plugin-js/instance_empty.js b/packages/create/templates/plugin-js/instance_empty.js new file mode 100644 index 00000000..177f9952 --- /dev/null +++ b/packages/create/templates/plugin-js/instance_empty.js @@ -0,0 +1,11 @@ +"use strict"; +const { BaseInstancePlugin } = require("@clusterio/host"); + +class InstancePlugin extends BaseInstancePlugin { + // This class is empty because an instance plugin must be defined for a module to be injected + // This requirement may change in the future to allow for standalone modules +} + +module.exports = { + InstancePlugin, +}; diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 9cc5942e..2f94158a 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState, -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller useCallback, useSyncExternalStore, //%endif } from "react"; @@ -12,33 +12,37 @@ import React, { import { BaseWebPlugin, PageLayout, Control, ControlContext, notifyErrorHandler, } from "@clusterio/web_ui"; +//%if multi_context // Subscribing requires multi context import { PluginExampleEvent, PluginExampleRequest, -//%if controller & web // Subscribing requires web content and the controller - ExampleSubscribableUpdate, //%endif +//%if controller // Subscribing requires web content and the controller + ExampleSubscribableUpdate, ExampleSubscribableValue, +//%endif +//%if multi_context // Subscribing requires multi context } from "../messages"; +//%endif import * as lib from "@clusterio/lib"; function MyTemplatePage() { let control = useContext(ControlContext); -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller const plugin = control.plugins.get("__plugin_name__"); const [subscribableData, synced] = plugin.useSubscribableData(); //%endif return

__plugin_name__

-//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller Synced: {String(synced)} Data: {JSON.stringify([...subscribableData.values()])} //%endif
; } export class WebPlugin extends BaseWebPlugin { -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller subscribableData = new lib.EventSubscriber(ExampleSubscribableUpdate, this.control); //%endif @@ -47,15 +51,24 @@ export class WebPlugin extends BaseWebPlugin { { path: "/__plugin_name__", sidebarName: "__plugin_name__", - permission: "__plugin_name__.page.view", + // This permission is client side only, so it must match the permission string of a resource request to be secure + // An undefined value means that the page will always be visible +//%if controller // Subscribing requires web content and the controller + permission: "__plugin_name__.example.permission.subscribe", +//%endif +//%if !controller + permission: undefined, // "__plugin_name__.example.permission.request", +//%endif content: , }, ]; +//%if multi_context this.control.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.control.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif } -//%if controller & web // Subscribing requires web content and the controller +//%if controller// Subscribing requires web content and the controller useSubscribableData() { const control = useContext(ControlContext); @@ -63,6 +76,7 @@ export class WebPlugin extends BaseWebPlugin { return useSyncExternalStore(subscribe, () => this.subscribableData.getSnapshot()); } //%endif +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); @@ -75,4 +89,5 @@ export class WebPlugin extends BaseWebPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } diff --git a/packages/create/templates/plugin-ts/controller.ts b/packages/create/templates/plugin-ts/controller.ts index cab2d3c0..cca31956 100644 --- a/packages/create/templates/plugin-ts/controller.ts +++ b/packages/create/templates/plugin-ts/controller.ts @@ -1,12 +1,16 @@ import * as lib from "@clusterio/lib"; import { BaseControllerPlugin, InstanceInfo } from "@clusterio/controller"; +//%if multi_context // Subscribing requires multi context import { PluginExampleEvent, PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif +//%if multi_context // Subscribing requires multi context } from "./messages"; +//%endif export class ControllerPlugin extends BaseControllerPlugin { //%if controller & web // Subscribing requires web content and the controller @@ -14,14 +18,18 @@ export class ControllerPlugin extends BaseControllerPlugin { storageDirty = false; //%endif +//%if multi_context // Subscribing requires multi context async init() { this.controller.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.controller.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if controller & web // Subscribing requires web content and the controller this.controller.subscriptions.handle(ExampleSubscribableUpdate, this.handleExampleSubscription.bind(this)); this.exampleDatabase = new Map(); // If needed, replace with loading from database file //%endif +//%if multi_context // Subscribing requires multi context } +//%endif async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.logger.info(`controller::onControllerConfigFieldChanged ${field}`); @@ -44,6 +52,7 @@ export class ControllerPlugin extends BaseControllerPlugin { async onPlayerEvent(instance: InstanceInfo, event: lib.PlayerEvent) { this.logger.info(`controller::onPlayerEvent ${instance.id} ${JSON.stringify(event)}`); } +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -56,6 +65,7 @@ export class ControllerPlugin extends BaseControllerPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if controller & web // Subscribing requires web content and the controller async handleExampleSubscription(request: lib.SubscriptionRequest) { diff --git a/packages/create/templates/plugin-ts/ctl.ts b/packages/create/templates/plugin-ts/ctl.ts index 75c3a1a4..dc0fa01b 100644 --- a/packages/create/templates/plugin-ts/ctl.ts +++ b/packages/create/templates/plugin-ts/ctl.ts @@ -1,6 +1,6 @@ -type Control = any; // import type { Control } from "@clusterio/ctl"; -import { BaseCtlPlugin } from "@clusterio/ctl"; +import { BaseCtlPlugin, type Control } from "@clusterio/ctl"; import { CommandTree, Command } from "@clusterio/lib"; +//%// We do not check for multi context here because it doesn't make sense to have a ctl without messages import { PluginExampleEvent, PluginExampleRequest } from "./messages"; /* eslint-disable no-console */ diff --git a/packages/create/templates/plugin-ts/host.ts b/packages/create/templates/plugin-ts/host.ts index bdfc05b5..9a395988 100644 --- a/packages/create/templates/plugin-ts/host.ts +++ b/packages/create/templates/plugin-ts/host.ts @@ -1,12 +1,16 @@ import * as lib from "@clusterio/lib"; import { BaseHostPlugin } from "@clusterio/host"; +//%if multi_context import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +//%endif export class HostPlugin extends BaseHostPlugin { +//%if multi_context async init() { this.host.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.host.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); } +//%endif async onHostConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.logger.info(`host::onInstanceConfigFieldChanged ${field}`); @@ -15,6 +19,7 @@ export class HostPlugin extends BaseHostPlugin { async onShutdown() { this.logger.info("host::onShutdown"); } +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -27,4 +32,5 @@ export class HostPlugin extends BaseHostPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } diff --git a/packages/create/templates/plugin-ts/index.ts b/packages/create/templates/plugin-ts/index.ts index d90f22be..8ef46604 100644 --- a/packages/create/templates/plugin-ts/index.ts +++ b/packages/create/templates/plugin-ts/index.ts @@ -1,4 +1,5 @@ import * as lib from "@clusterio/lib"; +//%if multi_context import * as Messages from "./messages"; lib.definePermission({ @@ -12,6 +13,7 @@ lib.definePermission({ title: "Example permission request", description: "Example Description. Request. Change me in index.ts", }); +//%endif //%if controller & web // Subscribing requires web content and the controller lib.definePermission({ @@ -28,37 +30,46 @@ lib.definePermission({ description: "Example Description. View. Change me in index.ts", }); //%endif +//%if config declare module "@clusterio/lib" { -//%if controller +//%endif +//%if controller & config export interface ControllerConfigFields { "__plugin_name__.myControllerField": string; } //%endif -//%if host +//%if host & config export interface HostConfigFields { "__plugin_name__.myHostField": string; } //%endif -//%if instance +//%if instance & config export interface InstanceConfigFields { "__plugin_name__.myInstanceField": string; } //%endif -//%if ctl +//%if ctl & config export interface ControlConfigFields { "__plugin_name__.myControlField": string; } //%endif +//%if config } +//%endif export const plugin: lib.PluginDeclaration = { name: "__plugin_name__", title: "__plugin_name__", description: "Example Description. Plugin. Change me in index.ts", -//%if controller +//%// There are a lot of statements here to make the formatting look good with and without 'config' +//%if controller | host & !config | instance & !config | ctl & !config // Blank line for formatting +//%endif +//%if controller controllerEntrypoint: "./dist/node/controller", +//%endif +//%if controller & config controllerConfigFields: { "__plugin_name__.myControllerField": { title: "My Controller Field", @@ -68,9 +79,13 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif -//%if host +//%if host & config // Blank line for formatting +//%endif +//%if host hostEntrypoint: "./dist/node/host", +//%endif +//%if host & config hostConfigFields: { "__plugin_name__.myHostField": { title: "My Host Field", @@ -80,9 +95,13 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif -//%if instance +//%if instance & config | module & config // Blank line for formatting +//%endif +//%if instance | module // Modules load an empty instance plugin instanceEntrypoint: "./dist/node/instance", +//%endif +//%if instance & config instanceConfigFields: { "__plugin_name__.myInstanceField": { title: "My Instance Field", @@ -92,9 +111,13 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif -//%if ctl +//%if ctl & config // Blank line for formatting +//%endif +//%if ctl ctlEntrypoint: "./dist/node/ctl", +//%endif +//%if ctl & config controlConfigFields: { "__plugin_name__.myControlField": { title: "My Control Field", @@ -104,14 +127,18 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif +//%if multi_context // Subscribing requires multi context messages: [ Messages.PluginExampleEvent, Messages.PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller Messages.ExampleSubscribableUpdate, //%endif +//%if multi_context // Subscribing requires multi context ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", @@ -119,9 +146,4 @@ export const plugin: lib.PluginDeclaration = { "/__plugin_name__", ], //%endif -//%if controller & !web // The controller always includes web entry even if there is no content - - webEntrypoint: "./web", - routes: [], -//%endif }; diff --git a/packages/create/templates/plugin-ts/instance.ts b/packages/create/templates/plugin-ts/instance.ts index 3cb7d18a..21af1189 100644 --- a/packages/create/templates/plugin-ts/instance.ts +++ b/packages/create/templates/plugin-ts/instance.ts @@ -1,6 +1,8 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; +//%if multi_context import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +//%endif //%if module type PuginExampleIPC = { @@ -10,13 +12,19 @@ type PuginExampleIPC = { //%endif export class InstancePlugin extends BaseInstancePlugin { +//%if multi_context | module async init() { +//%endif +//%if multi_context this.instance.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.instance.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if module this.instance.server.handle("__plugin_name__-plugin_example_ipc", this.handlePluginExampleIPC.bind(this)); //%endif +//%if multi_context | module } +//%endif async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.logger.info(`instance::onInstanceConfigFieldChanged ${field}`); @@ -33,9 +41,12 @@ export class InstancePlugin extends BaseInstancePlugin { async onPlayerEvent(event: lib.PlayerEvent) { this.logger.info(`onPlayerEvent::onPlayerEvent ${JSON.stringify(event)}`); //%if module - this.sendRcon("/sc __plugin_name__.foo()"); + if (this.instance.status === "running") { + this.sendRcon("/sc __plugin_name__.foo()"); + } //%endif } +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -48,6 +59,7 @@ export class InstancePlugin extends BaseInstancePlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if module async handlePluginExampleIPC(event: PuginExampleIPC) { diff --git a/packages/create/templates/plugin-ts/instance_empty.ts b/packages/create/templates/plugin-ts/instance_empty.ts new file mode 100644 index 00000000..e8fa7eeb --- /dev/null +++ b/packages/create/templates/plugin-ts/instance_empty.ts @@ -0,0 +1,7 @@ +import * as lib from "@clusterio/lib"; +import { BaseInstancePlugin } from "@clusterio/host"; + +export class InstancePlugin extends BaseInstancePlugin { + // This class is empty because an instance plugin must be defined for a module to be injected + // This requirement may change in the future to allow for standalone modules +} diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index df32127d..06a7542a 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -12,33 +12,37 @@ import React, { import { BaseWebPlugin, PageLayout, Control, ControlContext, notifyErrorHandler, } from "@clusterio/web_ui"; +//%if multi_context // Subscribing requires multi context import { PluginExampleEvent, PluginExampleRequest, -//%if controller & web // Subscribing requires web content and the controller - ExampleSubscribableUpdate, //%endif +//%if controller & web // Subscribing requires web content and the controller + ExampleSubscribableUpdate, ExampleSubscribableValue, +//%endif +//%if multi_context // Subscribing requires multi context } from "../messages"; +//%endif import * as lib from "@clusterio/lib"; function MyTemplatePage() { const control = useContext(ControlContext); -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller const plugin = control.plugins.get("__plugin_name__") as WebPlugin; const [subscribableData, synced] = plugin.useSubscribableData(); //%endif return

__plugin_name__

-//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller Synced: {String(synced)} Data: {JSON.stringify([...subscribableData.values()])} //%endif
; } export class WebPlugin extends BaseWebPlugin { -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller subscribableData = new lib.EventSubscriber(ExampleSubscribableUpdate, this.control); //%endif @@ -49,20 +53,22 @@ export class WebPlugin extends BaseWebPlugin { sidebarName: "__plugin_name__", // This permission is client side only, so it must match the permission string of a resource request to be secure // An undefined value means that the page will always be visible -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller permission: "__plugin_name__.example.permission.subscribe", //%endif -//%if !controller | !web +//%if !controller permission: undefined, // "__plugin_name__.example.permission.request", //%endif content: , }, ]; +//%if multi_context this.control.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.control.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif } -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller useSubscribableData() { const control = useContext(ControlContext); @@ -70,6 +76,7 @@ export class WebPlugin extends BaseWebPlugin { return useSyncExternalStore(subscribe, () => this.subscribableData.getSnapshot()); } //%endif +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -82,4 +89,5 @@ export class WebPlugin extends BaseWebPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif }