diff --git a/exp_commands/instance.ts b/exp_commands/instance.ts index e8fa7eeb..95a41ff8 100644 --- a/exp_commands/instance.ts +++ b/exp_commands/instance.ts @@ -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 () {} diff --git a/exp_groups/controller.ts b/exp_groups/controller.ts index 846fc0fd..89f6bd1e 100644 --- a/exp_groups/controller.ts +++ b/exp_groups/controller.ts @@ -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(); -export class ControllerPlugin extends BaseControllerPlugin { +export class ControllerPlugin { + controller: Controller; + logger: lib.Logger; + name: string; groups!: lib.SubscribableDatastore; roleMappings!: lib.SubscribableDatastore; manualAssignments!: lib.SubscribableDatastore; @@ -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(); +} diff --git a/exp_groups/instance.ts b/exp_groups/instance.ts index 207b969c..594f025d 100644 --- a/exp_groups/instance.ts +++ b/exp_groups/instance.ts @@ -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, }; -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(); @@ -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(); + 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(); +} diff --git a/exp_groups/web/components/AssignmentForm.tsx b/exp_groups/web/components/AssignmentForm.tsx index f20b21a2..503c2c61 100644 --- a/exp_groups/web/components/AssignmentForm.tsx +++ b/exp_groups/web/components/AssignmentForm.tsx @@ -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(); diff --git a/exp_groups/web/components/AssignmentsTable.tsx b/exp_groups/web/components/AssignmentsTable.tsx index f6997811..2dad4280 100644 --- a/exp_groups/web/components/AssignmentsTable.tsx +++ b/exp_groups/web/components/AssignmentsTable.tsx @@ -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(null); - const [assignments, assignmentsSynced] = plugin.useAssignments(); - const [groups, groupsSynced] = plugin.useGroups(); + const [assignments, assignmentsSynced] = useAssignments(); + const [groups, groupsSynced] = useGroups(); const [editing, setEditing] = useState(); const [open, setOpen] = useState(false); diff --git a/exp_groups/web/components/GroupViewPage.tsx b/exp_groups/web/components/GroupViewPage.tsx index 1f5ec8ac..57b909fa 100644 --- a/exp_groups/web/components/GroupViewPage.tsx +++ b/exp_groups/web/components/GroupViewPage.tsx @@ -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(""); diff --git a/exp_groups/web/components/GroupsTable.tsx b/exp_groups/web/components/GroupsTable.tsx index d3f6c9b9..f111119a 100644 --- a/exp_groups/web/components/GroupsTable.tsx +++ b/exp_groups/web/components/GroupsTable.tsx @@ -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); diff --git a/exp_groups/web/components/RoleMappingForm.tsx b/exp_groups/web/components/RoleMappingForm.tsx index 59918b29..7ebaf8c0 100644 --- a/exp_groups/web/components/RoleMappingForm.tsx +++ b/exp_groups/web/components/RoleMappingForm.tsx @@ -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(); diff --git a/exp_groups/web/components/RoleMappingsTable.tsx b/exp_groups/web/components/RoleMappingsTable.tsx index ab948960..ecf5329d 100644 --- a/exp_groups/web/components/RoleMappingsTable.tsx +++ b/exp_groups/web/components/RoleMappingsTable.tsx @@ -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(); diff --git a/exp_groups/web/index.tsx b/exp_groups/web/index.tsx index 5a2f24cd..1069d388 100644 --- a/exp_groups/web/index.tsx +++ b/exp_groups/web/index.tsx @@ -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() { ; } -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 | undefined; +let assignments: lib.MapSubscriber | undefined; +let roleMappings: lib.MapSubscriber | 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: , - }, - { - path: "/permission_groups/:id/view", - sidebarPath: "/permission_groups", - permission: "exp_groups.group.get", - content: , - }, - ]; - } +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: , + }, + { + path: "/permission_groups/:id/view", + sidebarPath: "/permission_groups", + permission: "exp_groups.group.get", + content: , + }, + ]); + + groups = new lib.MapSubscriber(messages.GroupUpdatedEvent, context.control); + assignments = new lib.MapSubscriber(messages.ManualAssignmentUpdatedEvent, context.control); + roleMappings = new lib.MapSubscriber(messages.RoleMappingUpdatedEvent, context.control); } diff --git a/exp_gui/instance.ts b/exp_gui/instance.ts index e8fa7eeb..95a41ff8 100644 --- a/exp_gui/instance.ts +++ b/exp_gui/instance.ts @@ -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 () {} diff --git a/exp_legacy/instance.ts b/exp_legacy/instance.ts index e8fa7eeb..95a41ff8 100644 --- a/exp_legacy/instance.ts +++ b/exp_legacy/instance.ts @@ -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 () {} diff --git a/exp_reports/controller.ts b/exp_reports/controller.ts index 21ec80c6..53c5adb3 100644 --- a/exp_reports/controller.ts +++ b/exp_reports/controller.ts @@ -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; + 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(); +} diff --git a/exp_reports/instance.ts b/exp_reports/instance.ts index 61c4c38c..e5ae3a43 100644 --- a/exp_reports/instance.ts +++ b/exp_reports/instance.ts @@ -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(); +} diff --git a/exp_reports/test/controller.test.js b/exp_reports/test/controller.test.js index 8097ee5b..b2dec669 100644 --- a/exp_reports/test/controller.test.js +++ b/exp_reports/test/controller.test.js @@ -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); diff --git a/exp_reports/test/instance.test.js b/exp_reports/test/instance.test.js index 6fdedcc8..e579dcec 100644 --- a/exp_reports/test/instance.test.js +++ b/exp_reports/test/instance.test.js @@ -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 }; } diff --git a/exp_roles/controller.ts b/exp_roles/controller.ts index 6d5b46f5..2299de23 100644 --- a/exp_roles/controller.ts +++ b/exp_roles/controller.ts @@ -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(); -export class ControllerPlugin extends BaseControllerPlugin { +export class ControllerPlugin { + controller: Controller; + logger: lib.Logger; + name: string; roleMeta!: lib.SubscribableDatastore; /** 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(); +} diff --git a/exp_roles/instance.ts b/exp_roles/instance.ts index d57a5819..488613d6 100644 --- a/exp_roles/instance.ts +++ b/exp_roles/instance.ts @@ -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(); +} diff --git a/exp_roles/test/controller.test.js b/exp_roles/test/controller.test.js index 0549c733..1231d093 100644 --- a/exp_roles/test/controller.test.js +++ b/exp_roles/test/controller.test.js @@ -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")] }); diff --git a/exp_roles/test/instance.test.js b/exp_roles/test/instance.test.js index e0651358..302e8528 100644 --- a/exp_roles/test/instance.test.js +++ b/exp_roles/test/instance.test.js @@ -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" }); diff --git a/exp_scenario/controller.ts b/exp_scenario/controller.ts index cd19bd96..2914f98c 100644 --- a/exp_scenario/controller.ts +++ b/exp_scenario/controller.ts @@ -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)) { diff --git a/exp_scenario/instance.ts b/exp_scenario/instance.ts index a1f3d1e4..95a41ff8 100644 --- a/exp_scenario/instance.ts +++ b/exp_scenario/instance.ts @@ -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 () {} diff --git a/exp_scenario/test/controller.test.js b/exp_scenario/test/controller.test.js index 097365ab..befc28df 100644 --- a/exp_scenario/test/controller.test.js +++ b/exp_scenario/test/controller.test.js @@ -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 }; } diff --git a/exp_scenario/web/index.tsx b/exp_scenario/web/index.tsx index cecbd7f0..60e26971 100644 --- a/exp_scenario/web/index.tsx +++ b/exp_scenario/web/index.tsx @@ -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, + })); } diff --git a/exp_server_ups/instance.ts b/exp_server_ups/instance.ts index 1783acc3..4ae128e6 100644 --- a/exp_server_ups/instance.ts +++ b/exp_server_ups/instance.ts @@ -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; 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(); +} diff --git a/exp_util/instance.ts b/exp_util/instance.ts index e8fa7eeb..95a41ff8 100644 --- a/exp_util/instance.ts +++ b/exp_util/instance.ts @@ -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 () {}