Inter-plugin interfaces
Plugins talk through typed, versioned interfaces — methods as natives, events as forwards (on / producer emit). A producer publishes an implementation. A hard-dep consumer prefers producer-as-import: named exports from the producer package. on(...) is ledgered against the consumer (load-window; auto-dropped on unload), returns void, and there is no off.
// producer
import { publish } from '@s2script/sdk';
import type { PublishHandle } from '@s2script/sdk';
let zones!: PublishHandle;
export function OnPluginStart(): void {
zones = publish('@s2script/zones', {
createZone(name: string, mins: number[], maxs: number[]) { /* … */ },
getZones() { /* … */ return []; },
isInZone(slot: number, name: string): boolean { /* … */ return false; }
});
}
export function OnGameFrame(): void {
zones.emit('stay', { slot: 0, zone: 'spawn' });
} // consumer — hard dep (preferred): producer-as-import
import { on, getZones } from '@s2script/zones';
export function OnPluginStart(): void {
try {
getZones(); // probe — host proxy throws InterfaceUnavailable while unloaded
} catch {
return; // producer may load after this plugin; defer subscribing
}
on('enter', ({ slot, zone }) => { /* … */ });
on('stay', ({ slot, zone }) => { /* … */ });
} // consumer — optional dep
import { tryUse } from '@s2script/sdk';
import type { Zones } from '@s2script/zones';
export function OnPluginStart(): void {
const handle = tryUse<Zones>('@s2script/zones'); // Zones | null
if (!handle) return;
handle.on('enter', ({ slot, zone }) => { /* … */ });
} Events are not methods on the Zones object type — subscribe with on(...) (producer-as-import) or handle.on(...) (tryUse / use). use() is the explicit load-window hard-dep form; prefer the import. Cookbook keeps @s2script/zones optional (tryUse) so the demo still loads when the zones plugin is absent — that is not the preferred hard-dep shape.
A hard dep (producer-as-import / use) is a proxy that throws InterfaceUnavailable while the producer is unloaded; an optional dep (tryUse) resolves to Zones | null. Call args and event payloads cross contexts by structured copy — never a live pointer — and EntityRef round-trips as a live, liveness-gated ref.
publish / use / tryUse / producer-as-import on(...) are load-window only. Per-frame forwards go on OnGameFrame (before simulation), not hook.server.
Declare deps under s2script.pluginDependencies (or optionalPluginDependencies). Every import is ledgered, and unload walks reverse-dependency order.
See the interfaces module and the zones plugin.