promotional bannermobile promotional banner

MixinTale - Developer Tools

MixinTale's compile-time half: the annotations, and the processor that checks your patches. Mod authors only

MixinTale — Developer Tools

The compile-time half of MixinTale. Mod authors only — players and server owners do not need this, and should install MixinTale instead

What's in the zip

File What it is
MixinTale-API-3.0.0.jar 5 KB. The annotations: @Patch, @Prefix, @Postfix, @Replace, @RedirectCall, @Accessor, @Invoker, and the parameter markers @This, @Arg, @Result
MixinTale-Processor-3.0.0.jar 49 KB. The annotation processor. Validates your patches while you compile and writes a mixintale.index.json manifest into your jar. No ASM, no weaver — nothing that could collide with a bytecode library your own build already uses
MIXINTALE.md The full manual, same as the one on GitHub
manifest.json Metadata for the archive itself. Nothing here is installed into Mods or EarlyPlugins

Neither jar ships inside your mod. The annotations have CLASS retention and the engine reads the generated index, never annotations at runtime

Setup

// build.gradle.kts
java {
    // Hytale 0.6.3 ships class-file version 69. javac refuses to *read* class files newer than
    // its --release setting, so anything compiling against the game needs 25 or above.
    toolchain { languageVersion = JavaLanguageVersion.of(25) }
}
tasks.withType<JavaCompile> {
    options.release = 25
    options.compilerArgs.add("-parameters")
}

val hytale = "D:/Games/Hytale/Hytale Game/install/release/package/game/latest/Server/HytaleServer.jar"

dependencies {
    compileOnly(files("MixinTale-API-3.0.0.jar"))
    annotationProcessor(files("MixinTale-Processor-3.0.0.jar"))
    compileOnly(files(hytale))
}

Groovy DSL, Maven and a plain javac -processorpath invocation all work the same way; the manual has each.

One thing that is not about MixinTale but will cost you an hour

Your manifest.json needs a ServerVersion range, and a bare version is not a range:

"ServerVersion": ">=0.6.0 <0.7.0"

"0.6.3" is rejected — Hytale answers "Bare version '0.6.3' is not a valid range. Use '=0.6.3' for an exact match, or '^0.6.3' / '~0.6.3' for a range." The rejection is an exception thrown while the manifest is decoded, so it does not merely warn: it takes the plugin load pass down with it, and none of your patches apply. *, ^0.6.0, ~0.6.1, >=0.6.0 <0.7.0 and 0.6 all parse

If you keep the version in one place, expand it into the manifest at build time:

tasks.named<ProcessResources>("processResources") {
    val values = mapOf("version" to project.version.toString())
    inputs.properties(values)
    filesMatching("manifest.json") { expand(values) }
}

Write a patch

@Patch(ItemStack.class)
public final class DoubleDurabilityPatch {

    private DoubleDurabilityPatch() {
    }

    @Postfix("getMaxDurability")
    public static double doubled(@This ItemStack self, @Result double original) {
        return original * 2.0D;
    }
}

No descriptor anywhere. The processor reads your handler's parameter types, works out that the target is ()D, and fails the build if getMaxDurability does not exist or is ambiguous. Then confirm the manifest landed:

jar tf your-mod.jar | grep mixintale.index.json

Processor options

Option Effect
-Amixintale.allowedPackages=a.b,c.d extra package prefixes a handler signature may reference
-Amixintale.verbose=true confirm what was generated; silent otherwise

Notes are off by default so a clean build stays clean. An index that cannot be written is always a build error, never a warning

Documentation

https://github.com/Traktool/MixinTale — every primitive with a worked example, signature inference, parameter binding, gating, priority, the runtime switches, troubleshooting, and what to do after a game update

Every Java example on that page is compiled against the real game classes before publication

Requirements

  • JDK 25 or later, and Gradle 9.1 or later if you use Gradle (earlier Gradle cannot run on Java 25 — its embedded Kotlin compiler fails to parse the version)
  • A Hytale 0.6.x install to compile against
  • Your users need MixinTale in EarlyPlugins Declare it in your mod page's requirements: there is no dependency mechanism for early plugins, so nothing will install it for them

Migrating from 2.0.0

Every annotation changed shape. Nothing is deprecated-but-working: 2.x sources do not compile against 3.0.0, and that is deliberate — the old API could not survive dropping the Mixin dependency, and there is far less to write now

2.0.0 3.0.0
@Patch(targetClass = "com/hypixel/hytale/…/Foo") @Patch(Foo.class) — the compiler checks it. className = "…" still takes a name, in dots or slashes, for targets off your compile path
@Prefix(targetMethod = "m", targetDesc = "(I)Z") @Prefix("m") — the descriptor is inferred from your handler. Pass descriptor = "(I)Z" only to pick between overloads
@Postfix(targetMethod = …, targetDesc = …) @Postfix("m")
@Replace(targetMethod = …, targetDesc = …) @Replace("m")
@RedirectCall(targetMethod, targetDesc, owner, name, desc, ordinal, require) @RedirectCall(value = "m", target = Owner.class, name = "called"). ordinal and require are unchanged; descriptor, ownerName and callDescriptor are the optional overrides for the three inferred parts
@Accessor("field") Same, and it now does setters, static fields and instance fields alike — told apart by the signature you declare
@Invoker("method") is new: calls a private method of the target
@WrapCall + Operation<R> Removed. Use @RedirectCall
@This, @Arg(n), @Result Unchanged in spelling. See the two notes below

What @WrapCall becomes

A wrap injection needs an interface that both your mod and the woven game class can load, and by construction no MixinTale class is visible from the game's class loader. @RedirectCall reaches the same place from the other side: its body is relocated into the target class, so it can perform the original invocation itself, on its own terms

// 2.0.0
@WrapCall(targetMethod = "compareTo", targetDesc = "(…)I",
          owner = "java/lang/Long", name = "compare", desc = "(JJ)I", ordinal = 1)
public static int wrap(Operation<Integer> original, @Arg(0) long l, @Arg(1) long r) throws Throwable {
    return original.call(l, r);
}

// 3.0.0
@RedirectCall(value = "compareTo", target = Long.class, name = "compare", ordinal = 1)
public static int lenientMinor(@Arg(0) long left, @Arg(1) long right) {
    if (left == SENTINEL) {
        return 0;
    }
    return Long.compare(left, right);   // the original call, whenever you decide to make it
}

You gain the ability to skip the original entirely, and lose the Object... boxing that Operation.call forced on every argument

Two behaviours to re-check

  • @Prefix control flow. Return void to observe; return boolean and false to skip the original body. To supply a return value when you cancel, take a @Result one-element array and write result[0]
  • Priority direction. Higher priority runs closer to the original — for prefixes as well as postfixes. A prefix-ordering bug meant this did not hold in 2.x; if you shipped two patches on one method and tuned their priorities around the old behaviour, check them

Your users

They need MixinTale 3.0.0 in EarlyPlugins. A mod compiled against 3.0.0 will not be applied by a 2.0.0 bootstrap, and vice versa — the index format changed. Bump the MixinTale version in your page's Relations → Dependencies at the same time you upload

The MixinTale - Developer Tools Team

profile avatar
  • 2
    Followers
  • 7
    Projects
  • 22.0K
    Downloads

More from TraktoolView all