Skip to content
On this page

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

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
            }
        }
    }
}