Skip to content
On this page

代码注入

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

插入

要在已有方法中插入代码,可以使用 injectBeforeinjectAfter。特别地,构造函数注入可使用 injectConstructorBeforeinjectConstructorAfter

替换或重定向

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

参数与返回值操作

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

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

示例

kotlin
import net.minecraft.network.chat.Component
import top.katton.api.dpcaller.tell
import top.katton.api.inject.*
import top.katton.registry.registerCommand
import kotlin.jvm.java

// 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
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 = injectReplace(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 = injectRedirect(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
            }
        }
    }
}