代码注入
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.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
}
}
}
}