Fix raw hooks: plugin-side hookRawMethod with Advice instrumentation

Remove the planted isContactsTabVisible() hook target and app-side visibility
conditionals (that was cheating). Plugins now declare raw JVM targets directly
via hookRawMethod on existing methods like MainScreenKt.selectMainPage,
ContactsTabKt.ContactsTab, NavigationBarKt.NavigationBarItem, and Logger.d.

Replace ByteBuddy MethodDelegation (schema-changing) with Advice-based
ClassReloadingStrategy so hooks actually install at runtime. Fix Logger hook
signature for Kotlin object instance methods. Add chat banner z-index fix and
PluginEngine.hostRevision to recompose MainScreen after plugin load.

Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
Cursor Agent
2026-09-17 00:17:25 +00:00
Unverified
parent ab4f6fcc53
commit 1ff4503be9
21 changed files with 536 additions and 89 deletions
@@ -21,9 +21,9 @@ actual object PluginPlatform {
private val pineHooks = mutableMapOf<String, MutableList<top.canyie.pine.Pine.HookRecord>>()
actual fun installSharedHook(registration: SharedHookRegistration) {
val target = SharedMethodHookRegistry.targetFor(registration.hookId) ?: return
val target = registration.target
val clazz = Class.forName(target.className)
val paramTypes = target.paramTypeNames.map { Class.forName(it) }.toTypedArray()
val paramTypes = target.paramTypeNames.map { resolveJvmParameterType(it) }.toTypedArray()
val method = clazz.getDeclaredMethod(target.methodName, *paramTypes)
val record = Pine.hook(method, object : MethodHook() {
override fun beforeCall(callFrame: Pine.CallFrame) {
@@ -0,0 +1,15 @@
package ru.fromchat.plugins.host
internal fun resolveJvmParameterType(typeName: String): Class<*> =
when (typeName) {
"boolean" -> java.lang.Boolean.TYPE
"byte" -> java.lang.Byte.TYPE
"char" -> java.lang.Character.TYPE
"short" -> java.lang.Short.TYPE
"int" -> Integer.TYPE
"long" -> java.lang.Long.TYPE
"float" -> java.lang.Float.TYPE
"double" -> java.lang.Double.TYPE
"void" -> java.lang.Void.TYPE
else -> Class.forName(typeName)
}
@@ -15,7 +15,7 @@ object HookRegistry {
LOGGER_DEBUG to SharedHookTarget(
className = "ru.fromchat.Logger",
methodName = "d",
paramTypeNames = arrayOf("java.lang.String", "java.lang.String"),
paramTypeNames = arrayOf("java.lang.String", "java.lang.String", "java.lang.Throwable"),
),
)
}
@@ -23,6 +23,7 @@ object HookRegistry {
data class SharedHookRegistration(
val pluginId: String,
val hookId: String,
val target: SharedHookTarget,
val priority: Int,
val before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
val after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
@@ -23,6 +23,9 @@ object PluginEngine : PluginHostBridge {
private val _installed = MutableStateFlow<List<PluginManifest>>(emptyList())
val installed: StateFlow<List<PluginManifest>> = _installed.asStateFlow()
private val _hostRevision = MutableStateFlow(0)
val hostRevision: StateFlow<Int> = _hostRevision.asStateFlow()
private val enabledIds = linkedSetOf<String>()
private val pinnedIds = linkedSetOf<String>()
private val loaded = mutableMapOf<String, LoadedPlugin>()
@@ -51,6 +54,7 @@ object PluginEngine : PluginHostBridge {
} else {
loaded.keys.toList().forEach { unloadPlugin(it) }
}
bumpHostRevision()
}
fun setDeveloperMode(enabled: Boolean) {
@@ -69,6 +73,7 @@ object PluginEngine : PluginHostBridge {
}
persistEnabledState()
refreshInstalledList()
bumpHostRevision()
}
fun isPluginEnabled(pluginId: String): Boolean = pluginId in enabledIds
@@ -182,6 +187,10 @@ object PluginEngine : PluginHostBridge {
PluginHookDispatcher.registerMenuItem(pluginId, item)
}
override fun registerUiOverlay(pluginId: String, slot: String, title: String, message: String) {
ru.fromchat.plugins.host.ui.PluginOverlayStore.register(pluginId, slot, title, message)
}
override fun showBulletin(message: String) {
PluginHookDispatcher.showBulletin(message)
}
@@ -193,7 +202,37 @@ object PluginEngine : PluginHostBridge {
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
): () -> Unit {
val registration = SharedHookRegistration(pluginId, hookId, priority, before, after)
val target = SharedMethodHookRegistry.targetFor(hookId)
?: run {
log(pluginId, "Unknown shared hook id: $hookId")
return {}
}
return registerSharedHook(pluginId, hookId, target, priority, before, after)
}
override fun hookRawMethod(
pluginId: String,
className: String,
methodName: String,
paramTypeNames: Array<String>,
priority: Int,
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
): () -> Unit {
val hookId = "raw:$className#$methodName"
val target = SharedHookTarget(className, methodName, paramTypeNames)
return registerSharedHook(pluginId, hookId, target, priority, before, after)
}
private fun registerSharedHook(
pluginId: String,
hookId: String,
target: SharedHookTarget,
priority: Int,
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
): () -> Unit {
val registration = SharedHookRegistration(pluginId, hookId, target, priority, before, after)
SharedMethodHookRegistry.register(registration)
val unhook = {
SharedMethodHookRegistry.unregisterPlugin(pluginId)
@@ -215,6 +254,7 @@ object PluginEngine : PluginHostBridge {
runCatching { loadedPlugin.instance.onPluginLoad() }
.onFailure { log(manifest.id, "onPluginLoad failed: ${it.message}") }
loaded[manifest.id] = loadedPlugin
bumpHostRevision()
}
private fun unloadPlugin(pluginId: String) {
@@ -223,6 +263,11 @@ object PluginEngine : PluginHostBridge {
PluginHookDispatcher.unregisterPlugin(pluginId)
sharedUnhooks.remove(pluginId)
PluginPlatform.current?.unloadPlugin(pluginId)
bumpHostRevision()
}
private fun bumpHostRevision() {
_hostRevision.value++
}
private fun loadSettingsFile(pluginId: String): MutableMap<String, String> {
@@ -4,6 +4,7 @@ import ru.fromchat.plugins.HookResult
import ru.fromchat.plugins.HookStrategy
import ru.fromchat.plugins.MenuItemData
import ru.fromchat.plugins.SendMessageHookContext
import ru.fromchat.plugins.host.ui.PluginOverlayStore
data class RegisteredSendHook(
val pluginId: String,
@@ -27,6 +28,7 @@ object PluginHookDispatcher {
featureOverrides.values.forEach { list -> list.removeAll { it.first == pluginId } }
menuItems.removeAll { it.first == pluginId }
SharedMethodHookRegistry.unregisterPlugin(pluginId)
PluginOverlayStore.unregisterPlugin(pluginId)
}
fun registerFeatureOverride(pluginId: String, featureId: String, provider: () -> Boolean?) {
@@ -37,8 +39,17 @@ object PluginHookDispatcher {
menuItems += pluginId to item
}
fun menuItemEntriesFor(type: ru.fromchat.plugins.MenuItemType): List<Pair<String, MenuItemData>> =
menuItems.filter { it.second.menuType == type }.sortedByDescending { it.second.priority }
fun menuItemsFor(type: ru.fromchat.plugins.MenuItemType): List<MenuItemData> =
menuItems.filter { it.second.menuType == type }.sortedByDescending { it.second.priority }.map { it.second }
menuItemEntriesFor(type).map { it.second }
fun dispatchMenuItemClick(clickKey: String) {
if (clickKey.isBlank()) return
val owner = menuItems.firstOrNull { it.second.onClickKey == clickKey } ?: return
PluginEngine.loadedPlugin(owner.first)?.instance?.onMenuItemClick(clickKey)
}
fun featureOverrideValue(id: String): Boolean? {
featureOverrides[id]?.asReversed()?.forEach { (_, provider) ->
@@ -0,0 +1,26 @@
package ru.fromchat.plugins.host.ui
import androidx.compose.runtime.mutableStateListOf
data class PluginUiOverlay(
val pluginId: String,
val slot: String,
val title: String,
val message: String,
)
object PluginOverlayStore {
val overlays = mutableStateListOf<PluginUiOverlay>()
fun register(pluginId: String, slot: String, title: String, message: String) {
overlays.removeAll { it.pluginId == pluginId && it.slot == slot }
overlays += PluginUiOverlay(pluginId, slot, title, message)
}
fun unregisterPlugin(pluginId: String) {
overlays.removeAll { it.pluginId == pluginId }
}
fun overlaysFor(slot: String): List<PluginUiOverlay> =
overlays.filter { it.slot == slot }
}
@@ -32,10 +32,12 @@ fun PluginUiHost(content: @Composable () -> Unit) {
val bulletins = remember { PluginBulletinStore.bulletins }
Box(Modifier.fillMaxSize()) {
content()
bulletins.lastOrNull()?.let { bulletin ->
bulletins.firstOrNull()?.let { bulletin ->
LaunchedEffect(bulletin.id) {
delay(4_000)
bulletins.remove(bulletin)
if (bulletins.firstOrNull()?.id == bulletin.id) {
bulletins.removeAt(0)
}
}
Surface(
modifier = Modifier
@@ -1,21 +1,16 @@
package ru.fromchat.plugins.host
import net.bytebuddy.ByteBuddy
import net.bytebuddy.agent.ByteBuddyAgent
import net.bytebuddy.implementation.MethodDelegation
import net.bytebuddy.implementation.bind.annotation.AllArguments
import net.bytebuddy.implementation.bind.annotation.Origin
import net.bytebuddy.implementation.bind.annotation.RuntimeType
import net.bytebuddy.implementation.bind.annotation.SuperCall
import net.bytebuddy.asm.Advice
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy
import net.bytebuddy.matcher.ElementMatchers
import ru.fromchat.plugins.BasePlugin
import ru.fromchat.plugins.HookResult
import ru.fromchat.plugins.HookStrategy
import ru.fromchat.plugins.PluginManifest
import ru.fromchat.plugins.host.util.PluginLogger
import java.io.File
import java.lang.reflect.Method
import java.net.URLClassLoader
import java.util.concurrent.Callable
import net.bytebuddy.ByteBuddy
actual object PluginPlatform {
actual val current: PluginPlatform? get() = this
@@ -26,14 +21,14 @@ actual object PluginPlatform {
private var agentInstalled = false
actual fun installSharedHook(registration: SharedHookRegistration) {
val target = SharedMethodHookRegistry.targetFor(registration.hookId) ?: return
val handle = JvmSharedHookHandle(registration, target)
val handle = JvmSharedHookHandle(registration, registration.target)
handle.install()
hookHandles.getOrPut(registration.pluginId) { mutableListOf() } += handle
}
actual fun uninstallSharedHook(registration: SharedHookRegistration) {
hookHandles[registration.pluginId]?.removeAll { it.registration == registration }
HookAdviceBindings.unbind(registration)
}
actual fun loadPlugin(manifest: PluginManifest, pluginDir: String): LoadedPlugin? {
@@ -56,7 +51,7 @@ actual object PluginPlatform {
private fun ensureAgent() {
if (agentInstalled) return
runCatching { ByteBuddyAgent.install() }
ByteBuddyAgent.install()
agentInstalled = true
}
@@ -71,52 +66,37 @@ actual object PluginPlatform {
private val target: SharedHookTarget,
) : SharedHookHandle {
private var installed = false
private var boundMethod: Method? = null
override fun install() {
if (installed) return
ensureAgent()
val clazz = Class.forName(target.className)
val paramTypes = target.paramTypeNames.map { Class.forName(it) }.toTypedArray()
val method = clazz.getDeclaredMethod(target.methodName, *paramTypes)
ByteBuddy()
.redefine(clazz)
.method(ElementMatchers.named(target.methodName))
.intercept(MethodDelegation.to(JvmHookInterceptor(registration)))
.make()
.load(clazz.classLoader)
installed = true
runCatching {
ensureAgent()
val clazz = Class.forName(target.className)
val paramTypes = target.paramTypeNames.map { resolveJvmParameterType(it) }.toTypedArray()
val method = clazz.getDeclaredMethod(target.methodName, *paramTypes)
HookAdviceBindings.bind(method, registration)
boundMethod = method
ByteBuddy()
.redefine(clazz)
.visit(Advice.to(SharedHookAdvice::class.java).on(ElementMatchers.`is`(method)))
.make()
.load(clazz.classLoader, ClassReloadingStrategy.fromInstalledAgent())
installed = true
}.onFailure {
boundMethod?.let { HookAdviceBindings.unbind(registration) }
boundMethod = null
PluginLogger.log(
registration.pluginId,
"Raw hook install failed for ${target.className}.${target.methodName}: ${it.message}",
)
}
}
override fun uninstall() {
HookAdviceBindings.unbind(registration)
boundMethod = null
installed = false
}
}
class JvmHookInterceptor(private val registration: SharedHookRegistration) {
@RuntimeType
fun intercept(
@AllArguments args: Array<Any?>,
@Origin method: Method,
@SuperCall callable: Callable<Any?>,
): Any? {
registration.before?.let { before ->
val result = before(args)
when (result.strategy) {
HookStrategy.CANCEL -> return null
HookStrategy.MODIFY, HookStrategy.MODIFY_FINAL -> result.value?.let { return@intercept callable.call() }
HookStrategy.DEFAULT -> Unit
}
}
val value = callable.call()
registration.after?.let { after ->
val result = after(args, value)
when (result.strategy) {
HookStrategy.CANCEL -> return null
HookStrategy.MODIFY, HookStrategy.MODIFY_FINAL -> return result.value
HookStrategy.DEFAULT -> Unit
}
}
return value
}
}
}
@@ -0,0 +1,64 @@
package ru.fromchat.plugins.host
import net.bytebuddy.asm.Advice
import ru.fromchat.plugins.HookStrategy
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
internal object HookAdviceBindings {
private val byMethodKey = ConcurrentHashMap<String, SharedHookRegistration>()
fun bind(method: Method, registration: SharedHookRegistration) {
byMethodKey[key(method)] = registration
}
fun unbind(registration: SharedHookRegistration) {
byMethodKey.entries.removeIf { it.value == registration }
}
fun registration(method: Method): SharedHookRegistration? = byMethodKey[key(method)]
private fun key(method: Method): String =
buildString {
append(method.declaringClass.name)
append('#')
append(method.name)
append('(')
method.parameterTypes.forEachIndexed { index, type ->
if (index > 0) append(',')
append(type.name)
}
append(')')
}
}
internal class SharedHookAdvice {
companion object {
@JvmStatic
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue::class)
fun onEnter(
@Advice.AllArguments arguments: Array<Any?>,
@Advice.Origin method: Method,
): Boolean {
val registration = HookAdviceBindings.registration(method) ?: return false
registration.before?.let { before ->
when (before(arguments).strategy) {
HookStrategy.CANCEL -> return true
HookStrategy.MODIFY, HookStrategy.MODIFY_FINAL, HookStrategy.DEFAULT -> Unit
}
}
return false
}
@JvmStatic
@Advice.OnMethodExit(onThrowable = Throwable::class)
fun onExit(
@Advice.AllArguments arguments: Array<Any?>,
@Advice.Origin method: Method,
@Advice.Enter skipOriginal: Boolean,
) {
if (skipOriginal) return
HookAdviceBindings.registration(method)?.after?.invoke(arguments, null)
}
}
}
+3 -3
View File
@@ -35,10 +35,10 @@ tasks.register("packageFcPlugin") {
{
"id": "hello_world",
"name": "Hello World",
"description": "Demonstrates high-level send hooks and raw Logger.d hooks.",
"description": "Demonstrates high-level send hooks, raw Logger.d hooks, and raw main-nav hooks.",
"author": "FromChat",
"version": "1.0.1",
"usage": "Send .hello Name in any chat to rewrite the message. Raw hook shows a bulletin when a message is queued for sending.",
"version": "1.0.3",
"usage": "Raw-hooks existing MainScreen/ContactsTab JVM methods (no app hook IDs). Also hides Devices/Logs settings and emoji; injects settings row, message menu item, and chat banner. Send .hello Name to test send hooks.",
"app_version": ">=1.0.0",
"sdk_version": ">=1.0.0",
"entry_class": "ru.fromchat.plugins.sample.hello.HelloWorldPlugin",
@@ -3,6 +3,8 @@ package ru.fromchat.plugins.sample.hello
import ru.fromchat.plugins.BasePlugin
import ru.fromchat.plugins.HookResult
import ru.fromchat.plugins.HookStrategy
import ru.fromchat.plugins.MenuItemData
import ru.fromchat.plugins.MenuItemType
import ru.fromchat.plugins.PluginSetting
import ru.fromchat.plugins.SendMessageHookContext
@@ -10,12 +12,15 @@ class HelloWorldPlugin : BasePlugin() {
override fun onPluginLoad() {
log("Hello World plugin loaded")
showBulletin("Hello World plugin is active")
registerUiOverrides()
registerInjectedUi()
registerRawHooks()
addOnSendMessageHook { context ->
onSendMessage(context)
}
hookShared(
hookId = "logger.debug",
after = { args, _ ->
before = { args ->
val tag = args.getOrNull(0) as? String ?: return@hookShared HookResult()
val message = args.getOrNull(1) as? String ?: return@hookShared HookResult()
if (tag == "OutgoingMessageCoordinator" && message.contains("enqueue")) {
@@ -26,6 +31,13 @@ class HelloWorldPlugin : BasePlugin() {
)
}
override fun onMenuItemClick(key: String) {
when (key) {
MENU_SETTINGS_DEMO -> showBulletin("Injected settings row clicked")
MENU_MESSAGE_DEMO -> showBulletin("Injected message menu clicked")
}
}
override fun createSettings(): List<PluginSetting> = listOf(
PluginSetting.Header("Hello World"),
PluginSetting.Input(
@@ -40,6 +52,122 @@ class HelloWorldPlugin : BasePlugin() {
),
)
private fun registerRawHooks() {
hookRawMethod(
className = "ru.fromchat.ui.main.MainScreenKt",
methodName = "selectMainPage",
paramTypeNames = arrayOf(
"int",
"com.pr0gramm3r101.utils.WindowWidthSizeClass",
"kotlinx.coroutines.CoroutineScope",
"androidx.compose.foundation.pager.PagerState",
"androidx.navigation.NavController",
),
before = { args ->
val page = (args.getOrNull(0) as? Number)?.toInt() ?: return@hookRawMethod HookResult()
if (page == MAIN_PAGE_CONTACTS) {
if (!contactsNavBlockedNotified) {
contactsNavBlockedNotified = true
showBulletin("Raw hook: blocked Contacts tab navigation")
}
HookResult(strategy = HookStrategy.CANCEL)
} else {
HookResult()
}
},
)
hookRawMethod(
className = "ru.fromchat.ui.main.ContactsTabKt",
methodName = "ContactsTab",
paramTypeNames = arrayOf(
"androidx.compose.runtime.Composer",
"int",
),
before = { _ ->
HookResult(strategy = HookStrategy.CANCEL)
},
)
hookRawMethod(
className = "ru.fromchat.ui.main.MainScreenKt",
methodName = "MainScreen",
paramTypeNames = arrayOf(
"androidx.compose.animation.SharedTransitionScope",
"androidx.compose.animation.AnimatedVisibilityScope",
"androidx.compose.material3.SnackbarHostState",
"boolean",
"int",
"boolean",
"kotlin.jvm.functions.Function1",
"androidx.compose.runtime.Composer",
"int",
"int",
),
before = { _ ->
mainScreenNavItemOrdinal = 0
HookResult()
},
)
hookRawMethod(
className = "androidx.compose.material3.NavigationBarKt",
methodName = "NavigationBarItem",
paramTypeNames = arrayOf(
"androidx.compose.foundation.layout.RowScope",
"boolean",
"kotlin.jvm.functions.Function0",
"kotlin.jvm.functions.Function2",
"androidx.compose.ui.Modifier",
"boolean",
"kotlin.jvm.functions.Function2",
"boolean",
"androidx.compose.material3.NavigationBarItemColors",
"androidx.compose.foundation.interaction.MutableInteractionSource",
"androidx.compose.runtime.Composer",
"int",
"int",
),
before = { _ ->
mainScreenNavItemOrdinal += 1
if (mainScreenNavItemOrdinal == CONTACTS_NAV_ITEM_ORDINAL) {
HookResult(strategy = HookStrategy.CANCEL)
} else {
HookResult()
}
},
)
}
private fun registerUiOverrides() {
registerFeatureOverride(FEATURE_SETTINGS_DEVICES) { false }
registerFeatureOverride(FEATURE_SETTINGS_LOGS) { false }
registerFeatureOverride(FEATURE_CHAT_EMOJI) { false }
}
private fun registerInjectedUi() {
registerUiOverlay(
slot = OVERLAY_CHAT_BANNER,
title = "Hello World plugin UI",
message = "This banner was injected by the plugin engine.",
)
addMenuItem(
MenuItemData(
menuType = MenuItemType.SETTINGS_ROW,
text = "Hello World demo action",
subtext = "Injected settings row from plugin",
priority = 10,
onClickKey = MENU_SETTINGS_DEMO,
),
)
addMenuItem(
MenuItemData(
menuType = MenuItemType.MESSAGE_CONTEXT,
text = "Hello World message action",
subtext = "Injected message menu item",
priority = 10,
onClickKey = MENU_MESSAGE_DEMO,
),
)
}
private fun onSendMessage(context: SendMessageHookContext): HookResult<SendMessageHookContext> {
val raw = context.text.trim()
if (!raw.startsWith(".hello")) return HookResult()
@@ -50,4 +178,21 @@ class HelloWorldPlugin : BasePlugin() {
showBulletin("High-level hook: rewrote .hello command")
return HookResult(strategy = HookStrategy.MODIFY, value = context)
}
private companion object {
const val MAIN_PAGE_CONTACTS = 1
const val CONTACTS_NAV_ITEM_ORDINAL = 2
const val FEATURE_SETTINGS_DEVICES = "settings.devices"
const val FEATURE_SETTINGS_LOGS = "settings.logs"
const val FEATURE_CHAT_EMOJI = "chat.emoji_picker"
const val OVERLAY_CHAT_BANNER = "chat.banner"
const val MENU_SETTINGS_DEMO = "hello_world.settings_demo"
const val MENU_MESSAGE_DEMO = "hello_world.message_demo"
@Volatile
var mainScreenNavItemOrdinal: Int = 0
@Volatile
var contactsNavBlockedNotified: Boolean = false
}
}
@@ -8,6 +8,7 @@ interface PluginHostBridge {
fun registerSendMessageHook(pluginId: String, priority: Int, handler: (SendMessageHookContext) -> HookResult<SendMessageHookContext>)
fun registerFeatureOverride(pluginId: String, featureId: String, provider: () -> Boolean?)
fun registerMenuItem(pluginId: String, item: MenuItemData)
fun registerUiOverlay(pluginId: String, slot: String, title: String, message: String)
fun showBulletin(message: String)
fun hookSharedMethod(
pluginId: String,
@@ -16,6 +17,16 @@ interface PluginHostBridge {
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
): () -> Unit
fun hookRawMethod(
pluginId: String,
className: String,
methodName: String,
paramTypeNames: Array<String>,
priority: Int,
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
): () -> Unit
}
abstract class BasePlugin {
@@ -31,6 +42,7 @@ abstract class BasePlugin {
open fun onPluginLoad() {}
open fun onPluginUnload() {}
open fun onAppEvent(event: AppEvent) {}
open fun onMenuItemClick(key: String) {}
open fun createSettings(): List<PluginSetting> = emptyList()
protected fun log(message: String) = bridge.log(pluginId, message)
@@ -63,6 +75,10 @@ abstract class BasePlugin {
bridge.registerMenuItem(pluginId, item)
}
protected fun registerUiOverlay(slot: String, title: String, message: String) {
bridge.registerUiOverlay(pluginId, slot, title, message)
}
protected fun showBulletin(message: String) {
bridge.showBulletin(message)
}
@@ -73,6 +89,23 @@ abstract class BasePlugin {
before: ((Array<Any?>) -> HookResult<Array<Any?>>)? = null,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)? = null,
): () -> Unit = bridge.hookSharedMethod(pluginId, hookId, priority, before, after)
protected fun hookRawMethod(
className: String,
methodName: String,
paramTypeNames: Array<String> = emptyArray(),
priority: Int = 0,
before: ((Array<Any?>) -> HookResult<Array<Any?>>)? = null,
after: ((Array<Any?>, Any?) -> HookResult<Any?>)? = null,
): () -> Unit = bridge.hookRawMethod(
pluginId,
className,
methodName,
paramTypeNames,
priority,
before,
after,
)
}
object PluginSettingsReload {