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