2026-07-07 00:03:14 +07:00
|
|
|
import {
|
|
|
|
|
callBridgeMethod,
|
|
|
|
|
callBridgeMethodSync,
|
|
|
|
|
registerJSMethod,
|
|
|
|
|
} from '@revenge-mod/modules/native'
|
|
|
|
|
import { getErrorStack } from '@revenge-mod/utils/error'
|
|
|
|
|
import { pUnscopedApi } from '../apis'
|
|
|
|
|
import {
|
|
|
|
|
disablePlugin,
|
|
|
|
|
InternalPluginFlags,
|
|
|
|
|
isPluginEnabled,
|
2026-07-07 23:12:40 +07:00
|
|
|
isPluginPendingReload,
|
2026-07-07 00:03:14 +07:00
|
|
|
PluginFlags,
|
|
|
|
|
pEmitter,
|
|
|
|
|
pList,
|
|
|
|
|
registerPlugin,
|
|
|
|
|
runPluginLate,
|
2026-07-07 23:12:40 +07:00
|
|
|
stopPlugin,
|
2026-07-07 00:03:14 +07:00
|
|
|
} from '.'
|
|
|
|
|
import type { PluginManifest, PluginOptionsFactory } from '../types'
|
|
|
|
|
import type { AnyPlugin } from '.'
|
|
|
|
|
|
|
|
|
|
interface ExternalPlugin {
|
|
|
|
|
manifest: PluginManifest
|
|
|
|
|
script?: string
|
|
|
|
|
internal?: boolean
|
|
|
|
|
essential?: boolean
|
|
|
|
|
enabledByDefault?: boolean
|
2026-07-07 23:12:40 +07:00
|
|
|
/** Errors the native side already hit (eg. at boot before JS was up, after faulty update). */
|
|
|
|
|
errors?: string[]
|
2026-07-07 00:03:14 +07:00
|
|
|
}
|
|
|
|
|
|
2026-07-07 23:12:40 +07:00
|
|
|
type PluginInstallResult =
|
|
|
|
|
| { error: false; plugin: ExternalPlugin }
|
|
|
|
|
| { error: string }
|
|
|
|
|
|
2026-07-07 00:03:14 +07:00
|
|
|
export function registerExternalPlugins() {
|
2026-07-07 23:12:40 +07:00
|
|
|
registerJSMethod(
|
|
|
|
|
'revenge.plugins.loader.stopPlugin',
|
|
|
|
|
async function stopHandler(id: string) {
|
|
|
|
|
const plugin = pList.get(id)
|
|
|
|
|
if (plugin?.status) await stopPlugin(plugin)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-07 00:03:14 +07:00
|
|
|
registerJSMethod(
|
|
|
|
|
'revenge.plugins.loader.pluginInstalled',
|
2026-07-07 23:12:40 +07:00
|
|
|
async function installedHandler(result: PluginInstallResult) {
|
|
|
|
|
if (result.error !== false) {
|
|
|
|
|
pEmitter.emit('install', { error: result.error })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { plugin } = result
|
|
|
|
|
const existing = pList.get(plugin.manifest.id)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (existing?.status) {
|
|
|
|
|
// Should already be stopped by this point, but just in case the native side didn't await
|
|
|
|
|
await stopPlugin(existing)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pendingReload =
|
|
|
|
|
existing !== undefined && isPluginPendingReload(existing)
|
|
|
|
|
|
|
|
|
|
if (existing) pList.delete(plugin.manifest.id)
|
|
|
|
|
|
|
|
|
|
if (pendingReload) {
|
|
|
|
|
// Since reload is required to (un)apply changes, we don't load the new plugin yet
|
|
|
|
|
// We'll do the next time the app is started
|
|
|
|
|
const registered = pList.get(
|
|
|
|
|
registerExternalPlugin(plugin).id,
|
|
|
|
|
)!
|
|
|
|
|
|
|
|
|
|
registered.flags |= PluginFlags.PendingUpdate
|
|
|
|
|
} else await loadExternalPlugin(plugin)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
pEmitter.emit('install', { error: getErrorStack(e) })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pEmitter.emit('install', {
|
|
|
|
|
error: false,
|
|
|
|
|
manifest: plugin.manifest,
|
|
|
|
|
updated: existing !== undefined,
|
|
|
|
|
})
|
2026-07-07 00:03:14 +07:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const externals = callBridgeMethodSync(
|
|
|
|
|
'revenge.plugins.loader.getPlugins',
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
if (!externals) return
|
|
|
|
|
|
|
|
|
|
for (const external of externals)
|
|
|
|
|
try {
|
|
|
|
|
// Skip plugins whose JS counterpart is already registered
|
|
|
|
|
if (pList.has(external.manifest.id)) continue
|
|
|
|
|
registerExternalPlugin(external)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
nativeLoggingHook(
|
|
|
|
|
`\u001b[31mFailed to register external plugin "${external.manifest.id}": ${getErrorStack(e)}\u001b[0m`,
|
|
|
|
|
2,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 23:12:40 +07:00
|
|
|
export function registerExternalPlugin(external: ExternalPlugin) {
|
|
|
|
|
const { manifest, script, internal, essential, enabledByDefault, errors } =
|
|
|
|
|
external
|
|
|
|
|
|
|
|
|
|
const dep = registerPlugin(
|
2026-07-07 00:03:14 +07:00
|
|
|
manifest,
|
|
|
|
|
createOptionsFactory(script),
|
|
|
|
|
essential || enabledByDefault ? PluginFlags.Enabled : 0,
|
|
|
|
|
(internal ? InternalPluginFlags.Internal : 0) |
|
|
|
|
|
(essential ? InternalPluginFlags.Essential : 0),
|
|
|
|
|
)
|
2026-07-07 23:12:40 +07:00
|
|
|
|
|
|
|
|
// Sync errors the native side already caught
|
|
|
|
|
if (errors?.length) {
|
|
|
|
|
const plugin = pList.get(dep.id)!
|
|
|
|
|
plugin.errors.push(...errors)
|
|
|
|
|
plugin.flags |= PluginFlags.Errored
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dep
|
2026-07-07 00:03:14 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function loadExternalPlugin(external: ExternalPlugin) {
|
|
|
|
|
const { id } = registerExternalPlugin(external)
|
|
|
|
|
const plugin = pList.get(id)
|
|
|
|
|
|
|
|
|
|
if (plugin && isPluginEnabled(plugin)) await runPluginLate(plugin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uninstallExternalPlugin(plugin: AnyPlugin) {
|
|
|
|
|
if (isPluginEnabled(plugin)) await disablePlugin(plugin)
|
|
|
|
|
|
2026-07-07 23:12:40 +07:00
|
|
|
await callBridgeMethod('revenge.plugins.loader.uninstallPlugin', [
|
2026-07-07 00:03:14 +07:00
|
|
|
plugin.manifest.id,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
pList.delete(plugin.manifest.id)
|
|
|
|
|
pEmitter.emit('unregister', plugin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createOptionsFactory(script?: string): PluginOptionsFactory {
|
|
|
|
|
if (!script) return () => ({})
|
|
|
|
|
return () => new Function('revenge', `return ${script}`)(pUnscopedApi)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
declare module '@revenge-mod/modules/native' {
|
|
|
|
|
interface Methods {
|
|
|
|
|
'revenge.plugins.loader.getPlugins': [[], ExternalPlugin[] | null]
|
|
|
|
|
'revenge.plugins.loader.installPlugin': [[], null]
|
|
|
|
|
'revenge.plugins.loader.uninstallPlugin': [[string], null]
|
|
|
|
|
}
|
|
|
|
|
}
|