Files
factorio-expcluster/exp_roles/instance.ts
T
BastiaanandClaude Opus 5.5 ea9bf1fb78 Construct plugins with a static fromContext (#487)
Each plugin class now has a private constructor and a static async
fromContext(context) that loads its datastores, constructs the instance
and attaches its handlers and hooks. This replaces the public constructor
followed by init(), and AutoRestart's start().

Datastores are loaded before construction and passed to the constructor,
so the definite assignment assertions on them are gone. Tests build the
plugins through fromContext and pass a wrapped logger in the context
instead of overwriting plugin.logger before init().

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:19:53 +01:00

127 lines
4.0 KiB
TypeScript

import type { Instance, InstancePluginContext } from "@clusterio/host";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
/** Sent by the lua side when roles are changed in game. */
export type IpcAssignmentUpdate = {
name: string,
assign: number[] | undefined,
unassign: number[] | undefined,
};
export class InstancePlugin {
private constructor(
public instance: Instance,
public logger: lib.Logger,
public name: string,
) {}
static async fromContext(context: InstancePluginContext) {
const instance = context.instance;
const plugin = new InstancePlugin(instance, context.logger, context.plugin.name);
instance.handle(messages.RoleUpdatedEvent, plugin.handleRoleUpdatedEvent.bind(plugin));
instance.handle(messages.AssignmentUpdatedEvent, plugin.handleAssignmentUpdatedEvent.bind(plugin));
instance.server.handle("exp_roles:assignment_update", plugin.handleAssignmentUpdateIPC.bind(plugin));
const hooks = instance.hooks;
hooks.instanceConfigFieldChanged.attach(plugin.name, plugin.onInstanceConfigFieldChanged.bind(plugin));
hooks.start.attach(plugin.name, plugin.onStart.bind(plugin));
return plugin;
}
get syncMode() {
return this.instance.config.get("exp_roles.sync_mode");
}
async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
switch (field) {
case "exp_roles.sync_mode":
await this.luaSetEmitEvents(curr === "bidirectional");
break;
}
}
async onStart() {
if (this.syncMode === "disabled") {
return;
}
// Date.now() is used because the lua state is initialised from the full
// list below, so only updates made after this point are of interest
const subscribedAtMs = Date.now();
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_roles:${messages.RoleUpdatedEvent.name}`, true, subscribedAtMs
));
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_roles:${messages.AssignmentUpdatedEvent.name}`, true, subscribedAtMs
));
const [roles, assignments] = await Promise.all([
this.instance.sendTo("controller", new messages.RoleListRequest()),
this.instance.sendTo("controller", new messages.AssignmentListRequest()),
]);
if (!roles.some(role => role.isDefault)) {
this.logger.warn("No default role is set on the controller, players will hold no roles in game");
}
await this.luaSend("initialise", {
...messages.encodeRolesForLua(roles),
assignments: assignments.map(assignment => assignment.toJSON()),
});
await this.luaSetEmitEvents(this.syncMode === "bidirectional");
}
async handleRoleUpdatedEvent(event: messages.RoleUpdatedEvent) {
if (this.syncMode === "disabled") {
return;
}
await this.luaSend("receive_role_updates", event.updates.map(role => role.toJSON()));
}
async handleAssignmentUpdatedEvent(event: messages.AssignmentUpdatedEvent) {
if (this.syncMode === "disabled") {
return;
}
await this.luaSend("receive_assignment_updates", event.updates.map(a => a.toJSON()));
}
async handleAssignmentUpdateIPC(event: IpcAssignmentUpdate) {
if (this.syncMode !== "bidirectional") {
return;
}
const assign = event.assign ?? [];
try {
await this.instance.sendTo("controller", new messages.AssignmentUpdateRequest(
event.name, assign, event.unassign ?? [],
));
} catch (err: any) {
// The roles were already applied in game, so they have to be taken
// back off again now that the controller has refused them
this.logger.warn(`Role change for ${event.name} was rejected: ${err.message}`);
if (assign.length) {
await this.luaSend("reject_assignment", { name: event.name, role_ids: assign });
}
return;
}
}
async luaSetEmitEvents(emitEvents: boolean) {
await this.luaSend("set_emit_events", emitEvents);
}
async luaSend(receiver: string, json: any) {
await this.instance.sendRcon(
`/sc exp_roles.${receiver}(helpers.json_to_table[=[${JSON.stringify(json)}]=])`, true
);
}
}
export default async function (context: InstancePluginContext) {
await InstancePlugin.fromContext(context);
}