Skip to content
On this page

代码注入 ​

WARNING

代码注入功能仅适用于 Fabric 和 NeoForge,Paper 不支持。

Mixin 是非常强大的模组开发工具,但它本质上是静态机制,只能在模组初始化阶段加载。所以我们无法直接在 Kotlin 脚本中使用 Mixin 做运行时注入。不过不用担心,Katton 提供了轻量级代码注入 API,可以在运行时向已有 Java 方法注入逻辑,并且支持热重载。

插入 ​

要在已有方法中插入代码,可以使用 injectBefore 或 injectAfter。特别地,构造函数注入可使用 injectConstructorBefore 或 injectConstructorAfter。

替换或重定向 ​

使用 replace 可以将目标方法整体替换成你自己的实现。你也可以使用 redirect 将目标方法调用重定向到你的实现。

参数与返回值操作 ​

injectBefore 与 injectAfter 都会提供 call 对象,你可以通过它修改方法参数或返回值。

你可以使用 call.setArgument(index, value) 修改参数,使用 call.setReturnValue(value) 修改返回值。你也可以调用 call.cancel() 直接取消原方法执行并立即返回。

示例 ​

kotlin
import net.minecraft.network.chat.Component
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.dpcaller.tell
import top.katton.api.inject.injectAfter
import top.katton.api.inject.injectBefore
import top.katton.api.inject.redirect
import top.katton.api.inject.replace
import top.katton.api.inject.rollbackUnsafe
import top.katton.registry.registerCommand

// The target test class we want to inject into
private class UnsafeDemoTarget {
    fun add(a: Int, b: Int): Int = a + b

    fun divide(a: Int, b: Int): Int = a / b

    fun multiply(a: Int, b: Int): Int = a * b

    fun addRedirect(a: Int, b: Int): Int = a + b

    fun multiplyTarget(a: Int, b: Int): Int = a * b
}

@ServerScriptEntrypoint(ServerPhase.READY)
fun unsafeDemo() {
    // register a simple command to test the code injection
    registerCommand("unsafe_demo") {
        literal("test") {
            executes { ctx ->
                // Get the method we want to inject into using reflection
                val method = UnsafeDemoTarget::class.java.getDeclaredMethod(
                    "add",
                    Int::class.javaPrimitiveType,
                    Int::class.javaPrimitiveType
                )

                // Inject code before and after the method execution
                val before = injectBefore(method) { call ->
                    tell("[unsafe_demo] before ${call.method.name}, args=${call.arguments.contentToString()}")
                }
                
                // The after injection can also access the result and throwable of the method execution
                val after = injectAfter(method) { call, result, throwable ->
                    tell("[unsafe_demo] after ${call.method.name}, result=$result, throwable=${throwable?.message}")
                }

                val methodDivide = UnsafeDemoTarget::class.java.getDeclaredMethod(
                    "divide",
                    Int::class.javaPrimitiveType,
                    Int::class.javaPrimitiveType
                )

                // You can also mutate the arguments and the return value, 
                // or even cancel the original method execution
                val beforeMutate = injectBefore(methodDivide) { call ->
                    // argument rewrite: force denominator to 1 if input is 0
                    if ((call.arguments[1] as Int) == 0) {
                        call.setArgument(1, 1)
                    }
                }

                val beforeCancel = injectBefore(methodDivide) { call ->
                    // cancellable: if numerator is negative, skip original method body
                    if ((call.arguments[0] as Int) < 0) {
                        call.cancelWith(42)
                    }
                }

                val afterOverride = injectAfter(methodDivide) { call, result, throwable ->
                    // return override: multiply positive result by 10
                    if (throwable == null && result is Int && result > 0) {
                        call.setReturnValue(result * 10)
                    }
                }

                val methodMultiply = UnsafeDemoTarget::class.java.getDeclaredMethod(
                    "multiply",
                    Int::class.javaPrimitiveType,
                    Int::class.javaPrimitiveType
                )

                val replaceMultiply = replace(methodMultiply) { call ->
                    val a = call.arguments[0] as Int
                    val b = call.arguments[1] as Int
                    // replace: ignore original body and return custom value
                    a * b + 100
                }

                val sourceRedirect = UnsafeDemoTarget::class.java.getDeclaredMethod(
                    "addRedirect",
                    Int::class.javaPrimitiveType,
                    Int::class.javaPrimitiveType
                )
                val targetRedirect = UnsafeDemoTarget::class.java.getDeclaredMethod(
                    "multiplyTarget",
                    Int::class.javaPrimitiveType,
                    Int::class.javaPrimitiveType
                )
                val redirectAdd = redirect(sourceRedirect, targetRedirect)

                // Now we can test the injected code by calling the target methods
                val target = UnsafeDemoTarget()
                val value = target.add(2, 3)
                val divA = target.divide(10, 0)
                val divB = target.divide(-5, 0)
                val mul = target.multiply(2, 3)
                val redirected = target.addRedirect(4, 5)

                // Manually rollback the injections to restore the original method behavior
                // It will also be automatically rolled back when the script is reloaded
                rollbackUnsafe(before)
                rollbackUnsafe(after)
                rollbackUnsafe(beforeMutate)
                rollbackUnsafe(beforeCancel)
                rollbackUnsafe(afterOverride)
                rollbackUnsafe(replaceMultiply)
                rollbackUnsafe(redirectAdd)

                ctx.source.sendSuccess(
                    {
                        Component.literal(
                            "[unsafe_demo] add=$value, divA=$divA, divB=$divB, replaceMul=$mul, redirect=$redirected"
                        )
                    },
                    false
                )
                1
            }
        }
    }
}