2025-05-08 20:25:11 +07:00
|
|
|
import { Logger } from '@revenge-mod/discord/common'
|
|
|
|
|
|
2025-04-29 21:38:14 +07:00
|
|
|
import * as ModulesLibrary from '@revenge-mod/modules'
|
|
|
|
|
import * as PatcherLibrary from '@revenge-mod/patcher'
|
|
|
|
|
import * as PluginsLibrary from '@revenge-mod/plugins'
|
|
|
|
|
import * as ReactLibrary from '@revenge-mod/react'
|
2025-05-06 22:08:35 +07:00
|
|
|
import * as UtilsLibrary from '@revenge-mod/utils'
|
2025-04-29 21:38:14 +07:00
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
import type {
|
|
|
|
|
InitPluginApi,
|
|
|
|
|
Plugin,
|
|
|
|
|
PluginApi,
|
|
|
|
|
PluginCleanup,
|
|
|
|
|
PluginFlags,
|
|
|
|
|
PluginLifecycles,
|
|
|
|
|
PluginManifest,
|
|
|
|
|
UnscopedInitPluginApi,
|
|
|
|
|
UnscopedPluginApi,
|
|
|
|
|
} from '../types'
|
|
|
|
|
|
|
|
|
|
const { PluginFlags: Flag, PluginStatus: Status } = PluginsLibrary
|
|
|
|
|
|
|
|
|
|
export const _uapi: UnscopedInitPluginApi | UnscopedPluginApi = {
|
2025-05-08 20:25:11 +07:00
|
|
|
get discord() {
|
|
|
|
|
return (this.discord = require('@revenge-mod/discord'))
|
|
|
|
|
},
|
|
|
|
|
get externals() {
|
|
|
|
|
return (this.externals = require('@revenge-mod/externals'))
|
|
|
|
|
},
|
2025-04-29 21:38:14 +07:00
|
|
|
modules: ModulesLibrary,
|
2025-05-06 22:08:35 +07:00
|
|
|
patcher: PatcherLibrary,
|
2025-04-29 21:38:14 +07:00
|
|
|
plugins: PluginsLibrary,
|
|
|
|
|
react: ReactLibrary,
|
2025-05-06 22:08:35 +07:00
|
|
|
utils: UtilsLibrary,
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
const _plugins = new Map<PluginManifest['id'], InternalPlugin>()
|
|
|
|
|
const _pluginIFlags = new Map<PluginManifest['id'], InternalPluginFlags>()
|
2025-04-29 21:38:14 +07:00
|
|
|
|
|
|
|
|
export function registerPlugin(
|
2025-05-08 18:52:57 +07:00
|
|
|
manifest: PluginManifest,
|
|
|
|
|
lifecycles: PluginLifecycles,
|
|
|
|
|
flags: PluginFlags,
|
2025-05-01 01:18:36 +07:00
|
|
|
iFlags: InternalPluginFlags,
|
2025-04-29 21:38:14 +07:00
|
|
|
) {
|
|
|
|
|
// TODO(plugins): verify plugin manifest
|
|
|
|
|
if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`)
|
|
|
|
|
|
2025-05-01 01:18:36 +07:00
|
|
|
_pluginIFlags.set(manifest.id, iFlags)
|
|
|
|
|
|
2025-04-29 21:38:14 +07:00
|
|
|
const handleError = (e: unknown) => {
|
|
|
|
|
plugin.errors.push(e)
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.flags |= Flag.Errored
|
|
|
|
|
if (plugin.flags & Flag.Enabled) return plugin.disable()
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const plugin: InternalPlugin = {
|
2025-05-01 01:18:36 +07:00
|
|
|
promises: [],
|
2025-04-29 21:38:14 +07:00
|
|
|
cleanups: [],
|
|
|
|
|
errors: [],
|
|
|
|
|
manifest,
|
|
|
|
|
lifecycles,
|
2025-05-01 01:18:36 +07:00
|
|
|
status: 0,
|
2025-04-29 21:38:14 +07:00
|
|
|
flags,
|
|
|
|
|
prepareStart: () => {
|
2025-05-08 18:52:57 +07:00
|
|
|
// const sapi = api as PluginApi
|
2025-04-29 21:38:14 +07:00
|
|
|
// sapi.settings ??= ...
|
|
|
|
|
},
|
2025-04-30 18:53:00 +07:00
|
|
|
async init() {
|
2025-05-08 18:52:57 +07:00
|
|
|
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
|
|
|
|
if (plugin.status & (Status.Initing | Status.Inited))
|
2025-05-01 01:18:36 +07:00
|
|
|
throw new Error(`Plugin "${manifest.id}" is initializing or already initialized`)
|
|
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.status |= Status.Initing
|
2025-04-29 21:38:14 +07:00
|
|
|
|
|
|
|
|
try {
|
2025-05-01 01:18:36 +07:00
|
|
|
const prom = lifecycles.init?.(api)
|
|
|
|
|
plugin.promises.push(prom)
|
|
|
|
|
await prom
|
|
|
|
|
|
|
|
|
|
// plugin.disable() already handles, so it's the try block
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.status |= Status.Inited
|
|
|
|
|
plugin.status &= ~Status.Initing
|
2025-04-29 21:38:14 +07:00
|
|
|
} catch (e) {
|
2025-05-01 01:18:36 +07:00
|
|
|
await handleError(e)
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 18:53:00 +07:00
|
|
|
async start() {
|
2025-05-08 18:52:57 +07:00
|
|
|
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
|
|
|
|
if (plugin.status & (Status.Starting | Status.Started))
|
2025-05-01 01:18:36 +07:00
|
|
|
throw new Error(`Plugin "${manifest.id}" is starting or already started`)
|
|
|
|
|
|
|
|
|
|
// Clear errors from previous runs
|
|
|
|
|
plugin.errors = []
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.status &= ~Flag.Errored
|
2025-05-01 01:18:36 +07:00
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.status |= Status.Starting
|
2025-04-29 21:38:14 +07:00
|
|
|
|
|
|
|
|
plugin.prepareStart()
|
|
|
|
|
|
|
|
|
|
try {
|
2025-05-08 18:52:57 +07:00
|
|
|
const prom = lifecycles.start?.(api as PluginApi)
|
2025-05-01 01:18:36 +07:00
|
|
|
plugin.promises.push(prom)
|
|
|
|
|
await prom
|
|
|
|
|
|
|
|
|
|
// plugin.disable() already handles, so it's in the try block
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.status |= Status.Started
|
|
|
|
|
plugin.status &= ~Status.Starting
|
2025-04-29 21:38:14 +07:00
|
|
|
} catch (e) {
|
2025-05-01 01:18:36 +07:00
|
|
|
await handleError(e)
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 18:53:00 +07:00
|
|
|
async stop() {
|
2025-05-08 18:52:57 +07:00
|
|
|
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
2025-05-01 01:18:36 +07:00
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
if (plugin.status & Status.Stopping) throw new Error(`Plugin "${manifest.id}" is stopping`)
|
2025-05-01 01:18:36 +07:00
|
|
|
|
|
|
|
|
// If the plugin is initializing or starting, we need to wait for it to finish, then we'll stop it
|
2025-05-08 18:52:57 +07:00
|
|
|
if (plugin.status & (Status.Initing | Status.Starting)) await Promise.all(plugin.promises)
|
2025-05-01 01:18:36 +07:00
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
if (plugin.status & (Status.Inited | Status.Started))
|
2025-05-01 01:18:36 +07:00
|
|
|
throw new Error(`Plugin "${manifest.id}" is not initialized started`)
|
2025-04-29 21:38:14 +07:00
|
|
|
|
|
|
|
|
// In case the plugin only has init() and stop()
|
|
|
|
|
plugin.prepareStart()
|
|
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.status |= Status.Stopping
|
2025-05-01 01:18:36 +07:00
|
|
|
|
2025-04-29 21:38:14 +07:00
|
|
|
try {
|
2025-05-08 18:52:57 +07:00
|
|
|
await lifecycles.stop?.(api as PluginApi)
|
2025-04-29 21:38:14 +07:00
|
|
|
} catch (e) {
|
2025-05-01 01:18:36 +07:00
|
|
|
await handleError(e)
|
2025-04-29 21:38:14 +07:00
|
|
|
} finally {
|
|
|
|
|
// Run cleanups
|
|
|
|
|
const results = await Promise.allSettled(plugin.cleanups.map(cleanup => cleanup()))
|
2025-05-01 01:18:36 +07:00
|
|
|
for (const result of results)
|
|
|
|
|
if (result.status === 'rejected') {
|
|
|
|
|
await handleError(result.reason)
|
|
|
|
|
// Some cleanup was unsuccessful, so we need to reload the app
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.flags |= Flag.ReloadRequired
|
2025-05-01 01:18:36 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear unnecessary data
|
|
|
|
|
plugin.promises = []
|
|
|
|
|
plugin.cleanups = []
|
|
|
|
|
plugin.status = 0
|
|
|
|
|
|
|
|
|
|
// Call garbage collector to free up memory
|
|
|
|
|
gc()
|
|
|
|
|
gc()
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 18:53:00 +07:00
|
|
|
async enable() {
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.flags |= Flag.Enabled
|
2025-04-29 21:38:14 +07:00
|
|
|
},
|
2025-04-30 18:53:00 +07:00
|
|
|
async disable() {
|
2025-05-08 18:52:57 +07:00
|
|
|
plugin.flags &= ~Flag.Enabled
|
2025-05-01 01:18:36 +07:00
|
|
|
|
|
|
|
|
// If plugin is not stopped, and is also not stopping, we need to stop it
|
2025-05-08 18:52:57 +07:00
|
|
|
if (plugin.status && !(plugin.status & Status.Stopping)) await plugin.stop()
|
2025-04-29 21:38:14 +07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 20:25:11 +07:00
|
|
|
let logger: InstanceType<typeof Logger>
|
2025-05-08 18:52:57 +07:00
|
|
|
|
|
|
|
|
const api: InitPluginApi | PluginApi = {
|
2025-04-29 21:38:14 +07:00
|
|
|
cleanup: (...items) => {
|
|
|
|
|
plugin.cleanups.push(...items)
|
|
|
|
|
},
|
2025-05-08 18:52:57 +07:00
|
|
|
get logger() {
|
2025-05-08 20:25:11 +07:00
|
|
|
return (logger ??= new Logger(`Revenge > ${manifest.name}`))
|
2025-05-08 18:52:57 +07:00
|
|
|
},
|
2025-04-29 21:38:14 +07:00
|
|
|
plugin,
|
|
|
|
|
unscoped: _uapi,
|
|
|
|
|
}!
|
|
|
|
|
|
|
|
|
|
_plugins.set(manifest.id, plugin)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-04 16:48:18 +07:00
|
|
|
export function getEnabledPluginsCount() {
|
|
|
|
|
let count = 0
|
2025-05-08 18:52:57 +07:00
|
|
|
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) count++
|
2025-05-04 16:48:18 +07:00
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-30 18:53:41 +07:00
|
|
|
export async function initPlugins() {
|
2025-04-29 21:38:14 +07:00
|
|
|
const promises: Promise<void>[] = []
|
2025-05-08 18:52:57 +07:00
|
|
|
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(plugin.init())
|
2025-04-29 21:38:14 +07:00
|
|
|
// These Promises never throw, so it is fine
|
2025-04-30 18:53:41 +07:00
|
|
|
await Promise.all(promises)
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
|
2025-04-30 18:53:41 +07:00
|
|
|
export async function startPlugins() {
|
2025-04-29 21:38:14 +07:00
|
|
|
const promises: Promise<void>[] = []
|
2025-05-08 18:52:57 +07:00
|
|
|
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(plugin.start())
|
2025-04-29 21:38:14 +07:00
|
|
|
// These Promises never throw, so it is fine
|
2025-04-30 18:53:41 +07:00
|
|
|
await Promise.all(promises)
|
2025-04-29 21:38:14 +07:00
|
|
|
}
|
|
|
|
|
|
2025-05-08 18:52:57 +07:00
|
|
|
export interface InternalPlugin extends Plugin {
|
2025-05-01 01:18:36 +07:00
|
|
|
promises: Promise<void>[]
|
2025-05-08 18:52:57 +07:00
|
|
|
cleanups: PluginCleanup[]
|
2025-04-29 21:38:14 +07:00
|
|
|
prepareStart(): void
|
|
|
|
|
init(): Promise<void>
|
|
|
|
|
start(): Promise<void>
|
|
|
|
|
stop(): Promise<void>
|
|
|
|
|
}
|
2025-05-01 01:18:36 +07:00
|
|
|
|
|
|
|
|
export const InternalPluginFlags = {
|
|
|
|
|
/**
|
|
|
|
|
* Marks the plugin as internal.
|
|
|
|
|
*/
|
|
|
|
|
Internal: 1 << 0,
|
|
|
|
|
/**
|
|
|
|
|
* Marks the plugin as essential. This means it should not be removed, disabled, or stopped by normal means.
|
|
|
|
|
*/
|
|
|
|
|
Essential: 1 << 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type InternalPluginFlags = (typeof InternalPluginFlags)[keyof typeof InternalPluginFlags]
|