Code injection
Mixin is a powerful tool for modding, but it is designed to be static and can only be loaded during the mod initialization phase. So unfortunately, we cannot use mixin to inject code in our kotlin scripts. But don't worry, Katton provides a lightweight code injection API that allows you to inject code into existing Java methods at runtime and still hot-reloadable.
Insert
To insert code into an existing method, you can use injectBefore or injectAfter function. Specially, use injectConstructorBefore or injectConstructorAfter to inject code into constructors.
Replace or Redirect
Use replace function to replace the target method with your own implementation. You can also use redirect function to redirect the target method call to your own method.
Argument and Return Value Manipulation
injectBefore and injectAfter functions provide a call object that allows you to manipulate the method arguments and return value. You can use call.setArgument(index, value) to change the method arguments, and use call.setReturnValue(value) to change the return value. You can also use call.cancel() to skip the original method execution and return immediately.
Example
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
}
}
}
}