Export init functions instead of plugin classes

clusterio logs "using deprecated class export" for every node entrypoint
that exports a Base*Plugin subclass, and the web UI logs "using deprecated
class hooks" for WebPlugin classes. Every entrypoint now has a default
export function, like exp_auto_restart and the exp_roles web plugin.

The classes stay where they hold state, but no longer extend the base
classes. The constructor takes the load context and init() attaches the
hooks the base class used to attach from the on* methods, so the tests can
still call those methods directly. exp_server_ups sends its RCON command
through instance.sendRcon with the plugin name, which is what the removed
BaseInstancePlugin.sendRcon did.

The instance entrypoints of exp_commands, exp_gui, exp_legacy, exp_util
and exp_scenario were empty classes, kept only so the host loads the
plugin and patches its module into the save. An empty function does the
same.

The exp_groups web components read the subscriptions through exported
useGroups, useAssignments and useRoleMappings hooks instead of looking
the plugin up in control.plugins, which only holds class plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-09-27 00:10:51 +00:00
1 parent ddf8497fa3
commit 0ff07c012c
26 files changed
+268 -131

No files matched your search

+3 -7
View File
@@ -1,7 +1,3 @@
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
}
// Empty because an instance entrypoint must be defined for a module to be injected
// This requirement may change in the future to allow for standalone modules
export default async function () {}
+17 -2
View File
@@ -1,11 +1,14 @@
import { BaseControllerPlugin, type Controller } from "@clusterio/controller";
import type { Controller, ControllerPluginContext } from "@clusterio/controller";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
import * as path from "node:path";
const loaded = new WeakMap<Controller, ControllerPlugin>();
export class ControllerPlugin extends BaseControllerPlugin {
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
name: string;
groups!: lib.SubscribableDatastore<messages.GroupRecord>;
roleMappings!: lib.SubscribableDatastore<messages.RoleMappingRecord>;
manualAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
@@ -16,6 +19,12 @@ export class ControllerPlugin extends BaseControllerPlugin {
return loaded.get(controller);
}
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
loaded.set(this.controller, this);
const databaseDirectory = this.controller.config.get("controller.database_directory");
@@ -70,6 +79,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
this.controller.handle(messages.RoleMappingDeleteRequest, this.handleRoleMappingDeleteRequest.bind(this));
this.controller.handle(messages.RoleMappingGetRequest, this.handleRoleMappingGetRequest.bind(this));
this.controller.handle(messages.RoleMappingListRequest, this.handleRoleMappingListRequest.bind(this));
this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this));
}
async onShutdown() {
@@ -440,3 +451,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
return Promise.all(playerNames.map(name => this.computeResolvedAssignment(name)));
}
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
}
+20 -2
View File
@@ -1,4 +1,4 @@
import { BaseInstancePlugin } from "@clusterio/host";
import type { Instance, InstancePluginContext } from "@clusterio/host";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
@@ -17,7 +17,10 @@ export type IpcPlayerAssignments = {
assignments: Record<string, number>,
};
export class InstancePlugin extends BaseInstancePlugin {
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
// Once only, don't send permissions for these groups
// This is used for groups created on this instance that only need the controller generated id
skipSendingPermissions = new Set<string>();
@@ -26,12 +29,23 @@ export class InstancePlugin extends BaseInstancePlugin {
// Track known online players so that we only apply assignment updates for them
onlinePlayers = new Set<string>();
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
this.instance.handle(messages.GroupUpdatedEvent, this.handleGroupUpdatedEvent.bind(this));
this.instance.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentUpdatedEvent.bind(this));
this.instance.server.handle(`exp_group:group_updated`, this.handleGroupUpdatedIPC.bind(this))
this.instance.server.handle(`exp_group:group_deleted`, this.handleGroupDeletedIPC.bind(this))
this.instance.server.handle(`exp_group:player_assignments`, this.handlePlayerAssignmentsIPC.bind(this))
const hooks = this.instance.hooks;
hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this));
hooks.start.attach(this.name, this.onStart.bind(this));
hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this));
}
async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
@@ -169,3 +183,7 @@ export class InstancePlugin extends BaseInstancePlugin {
await this.instance.sendRcon(`/sc exp_groups.${receiver}(helpers.json_to_table[=[${JSON.stringify(json)}]=])`, true)
}
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
}
+2 -3
View File
@@ -3,7 +3,7 @@ import { Modal, Form, Select } from "antd";
import { ControlContext, useUsers } from "@clusterio/web_ui";
import * as messages from "../../messages.js";
import type { WebPlugin } from "..";
import { useGroups } from "..";
export default function AssignmentForm({ open, setOpen, initial }: {
open: boolean,
@@ -11,9 +11,8 @@ export default function AssignmentForm({ open, setOpen, initial }: {
initial?: messages.AssignmentRecord,
}) {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const [groups] = plugin.useGroups();
const [groups] = useGroups();
const [users] = useUsers();
const [form] = Form.useForm();
@@ -4,7 +4,7 @@ import { EditOutlined, SearchOutlined } from "@ant-design/icons";
import { ControlContext, useAccount } from "@clusterio/web_ui";
import { AssignmentDeleteRequest, AssignmentRecord } from "../../messages.js";
import type { WebPlugin } from "..";
import { useAssignments, useGroups } from "..";
import AssignmentForm from "./AssignmentForm";
import DeletedConfirm from "./DeleteConfirm";
@@ -13,13 +13,12 @@ const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base"
export default function AssignmentsTable() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const account = useAccount();
const searchInput = useRef<InputRef>(null);
const [assignments, assignmentsSynced] = plugin.useAssignments();
const [groups, groupsSynced] = plugin.useGroups();
const [assignments, assignmentsSynced] = useAssignments();
const [groups, groupsSynced] = useGroups();
const [editing, setEditing] = useState<AssignmentRecord | undefined>();
const [open, setOpen] = useState(false);
+2 -3
View File
@@ -6,7 +6,7 @@ import { ControlContext, useAccount, useDefaultModPack, PageLayout, PageHeader,
import DeletedConfirm from "./DeleteConfirm";
import * as messages from "../../messages.js";
import type { WebPlugin } from "..";
import { useGroups } from "..";
const DOMAIN_MAPPING = {
"Admin": ["admin", "cheat", "permission", "infinity", "editor", "spawn"],
@@ -34,14 +34,13 @@ function getDomain(name: string) {
export default function GroupViewPage() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const navigate = useNavigate();
const account = useAccount();
const { id } = useParams();
const groupId = Number(id);
const [groups, synced] = plugin.useGroups();
const [groups, synced] = useGroups();
const group = groups.get(groupId);
const [name, setName] = useState("");
+3 -6
View File
@@ -1,21 +1,18 @@
import React, { useContext, useState } from "react";
import React, { useState } from "react";
import { Table } from "antd";
import { useNavigate } from "react-router-dom";
import { ControlContext } from "@clusterio/web_ui";
import { GroupRecord } from "../../messages.js";
import type { WebPlugin } from "..";
import { useGroups } from "..";
import GroupForm from "./GroupForm";
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
export default function GroupsTable() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const navigate = useNavigate();
const [groups, synced] = plugin.useGroups();
const [groups, synced] = useGroups();
const [open, setOpen] = useState(false);
@@ -3,7 +3,7 @@ import { Modal, Form, Select, InputNumber, Switch, Alert } from "antd";
import { ControlContext, useRoles } from "@clusterio/web_ui";
import { RoleMappingRecord, RoleMappingCreateRequest, RoleMappingUpdateRequest } from "../../messages.js";
import type { WebPlugin } from "..";
import { useGroups } from "..";
export default function RoleMappingForm({ open, setOpen, initial }: {
open: boolean,
@@ -11,9 +11,8 @@ export default function RoleMappingForm({ open, setOpen, initial }: {
initial?: RoleMappingRecord,
}) {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const [groups] = plugin.useGroups();
const [groups] = useGroups();
const [roles] = useRoles();
const [form] = Form.useForm();
@@ -4,18 +4,17 @@ import { EditOutlined } from "@ant-design/icons";
import { ControlContext, useAccount, useRoles } from "@clusterio/web_ui";
import { RoleMappingRecord, RoleMappingDeleteRequest } from "../../messages.js";
import type { WebPlugin } from "..";
import { useGroups, useRoleMappings } from "..";
import RoleMappingForm from "./RoleMappingForm";
import DeletedConfirm from "./DeleteConfirm";
export default function RoleMappingsTable() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const account = useAccount();
const [roleMappings, roleMappingsSynced] = plugin.useRoleMappings();
const [groups, groupsSynced] = plugin.useGroups();
const [roleMappings, roleMappingsSynced] = useRoleMappings();
const [groups, groupsSynced] = useGroups();
const [roles, rolesSynced] = useRoles();
const [editing, setEditing] = useState<RoleMappingRecord | undefined>();
+39 -37
View File
@@ -1,5 +1,5 @@
import React, { useState, useCallback, useSyncExternalStore } from "react";
import { BaseWebPlugin, PageLayout, PageHeader, useAccount, SectionHeader } from "@clusterio/web_ui";
import { WebPluginContext, PageLayout, PageHeader, useAccount, SectionHeader } from "@clusterio/web_ui";
import { Button } from "antd";
import * as messages from "../messages.js";
@@ -65,44 +65,46 @@ function ExpGroupsPage() {
</PageLayout>;
}
export class WebPlugin extends BaseWebPlugin {
groups = new lib.MapSubscriber(messages.GroupUpdatedEvent, this.control);
assignments = new lib.MapSubscriber(messages.ManualAssignmentUpdatedEvent, this.control);
roleMappings = new lib.MapSubscriber(messages.RoleMappingUpdatedEvent, this.control);
let groups: lib.MapSubscriber<messages.GroupUpdatedEvent> | undefined;
let assignments: lib.MapSubscriber<messages.ManualAssignmentUpdatedEvent> | undefined;
let roleMappings: lib.MapSubscriber<messages.RoleMappingUpdatedEvent> | undefined;
async init() {
this.pages = [
{
path: "/permission_groups",
sidebarName: "Permission Groups",
permission: (account => account.hasAnyPermission(
"exp_groups.group.list",
"exp_groups.assignment.list",
"exp_groups.role_mapping.list",
)),
content: <ExpGroupsPage />,
},
{
path: "/permission_groups/:id/view",
sidebarPath: "/permission_groups",
permission: "exp_groups.group.get",
content: <GroupViewPage />,
},
];
}
export function useGroups() {
const subscribe = useCallback((cb: () => void) => groups!.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => groups!.getSnapshot());
}
useGroups() {
const subscribe = useCallback((cb: () => void) => this.groups.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => this.groups.getSnapshot());
}
export function useAssignments() {
const subscribe = useCallback((cb: () => void) => assignments!.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => assignments!.getSnapshot());
}
useAssignments() {
const subscribe = useCallback((cb: () => void) => this.assignments.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => this.assignments.getSnapshot());
}
export function useRoleMappings() {
const subscribe = useCallback((cb: () => void) => roleMappings!.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => roleMappings!.getSnapshot());
}
useRoleMappings() {
const subscribe = useCallback((cb: () => void) => this.roleMappings.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => this.roleMappings.getSnapshot());
}
export default function registerPlugin(context: WebPluginContext) {
context.control.hooks.pages.attach("exp_groups", () => [
{
path: "/permission_groups",
sidebarName: "Permission Groups",
permission: (account => account.hasAnyPermission(
"exp_groups.group.list",
"exp_groups.assignment.list",
"exp_groups.role_mapping.list",
)),
content: <ExpGroupsPage />,
},
{
path: "/permission_groups/:id/view",
sidebarPath: "/permission_groups",
permission: "exp_groups.group.get",
content: <GroupViewPage />,
},
]);
groups = new lib.MapSubscriber(messages.GroupUpdatedEvent, context.control);
assignments = new lib.MapSubscriber(messages.ManualAssignmentUpdatedEvent, context.control);
roleMappings = new lib.MapSubscriber(messages.RoleMappingUpdatedEvent, context.control);
}
+3 -7
View File
@@ -1,7 +1,3 @@
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
}
// Empty because an instance entrypoint must be defined for a module to be injected
// This requirement may change in the future to allow for standalone modules
export default async function () {}
+3 -7
View File
@@ -1,7 +1,3 @@
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
}
// Empty because an instance entrypoint must be defined for a module to be injected
// This requirement may change in the future to allow for standalone modules
export default async function () {}
+17 -2
View File
@@ -1,11 +1,20 @@
import { BaseControllerPlugin } from "@clusterio/controller";
import type { Controller, ControllerPluginContext } from "@clusterio/controller";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
import * as path from "node:path";
export class ControllerPlugin extends BaseControllerPlugin {
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
name: string;
reports!: lib.SubscribableDatastore<messages.ReportRecord>;
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
const databaseDirectory = this.controller.config.get("controller.database_directory");
@@ -23,6 +32,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
this.controller.handle(messages.ReportGetRequest, this.handleReportGetRequest.bind(this));
this.controller.handle(messages.ReportCreateRequest, this.handleReportCreateRequest.bind(this));
this.controller.handle(messages.ReportDeleteRequest, this.handleReportDeleteRequest.bind(this));
this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this));
}
async onShutdown() {
@@ -141,3 +152,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
}
}
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
}
+17 -2
View File
@@ -1,4 +1,5 @@
import { BaseInstancePlugin } from "@clusterio/host";
import type { Instance, InstancePluginContext } from "@clusterio/host";
import type * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
/** Sent by the lua side when a player reports another. */
@@ -28,7 +29,17 @@ export type IpcReportDelete = {
* the answer is handed back to lua once it arrives. The instance may have
* stopped by then, in which case the answer is dropped.
*/
export class InstancePlugin extends BaseInstancePlugin {
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
this.instance.server.handle("exp_reports:create", this.handleCreateIPC.bind(this));
this.instance.server.handle("exp_reports:list", this.handleListIPC.bind(this));
@@ -93,3 +104,7 @@ export class InstancePlugin extends BaseInstancePlugin {
);
}
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
}
+12 -2
View File
@@ -1,7 +1,7 @@
import t from "tap";
import * as lib from "@clusterio/lib";
import { Controller, InstanceRecord } from "@clusterio/controller";
import { ControllerPlugin } from "../dist/node/controller.js";
import entrypoint, { ControllerPlugin } from "../dist/node/controller.js";
import * as messages from "../dist/node/messages.js";
import { plugin as pluginDeclaration } from "../dist/node/index.js";
@@ -46,13 +46,23 @@ async function startPlugin(t2, { config = {} } = {}) {
broadcast(event);
};
const plugin = new ControllerPlugin({ name: "exp_reports" }, controller, undefined, logger);
const plugin = new ControllerPlugin({ plugin: { name: "exp_reports" }, controller, logger });
plugin.logger = { ...logger, warn: message => state.warnings.push(message) };
plugin.postWebhook = async (url, body) => { state.posts.push({ url, body }); };
await plugin.init();
return { plugin, controller, state };
}
t.test("entrypoint", async t2 => {
const controllerConfig = new lib.ControllerConfig("controller", { "controller.database_directory": t2.testdir() });
const controller = new Controller(logger, [], controllerConfig);
await entrypoint({ controller, logger, plugin: { name: "exp_reports" }, metrics: undefined });
for (const hook of ["shutdown"]) {
t2.ok([...controller.hooks[hook].attached].includes("exp_reports"), `attached to ${hook}`);
}
await controller.hooks.shutdown.invoke();
});
t.test("class ControllerPlugin", t2 => {
t2.test(".handleReportCreateRequest() from an instance records the instance and the reporter", async t3 => {
const { plugin } = await startPlugin(t3);
+3 -1
View File
@@ -10,6 +10,8 @@ for (const Message of pluginDeclaration.messages) {
lib.Link.register(Message);
}
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
class TestConnector extends lib.BaseConnector {
constructor() {
super(lib.Address.fromShorthand({ instanceId: 1 }), lib.Address.fromShorthand({ hostId: 1 }));
@@ -61,7 +63,7 @@ async function startPlugin(t2, { reports = [report(1, "bob", "alice"), report(2,
instance.notifyStatus("running");
state.sent.length = 0;
const plugin = new InstancePlugin({ name: "exp_reports" }, instance, {});
const plugin = new InstancePlugin({ plugin: { name: "exp_reports" }, instance, host: {}, logger });
await plugin.init();
return { plugin, instance, state };
}
+20 -2
View File
@@ -1,11 +1,14 @@
import { BaseControllerPlugin, InstanceRecord, type Controller } from "@clusterio/controller";
import type { Controller, ControllerPluginContext, InstanceRecord } from "@clusterio/controller";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
import * as path from "node:path";
const loaded = new WeakMap<Controller, ControllerPlugin>();
export class ControllerPlugin extends BaseControllerPlugin {
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
name: string;
roleMeta!: lib.SubscribableDatastore<messages.RoleMetaRecord>;
/** The plugin loaded on a controller, for the exp_scenario seed. */
@@ -13,6 +16,12 @@ export class ControllerPlugin extends BaseControllerPlugin {
return loaded.get(controller);
}
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
loaded.set(this.controller, this);
const databaseDirectory = this.controller.config.get("controller.database_directory");
@@ -45,6 +54,11 @@ export class ControllerPlugin extends BaseControllerPlugin {
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
const hooks = this.controller.hooks;
hooks.shutdown.attach(this.name, this.onShutdown.bind(this));
hooks.controllerConfigFieldChanged.attach(this.name, this.onControllerConfigFieldChanged.bind(this));
hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this));
}
async onShutdown() {
@@ -319,3 +333,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
}
}
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
}
+20 -2
View File
@@ -1,4 +1,4 @@
import { BaseInstancePlugin } from "@clusterio/host";
import type { Instance, InstancePluginContext } from "@clusterio/host";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
@@ -9,11 +9,25 @@ export type IpcAssignmentUpdate = {
unassign: number[] | undefined,
};
export class InstancePlugin extends BaseInstancePlugin {
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
this.instance.handle(messages.RoleUpdatedEvent, this.handleRoleUpdatedEvent.bind(this));
this.instance.handle(messages.AssignmentUpdatedEvent, this.handleAssignmentUpdatedEvent.bind(this));
this.instance.server.handle("exp_roles:assignment_update", this.handleAssignmentUpdateIPC.bind(this));
const hooks = this.instance.hooks;
hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this));
hooks.start.attach(this.name, this.onStart.bind(this));
}
get syncMode() {
@@ -106,3 +120,7 @@ export class InstancePlugin extends BaseInstancePlugin {
);
}
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
}
+12 -2
View File
@@ -1,7 +1,7 @@
import t from "tap";
import * as lib from "@clusterio/lib";
import { Controller } from "@clusterio/controller";
import { ControllerPlugin } from "../dist/node/controller.js";
import entrypoint, { ControllerPlugin } from "../dist/node/controller.js";
import * as messages from "../dist/node/messages.js";
// The controller validates message classes against the link registry
@@ -44,13 +44,23 @@ async function startPlugin(t2, { roles = [] } = {}) {
permissionsUpdated(user);
};
const plugin = new ControllerPlugin({ name: "exp_roles" }, controller, undefined, logger);
const plugin = new ControllerPlugin({ plugin: { name: "exp_roles" }, controller, logger });
await plugin.init();
return { plugin, controller, state };
}
const role = (id, name, permissions = []) => new lib.Role(id, name, "", new Set(permissions));
t.test("entrypoint", async t2 => {
const controllerConfig = new lib.ControllerConfig("controller", { "controller.database_directory": t2.testdir() });
const controller = new Controller(logger, [], controllerConfig);
await entrypoint({ controller, logger, plugin: { name: "exp_roles" }, metrics: undefined });
for (const hook of ["shutdown", "controllerConfigFieldChanged", "playerEvent"]) {
t2.ok([...controller.hooks[hook].attached].includes("exp_roles"), `attached to ${hook}`);
}
await controller.hooks.shutdown.invoke();
});
t.test("class ControllerPlugin", t2 => {
t2.test(".init() and .sweepRoleMeta() manage the role properties", async t3 => {
const { plugin } = await startPlugin(t3, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
+16 -2
View File
@@ -1,7 +1,7 @@
import t from "tap";
import * as lib from "@clusterio/lib";
import { Instance } from "@clusterio/host";
import { InstancePlugin } from "../dist/node/instance.js";
import entrypoint, { InstancePlugin } from "../dist/node/instance.js";
import { plugin as pluginDeclaration } from "../dist/node/index.js";
import * as messages from "../dist/node/messages.js";
@@ -69,7 +69,7 @@ async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles
return undefined;
};
const plugin = new InstancePlugin({ name: "exp_roles" }, instance, {});
const plugin = new InstancePlugin({ plugin: { name: "exp_roles" }, instance, host: {}, logger });
plugin.logger = { ...logger, warn: message => state.warnings.push(message) };
await plugin.init();
return { plugin, state };
@@ -81,6 +81,20 @@ function decodeRcon(command) {
return { receiver: match[1], payload: JSON.parse(match[2]) };
}
t.test("entrypoint", async t2 => {
const instanceConfig = new lib.InstanceConfig("host");
instanceConfig.set("instance.id", 1);
instanceConfig.set("instance.name", "test");
const instance = new Instance(
{ assignGamePort: () => 1 }, new TestConnector(), t2.testdir(), "factorioDir", instanceConfig
);
instance.server = { handle: () => {} };
await entrypoint({ instance, host: {}, logger, plugin: { name: "exp_roles" } });
for (const hook of ["instanceConfigFieldChanged", "start"]) {
t2.ok([...instance.hooks[hook].attached].includes("exp_roles"), `attached to ${hook}`);
}
});
t.test("class InstancePlugin", t2 => {
t2.test(".onStart() subscribes and initialises the lua module", async t3 => {
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
+14 -2
View File
@@ -1,5 +1,5 @@
import * as lib from "@clusterio/lib";
import { BaseControllerPlugin } from "@clusterio/controller";
import type { Controller, ControllerPluginContext } from "@clusterio/controller";
import { RoleMetaRecord } from "@expcluster/roles";
import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups";
import { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller.js";
@@ -7,7 +7,15 @@ import { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/
import * as messages from "./messages.js";
import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed.js";
export class ControllerPlugin extends BaseControllerPlugin {
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
}
async init() {
this.controller.handle(messages.SeedRequest, this.handleSeedRequest.bind(this));
}
@@ -148,6 +156,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
}
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
}
function newId(datastore: { has(id: number): boolean }) {
let id = Math.random() * 2 ** 31 | 0;
while (datastore.has(id)) {
+3 -10
View File
@@ -1,10 +1,3 @@
import * as lib from "@clusterio/lib";
import { BaseInstancePlugin } from "@clusterio/host";
export class InstancePlugin extends BaseInstancePlugin {
async init() {
}
async onStart() {
}
}
// Empty because an instance entrypoint must be defined for a module to be injected
// This requirement may change in the future to allow for standalone modules
export default async function () {}
+2 -2
View File
@@ -40,12 +40,12 @@ async function startPlugin(t2, { withPlugins = true } = {}) {
if (withPlugins) {
for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) {
const plugin = new Plugin({ name }, controller, undefined, logger);
const plugin = new Plugin({ plugin: { name }, controller, logger });
await plugin.init();
}
}
const plugin = new ControllerPlugin({ name: "exp_scenario" }, controller, undefined, logger);
const plugin = new ControllerPlugin({ plugin: { name: "exp_scenario" }, controller, logger });
await plugin.init();
return { plugin, controller };
}
+5 -7
View File
@@ -1,11 +1,9 @@
import { BaseWebPlugin } from "@clusterio/web_ui";
import { WebPluginContext } from "@clusterio/web_ui";
import Seed from "./components/Seed";
export class WebPlugin extends BaseWebPlugin {
async init() {
this.componentExtra = {
RolesPage: Seed,
};
}
export default function registerPlugin(context: WebPluginContext) {
context.control.hooks.extensionComponents.attach("exp_scenario", () => ({
RolesPage: Seed,
}));
}
+24 -3
View File
@@ -1,10 +1,27 @@
import * as lib from "@clusterio/lib";
import { BaseInstancePlugin } from "@clusterio/host";
import type { Instance, InstancePluginContext } from "@clusterio/host";
export class InstancePlugin extends BaseInstancePlugin {
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
private updateInterval?: ReturnType<typeof setInterval>;
private gameTimes: number[] = [];
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
init() {
const hooks = this.instance.hooks;
hooks.start.attach(this.name, this.onStart.bind(this));
hooks.exit.attach(this.name, this.onExit.bind(this));
hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this));
hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this));
}
async onStart() {
if (!this.instance.config.get("factorio.settings")["auto_pause"]) {
this.setInterval();
@@ -56,7 +73,7 @@ export class InstancePlugin extends BaseInstancePlugin {
}
try {
const newGameTime = await this.sendRcon(`/_rcon return exp_server_ups.refresh(${ups})`);
const newGameTime = await this.instance.sendRcon(`/_rcon return exp_server_ups.refresh(${ups})`, false, this.name);
this.gameTimes.push(Number(newGameTime));
} catch (error: any) {
this.logger.error(`Failed to receive new game time: ${error}`);
@@ -67,3 +84,7 @@ export class InstancePlugin extends BaseInstancePlugin {
}
}
}
export default async function (context: InstancePluginContext) {
new InstancePlugin(context).init();
}
+3 -7
View File
@@ -1,7 +1,3 @@
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
}
// Empty because an instance entrypoint must be defined for a module to be injected
// This requirement may change in the future to allow for standalone modules
export default async function () {}