Merge pull request #483 from bbassie/refactor/init-function-exports

Export init functions instead of plugin classes
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-09-27 01:53:11 +01:00
commit 46bb15e4a6
28 files changed
+268 -134

No files matched your search

-2
View File
@@ -47,6 +47,4 @@ export const plugin: lib.PluginDeclaration = {
initialValue: false,
},
},
webEntrypoint: "./web",
};
-1
View File
@@ -18,7 +18,6 @@ module.exports = (env = {}) => merge(common(env), {
exposes: {
"./": "./index.ts",
"./package.json": "./package.json",
"./web": "./web/index.tsx",
},
shared: {
"@clusterio/lib": { import: false },
+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 () {}