Skip to content

Injection DSL & Mixin ​

When you need to change Minecraft's unexposed internals, use OML's injection DSL instead of Mixin. Rules are purely declarative and verified one by one at startup (handler existence, signatures, match counts) — a wrong rule fails the launch instead of silently doing nothing in game.

Rule set skeleton ​

kotlin
@RuleSource("my_mod")
object MyRules : RuleSetProvider {
    override fun rules(): RuleSet = injection {
        classTarget("net/minecraft/client/Minecraft") {
            method("runTick") {
                atHead { call("com/example/MyRules", "onGameTick", "()V") }
                require(1)
            }
        }
    }
}
  • classTarget takes the target class's internal name (net/minecraft/..., slash-separated);
  • the handler (onGameTick) is a static method in your own class, called directly by the injected bytecode;
  • require(1) declares a match-count lower bound: not met, the launch fails with the reason printed. Always declare it on must-hit rules — otherwise a drifted target makes the rule silently do nothing.

Anchors ​

AnchorSemanticsMixin equivalent
atHeadmethod headHEAD
atConstructorHeadafter super() in a constructor (from here, this is usable)–
atReturn(ordinal)before every return; atReturn() = allRETURN
atTailbefore the last returnTAIL
beforeCall / afterCallbefore / after a method callINVOKE / INVOKE_ASSIGN
beforeField / afterFieldbefore / after a field access–
beforeNewbefore a new instruction–
beforeConstant / afterConstantbefore / after a constant loadCONSTANT
afterStore / beforeLoadafter a local-variable store / before a loadStore / Load
within(from, to)restricts the search window between two anchors (nestable, intersection)@Slice

Anchors accept owner / desc / ordinal filters (atReturn(0) hits only the first return).

Payloads ​

Each anchor block declares what to inject:

PayloadSemanticsMixin equivalent
call(owner, method, desc)calls a static method at the anchor (locals can be injected as arguments)@Inject
redirectCall(...)redirects one method call@Redirect
modifyArg(...) / modifyArgs(...)rewrites call arguments@ModifyArg / @ModifyArgs
modifyConstant(...)rewrites a constant@ModifyConstant
modifyExpressionValue(...)rewrites an expression's value@ModifyExpressionValue
modifyVariable(...)rewrites a local variable@ModifyVariable

Match-count policies ​

DeclarationSemantics
require(n)at least n hits, otherwise the launch fails (lower bound)
allow(n)at most n hits, more fails (guards over-broad rules)
expect(n)expects n hits; deviation only warns
optional()zero hits allowed, suppressing the "no match" warning

Access rewriting and class merging ​

kotlin
classTarget("net/minecraft/client/Minecraft") {
    field("proxy", desc = "Ljava/net/Proxy;") {
        makePublic()
        removeFinal()
    }
    methodAccess("runTick") { makePublic() }

    // class merging: a mod class is *merged into* the target class; its handlers
    // run with `this` = the target instance
    merge("com/example/MyHooks")
}

Inside a merge source, mark members with @Shadow ("the target class already has this member"), @Unique (new member), @Overwrite (replace a method body) and @Accessor / @Invoker (synthesized field accessors / private-method callers) — Mixin semantics, synthesized at the bytecode level by the engine.

Mixin annotations ​

Used to Mixin? Keep writing them. @Mixin / @Inject / @Redirect / @ModifyArg / @ModifyArgs / @ModifyConstant / @ModifyVariable / @ModifyReturnValue / @ModifyExpressionValue are supported, with @At and @Slice. The front-end compiles the annotations into the same injection rules — same pipeline, same startup checks. Unsupported Mixin features (Shift.BY, dynamic targets, @Pseudo, refmaps) fail the startup with an explanation instead of failing silently.

Startup checks ​

Every rule is re-verified before control is handed to the game's main thread: handler existence, staticness, signature consistency and match counts. The default mode fail prints each problem and aborts; -Doml.injection.verify=warn only warns; off disables. For timing and scale statistics, add -Doml.diagnostics=1 — that switch is boolean (any non-blank value other than false turns it on; the value carries no meaning).

Released under the AGPL-3.0 license.