import type { CurrentStateAPI, fflagList, fflagValue, SoberConfig } from "./types"; import { currentStateManager } from "./CurrentState"; import { eventCollector, type EventType, type EventCallback, type EventSubscription } from "./EventCollector"; type PluginMeta = { name: string; id: string; forceEnable?: boolean; /** the lower, the earlier configs are applied. higher means more priority over end value */ configPrio?: number; description?: string; }; export class Plugin { public name: string; public id: string; public forceEnable: boolean; public configPrio: number; public description: string; private runtimeSetFFlags: fflagList = {}; private runtimeSetSoberConfig: SoberConfig = {}; constructor({ name, id, forceEnable, configPrio, description }: PluginMeta) { this.description = description || `The "${name}" plugin`; this.name = name; this.id = id; this.forceEnable = forceEnable ?? false; this.configPrio = configPrio ?? 0; this.runtimeSetFFlags = {}; } get fflags(): fflagList { return this.runtimeSetFFlags; } get soberConfig(): SoberConfig { return this.runtimeSetSoberConfig; } public setFFlag(flag: string, value: fflagValue) { this.runtimeSetFFlags[flag] = value; } public setSoberConfigOption( opt: K, value: NonNullable ) { this.runtimeSetSoberConfig[opt] = value; } /** * Subscribe to events using the new event system */ public on( eventType: T, callback: EventCallback ): EventSubscription { return eventCollector.on(eventType, callback); } /** * Legacy method for backward compatibility - maps to new event system */ public hookLog = { GAME_JOIN: (callback: EventCallback<"GAME_JOIN">) => this.on("GAME_JOIN", callback), GAME_LEAVE: (callback: EventCallback<"GAME_LEAVE">) => this.on("GAME_LEAVE", callback), JOIN_LEAVE: ( callback: EventCallback<"PLAYER_JOIN" | "PLAYER_LEAVE"> ) => { // Subscribe to both player join and leave events const joinSub = this.on( "PLAYER_JOIN", callback as EventCallback<"PLAYER_JOIN"> ); const leaveSub = this.on( "PLAYER_LEAVE", callback as EventCallback<"PLAYER_LEAVE"> ); return { unsubscribe: () => { joinSub.unsubscribe(); leaveSub.unsubscribe(); } }; }, BLOXSTRAP: (callback: EventCallback<"BLOXSTRAP_RPC">) => this.on("BLOXSTRAP_RPC", callback) }; get currentState(): CurrentStateAPI { return { getCurrentState: () => currentStateManager.getCurrentState(), onStateChange: (callback) => currentStateManager.onStateChange(callback), isInGame: () => currentStateManager.isInGame(), getPlaceId: () => currentStateManager.getPlaceId(), getJobId: () => currentStateManager.getJobId() }; } } let pluginsRegistered: Plugin[] = []; let pluginMetadatas: PluginMeta[] = []; let pluginsRegisterFuncs: ((forceEnable: string[], forceDisable: string[]) => void)[] = []; export function registerPlugin( details: PluginMeta, initFunc: (plugin: Plugin) => void ) { pluginMetadatas.push(details); pluginsRegisterFuncs.push(async (forceEnable, forceDisable) => { if (!details.forceEnable && !forceEnable.includes(details.id)) return; if (forceDisable.includes(details.id)) return; if (!details.configPrio) details.configPrio = 0; while (true) { if ( !pluginsRegistered.find( (a) => a.configPrio === details.configPrio ) ) break; details.configPrio++; } const plugin = new Plugin(details); try { await initFunc(plugin); pluginsRegistered.push(plugin); console.log( `[api/Plugin] PluginInit(${plugin.id}): "${plugin.name}" successfully initalized` ); } catch (error) { console.error( `[api/Plugin] PluginInitError(${plugin.id}): "${plugin.name}" errored:`, error ); } }); } export async function registerPluginsAllFinal(forceEnable: string[], forceDisable: string[]) { for (const f of pluginsRegisterFuncs) { await f(forceEnable, forceDisable); } } export function getPlugins(): Plugin[] { return pluginsRegistered; } export function getPluginInfos(): PluginMeta[] { return pluginMetadatas; }