Files
factorio-clusterio/packages/lib/src/plugin.ts
T
bbassieandClaude Fable 5.1 788ae17657 Add UDP communication support for instances
Add opt-in UDP messaging between the host and the Factorio server using
the --enable-lua-udp option of Factorio 2.0.59+.  When
factorio.enable_lua_udp is set the instance binds a localhost UDP socket,
starts Factorio with --enable-lua-udp on factorio.lua_udp_port (random
if null, like the RCON port) and tells the clusterio module the host port
via clusterio_private.update_instance.

Data from Factorio is sent with clusterio_api.send_udp(channel, data) in
the same IPC format as send_json, so plugins receive it through the
existing ipc-<channel> events.  Packets not sent from the Lua UDP port
are ignored, as that is the only unauthenticated way into the IPC
handler.  Data to Factorio is sent with the new sendUdp method on
instances and instance plugins, and is dispatched in-game as
on_udp_packet_received events by polling helpers.recv_udp every tick
from the clusterio module.

Plugins requiring UDP declare the new LuaUdp feature flag, which like
SavePatching and ScriptCommands refuses to load the plugin on instances
without the option enabled.

Verified against a headless 2.1.17 server: send_udp and recv_udp are
silent no-ops when the option is off, so a stale port in a save that is
moved to an instance without UDP does no harm.  The integration test
matrix now starts the instance with UDP enabled on Factorio 2.0.59+ and
round trips a player event over it.

Closes #969

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:38:28 +00:00

146 lines
4.0 KiB
TypeScript

/**
* Plugin interfaces and utilities.
* @module lib/plugin
*/
import type { Static } from "@sinclair/typebox";
import type { Logger } from "./logging.js";
import type { ConfigSchema, FieldDefinition } from "./config/index.js";
import type { PermissionDefinition } from "./permissions.js";
import type { PlayerStats, Role } from "./data/index.js";
import type { SyncHook } from "./SyncHook.js";
export const PluginFeatureFlags = [
/** The plugin requires module code to be patched into the save */
"SavePatching",
/** The plugin requires access to script commands over rcon */
"ScriptCommands",
/** The plugin requires UDP messaging with the Factorio server */
"LuaUdp",
] as const;
/** Used to define the plugin export in plugins */
export type PluginDeclaration = {
name: string;
title: string;
description?: string;
controllerEntrypoint?: string;
controllerConfigFields?: Record<string, FieldDefinition>;
hostEntrypoint?: string;
hostConfigFields?: Record<string, FieldDefinition>;
instanceEntrypoint?: string;
instanceConfigFields?: Record<string, FieldDefinition>;
ctlEntrypoint?: string;
webEntrypoint?: string;
ctlConfigFields?: Record<string, FieldDefinition>;
migrationEntrypoint?: string;
features?: (typeof PluginFeatureFlags)[number][];
messages?: any[];
permissions?: PermissionDefinition[];
routes?: string[];
}
/**
* Check if a plugin is expected to ship a web build.
*
* Mirrors the rule used by the create tool: a web build is generated when the
* plugin has a web or controller entrypoint or defines config fields.
*/
export function pluginNeedsWebBuild(info: PluginDeclaration) {
return Boolean(
info.webEntrypoint
|| info.controllerEntrypoint
|| info.controllerConfigFields
|| info.hostConfigFields
|| info.instanceConfigFields
|| info.ctlConfigFields
);
}
export type PluginNodeEnvInfo = PluginDeclaration & {
/**
* Path to the folder with the static files that should be hosted on the web
* server in order for the web interface to be able to load the plugin.
*/
webStaticPath: string;
/**
* Absolute path to the package.json file for the plugin.
*/
packagePath: string;
requirePath: string;
version: string;
manifest: any;
/**
* NPM package this plugin is published as. Not present if the package
* is private or the path used to load it does not match the name of the
* package.
*/
npmPackage?: string;
};
export type PluginWebpackEnvInfo = PluginDeclaration & {
container?: any;
package?: any;
enabled?: boolean;
error?: string;
};
/**
* Information about the event.
*/
export interface PlayerEvent {
type: "join" | "leave" | "import" | "promote" | "demote" | "ban" | "unban" | "whitelisted" | "unwhitelisted";
/** Name of the player that caused the event */
name: string,
/**
* Only present for type "leave" and "ban". Reason for player leaving the
* game, one of the possible reasons in defines.disconnect_reason
* or "server_quit" if the server exits while the player is online.
* When type is "ban" this is the reason given for the ban.
*/
reason?: string;
/**
* Statistics recorded for this player on the instance this event
* originated from.
*/
stats: PlayerStats,
}
export type PluginLoadContext<
Context extends object,
Info extends PluginDeclaration = PluginNodeEnvInfo
> = Context & {
logger: Logger;
plugin: Info;
};
/** Hooks for migrating data that Clusterio stores on behalf of plugins */
export type MigrationHooks = {
controllerConfig: SyncHook<ConfigSchema>;
hostConfig: SyncHook<ConfigSchema>;
instanceConfig: SyncHook<ConfigSchema>;
role: SyncHook<Static<typeof Role.jsonSchema>>;
};
export type MigrationContext = PluginLoadContext<{ hooks: MigrationHooks }>;
export type PluginClass<
Context extends object,
Info extends PluginDeclaration,
> = {
new (...args: any[]): any;
fromContext(context: PluginLoadContext<Context, Info>): {
init(): Promise<void>;
detachHooks(): void;
};
};
export type PluginType =
Extract<keyof PluginDeclaration, `${string}Entrypoint`> extends `${infer P}Entrypoint` ? P : never;