CommunityCodierung & Entwicklunggithub.com

vvs1off/fabric-mixins

Schreiben, Überprüfen oder Reparieren von Mixins in einem Fabric-Mod – Auswahl zwischen einem Fabric API-Ereignis und einem Mixin, @Inject, @Redirect, @ModifyVariable, den sichereren MixinExtras-Injektoren (@WrapOperation, @ModifyExpressionValue, @WrapWithCondition, @Local), Accessoren/Invokern, Class-Tweakern (Access Wideners), modulübergreifender Kompatibilität und Debugging mit mixin.debug.export. Verwenden, wenn eine Aufgabe das Ändern von Vanilla-Verhalten, das Lesen privater Felder oder das Beheben fehlgeschlagener Mixins erfordert.

Was ist fabric-mixins?

Dieses Skill-Dokument beschreibt, wie man Mixins in einem Fabric-Mod für Minecraft korrekt erstellt, überprüft und repariert. Es betont, dass Mixins mächtig, aber zerbrechlich sind und daher sparsam und so schmal wie möglich eingesetzt werden sollten. Die Anleitung führt in einer Entscheidungshierarchie auf, zuerst Fabric API-Ereignisse, Vanilla-Erweiterungspunkte, Accessoren und Class-Tweaker zu prüfen, bevor man zu Injektor-Mixins greift. Es behandelt die wichtigsten Injektor-Typen (@Inject, @ModifyExpressionValue, @WrapOperation, @WrapWithCondition, @Local) und rät zu MixinExtras anstelle von @Redirect und @ModifyVariable, da diese besser mit anderen Mods kombinierbar sind. Das Dokument enthält Codebeispiele für Accessoren/Invoker, Class-Tweaker (Access Wideners) und eine Checkliste für modulübergreifende Kompatibilität. Debugging-Tipps (mixin.debug.export) und häufige Fehler runden das Skill ab.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/vvs1off/fabric-skills/tree/main/skills/fabric-mixins

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Was macht fabric-mixins?

Mixins patchen den Bytecode von Minecraft zur Ladezeit. Sie sind mächtig und zerbrechlich: Jedes Mixin ist ein Versprechen, dass eine bestimmte Methode mit einer bestimmten Signatur in der nächsten Version noch existiert und dass kein anderer Mod sie inkompatibel ändert. Schreibe so wenige wie möglich, so schmal wie möglich.

Entscheidungsreihenfolge – Versuche dies, bevor du ein Mixin schreibst

  1. Fabric API-Ereignis oder Registry. Durchsuche zuerst Fabric API: Lebenszyklus-Ereignisse (ServerLifecycleEvents, ClientTickEvents), Spieler-Ereignisse (UseBlockCallback, AttackEntityCallback, PlayerBlockBreakEvents), HUD (HudElementRegistry), Bildschirme (ScreenEvents), Beute (LootTableEvents), Befehle (CommandRegistrationCallback), Netzwerk, Item-Tooltips, Resource-Reload-Listener und viele mehr. Durchstöbere https://github.com/FabricMC/fabric-api (das api-Paket jedes Moduls) und die Fabric-Dokumentation. Ein Ereignis ist versionsstabil und komponiert problemlos mit anderen Mods.
  2. Vanilla-Erweiterungspunkte. Datengetriebene Inhalte (Tags, Beutetabellen, Rezepte, Datenkomponenten, Registries) benötigen oft überhaupt kein Code-Patching.
  3. Accessor-/Invoker-Mixin – wenn du nur ein privates Feld lesen/schreiben oder eine private Methode aufrufen musst.
  4. Class-Tweaker (Access Widener) – wenn du eine finale Klasse ableiten, eine finale Methode überschreiben oder eine private/package-private Klasse referenzieren musst.
  5. Injektor-Mixin – erst jetzt. Bevorzuge MixinExtras-Injektoren gegenüber @Redirect/@Overwrite.

Notiere (in einem Kommentar über dem Mixin), warum die Schritte 1–4 nicht funktioniert haben. Review-Entwickler werden danach fragen.

Bevor du einen Injektor schreibst: Bestätige das Ziel

Namen und Signaturen ändern sich mit jeder Minecraft-Version. Schreibe niemals ein Ziel aus dem Gedächtnis.

./gradlew genSources        # öffne dann die Klasse in der IDE oder durchsuche die generierten Quellen
  • 26.1+ (entobfuskiert): Ziele auf die Mojang-Namen genau so, wie sie in den Quellen erscheinen.
  • Vor 26.1: Ziele auf die Namen deiner Projekt-Mappings (Yarn oder Mojang). Looms Mixin-Annotationsprozessor mapped sie zur Build-Zeit auf Intermediary um; eine Nichtübereinstimmung zeigt sich als "Cannot remap"-Warnung zur Kompilierzeit – behandle diese Warnung als Fehler.
  • Kopiere Methodendeskriptoren aus der Bytecode-Ansicht oder einem Tool (MCDev IntelliJ Plugin, mcsrc.dev), nicht von Hand. Deskriptor-Referenz: https://docs.fabricmc.net/develop/mixins/bytecode

Mixin-Konfiguration und Konventionen

{
  "required": true,
  "package": "com.example.examplemod.mixin",
  "compatibilityLevel": "JAVA_25",
  "mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
  "client": [],
  "server": [],
  "injectors": { "defaultRequire": 1 },
  "overwrites": { "requireAnnotations": true }
}
  • "defaultRequire": 1 lässt ein Mixin, das nichts findet, lautstark fehlschlagen, anstatt still nichts zu tun. Behalte es bei.
  • Nur-Client-Ziele (net.minecraft.client.*) kommen in die Client-Konfiguration (in src/client/resources, deklariert mit "environment": "client" in fabric.mod.json).
  • Mixin-Klassen werden niemals von normalem Code referenziert (außer Accessor-Interfaces, via Cast).
  • Präfixiere jedes hinzugefügte Mitglied mit deiner Mod-ID, um Kollisionen mit anderen Mods zu vermeiden: examplemod$onTick, @Unique private int examplemod$counter;.
  • Markiere Hilfsfelder/-methoden, die du hinzufügst, mit @Unique.
  • Handler-Methoden sind private (nicht-statisch, wenn das Ziel nicht-statisch ist).

Die Ziele in den Beispielen unten verwenden Mojang-Namen und zeigen die Form jedes Injektors. Methodennamen und Deskriptoren ändern sich zwischen den Versionen – überprüfe jeden einzelnen in deinen generierten Quellen, bevor du ihn wiederverwendest.

@Inject – Code an einem Punkt ausführen, optional abbrechen

@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
    @Inject(method = "jumpFromGround", at = @At("HEAD"), cancellable = true)
    private void examplemod$cancelJump(CallbackInfo ci) {
        if (ExampleConfig.get().disableJump) {
            ci.cancel();
        }
    }

    // getJumpPower ist überladen (getJumpPower() und getJumpPower(float)): gib den Deskriptor an.
    @Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
    private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
        cir.setReturnValue(cir.getReturnValueF() * 1.5f);
    }
}
  • CallbackInfo für void-Ziele, CallbackInfoReturnable<T> sonst (boxed type).
  • Deklariere cancellable = true nur, wenn du tatsächlich abbrichst. Ein Abbruch bei HEAD überspringt die Vanilla-Methode und jede andere Mod-Injektion nach deiner – das ist das inkompatibelste, was ein @Inject tun kann. Bevorzuge das Ändern eines Wertes.
  • Um die Parameter des Ziels zu lesen, füge sie der Handler-Signatur vor dem CallbackInfo hinzu, in der Reihenfolge: (float amount, CallbackInfo ci).
  • Häufige @At-Werte: HEAD, RETURN (jeder Rückgabepunkt), TAIL (nur letzter Rückgabepunkt), INVOKE (vor einem Aufruf; füge shift = At.Shift.AFTER für danach hinzu), FIELD, NEW.
  • Verwende ordinal nur, wenn es sein muss; es bricht, sobald Mojang einen weiteren passenden Aufruf hinzufügt.

MixinExtras – Verwende diese anstelle von @Redirect und den meisten @ModifyVariable

MixinExtras wird in Fabric Loader (0.15+) mitgeliefert, daher ist keine zusätzliche Abhängigkeit nötig. Importe sind unter com.llamalad7.mixinextras. Dokumentation: https://github.com/LlamaLad7/MixinExtras/wiki

@ModifyExpressionValue – Ändere, was ein Aufruf/eine Feld-Lesung zurückgibt

@ModifyExpressionValue(
    method = "travelInWater",
    at = @At(value = "INVOKE",
             target = "Lnet/minecraft/world/entity/LivingEntity;getSpeed()F")
)
private float examplemod$fasterInWater(float original) {
    return original * 1.2f;
}

Mehrere Mods können @ModifyExpressionValue auf denselben Ausdruck verketten; jeder erhält das Ergebnis des vorherigen Mods. Ein @Redirect auf denselben Aufruf würde sich widersprechen und abstürzen.

@WrapOperation – Umhülle einen Aufruf; rufe das Original auf (oder nicht)

@WrapOperation(
    method = "hurtServer",
    at = @At(value = "INVOKE",
             target = "Lnet/minecraft/world/entity/LivingEntity;actuallyHurt(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;F)V")
)
private void examplemod$logDamage(LivingEntity self, ServerLevel level, DamageSource source,
                                  float amount, Operation<Void> original) {
    ExampleMod.LOGGER.debug("{} hat {} Schaden erlitten", self, amount);
    original.call(self, level, source, amount);   // behalte die Kette intakt
}
  • Parameter: der Empfänger (für nicht-statische Aufrufe), dann die Argumente des Aufrufs, dann Operation<R> wobei R der boxed return type ist (Void für void).
  • Rufe immer original.call(...) auf, es sei denn, das Überspringen des Aufrufs ist das Feature. Wenn du es nicht aufrufst, deaktivierst du stillschweigend jeden anderen Mod-Wrapper auf demselben Aufruf.

@WrapWithCondition – Bedingt einen void-Aufruf überspringen

@WrapWithCondition(
    method = "render",
    at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, Aufruf-Argumente... */) {
    return !ExampleConfig.get().hideThing;   // false = Aufruf überspringen
}

@Local – Eine lokale Variable lesen ohne @ModifyVariable

@Inject(method = "tick", at = @At(value = "INVOKE", target = "..."))
private void examplemod$readLocal(CallbackInfo ci, @Local(name = "pos") BlockPos pos) { ... }

Ab 26.1+ liefert das Spiel Parameter- und lokale Namen, daher ist @Local(name = ...) dort zuverlässig. Vor 26.1 (keine lokalen Namen in obfuskierten JARs) bevorzuge @Local(ordinal = n) nach Typ und überprüfe es mit mixin.debug.export.

Andere nützliche MixinExtras-Werkzeuge

@ModifyReturnValue (ändere den Rückgabewert einer Methode bei RETURN, verkettet mit anderen Mods), @ModifyReceiver, @WrapMethod (umhülle eine gesamte Methode), @Share (übergebe einen Wert zwischen zwei Injektoren in derselben Methode), @Cancellable (breche von innerhalb einer @WrapOperation ab).

@Redirect und @ModifyVariable – Nur wenn nichts anderes passt

  • @Redirect ersetzt einen Aufruf. Nur ein Mod kann einen bestimmten Aufruf redirecten; ein zweiter Mods @Redirect (oder ein @WrapOperation, das in manchen Reihenfolgen auf einen redirecteten Aufruf angewendet wird) führt zu einem harten Absturz. Verwende stattdessen @WrapOperation.
  • @ModifyVariable zielt auf eine lokale Variable nach Typ und ordinal, was sich zwischen den Versionen ändert. Verwende @ModifyExpressionValue auf den Ausdruck, der den Wert produziert, oder @Local, um ihn zu lesen.
  • @Overwrite ersetzt die gesamte Methode: inkompatibel mit jedem anderen Mod, der sie berührt. Verwende es nicht in einem öffentlichen Mod.

Accessoren und Invoker

package com.example.examplemod.mixin.accessor;

@Mixin(LivingEntity.class)
public interface LivingEntityAccessor {
    @Accessor("noJumpDelay")
    int examplemod$getNoJumpDelay();

    @Accessor("noJumpDelay")
    void examplemod$setNoJumpDelay(int value);

    @Invoker("getJumpPower")
    float examplemod$invokeGetJumpPower();
}

// Verwendung
int delay = ((LivingEntityAccessor) entity).examplemod$getNoJumpDelay();
  • Instanz-Accessor-Methoden erhalten das Mod-ID-Präfix; statische (static T getX() { throw new AssertionError(); }) benötigen es nicht.
  • @Mutable bei einem Setter entfernt final vom Zielfeld.
  • Constructor-Invoker: @Invoker("<init>") static Foo examplemod$new(...).
  • Accessoren funktionieren auch bei Klassen anderer Mods; Class-Tweaker funktionieren nur bei Vanilla.

Class-Tweaker (früher Access Wideners)

Erforderlich, um eine final-Klasse abzuleiten, eine final-Methode zu überschreiben oder eine nicht-öffentliche Klasse zu referenzieren. Erfordert Loader 0.18+ und Loom 1.12+; ältere Setups verwenden dieselbe Syntax unter dem Namen "Access Widener" mit einem accessWidener v2 named-Header.

src/main/resources/examplemod.classtweaker:

classTweaker v1 official
accessible  class   net/minecraft/world/inventory/MenuType$MenuSupplier
accessible  method  net/minecraft/network/chat/TextColor  formatValue  ()Ljava/lang/String;
accessible  field   net/minecraft/world/damagesource/DamageSources  damageTypes  Lnet/minecraft/core/Registry;

Verkabelung an beiden Stellen vornehmen, dann Gradle aktualisieren und genSources erneut ausführen:

loom { accessWidenerPath = file("src/main/resources/examplemod.classtweaker") }
"accessWidener": "examplemod.classtweaker"

Führe ./gradlew validateAccessWidener aus – ungültige Einträge werden sonst stillschweigend ignoriert. Verwende transitive--Direktiven nur, wenn von deinem Mod abhängige Mods die Änderung sehen müssen. Header-Details, v2-Features (Interface-Injektion, Enum-Erweiterung): https://docs.fabricmc.net/develop/class-tweakers/

Checkliste für modulübergreifende Kompatibilität

  • Injiziere am schmalsten funktionierenden Punkt; breche niemals bei HEAD einer weit verbreiteten Methode ab (tick, render, hurt, travel), es sei denn, du bist sicher.
  • Bevorzuge Injektoren, die sich verketten (@ModifyExpressionValue, @ModifyReturnValue, @WrapOperation) gegenüber Injektoren, die ersetzen (@Redirect, @Overwrite).
  • Verwende @Unique + Mod-ID-Präfix für allen hinzugefügten Zustand.
  • Vermeide ordinal und @At("TAIL"), wenn @At("RETURN") funktioniert.
  • Verwende @Mixin(value = X.class, priority = ...) nur, um einen bekannten Konflikt zu lösen; dokumentiere ihn.
  • Optionale Mixins, die auf einen anderen Mod abzielen: platziere sie in einer separaten Konfiguration und schalte sie mit einem Mixin-Plugin frei (IMixinConfigPlugin#shouldApplyMixin prüft FabricLoader.getInstance().isModLoaded("othermod")), oder verwende "require": 0 für diesen Injektor.
  • Bekannte starke Patcher (Sodium, Iris, Lithium, ImmediatelyFast) ersetzen große Teile des Renderers und der Spiel-Logik. Wenn du in Rendering oder Entity-Ticking injizierst, teste mit ihnen.

Debugging von Mixins

Füge JVM-Argumente zu den Dev-Run-Konfigurationen hinzu:

loom {
    runs {
        configureEach {
            vmArg "-Dmixin.debug.export=true"     // schreibt transformierte Klassen nach run/.mixin.out/
            vmArg "-Dmixin.debug.verbose=true"
            vmArg "-Dmixin.dumpTargetOnFailure=true"
        }
    }
}
  • run/.mixin.out/class/... enthält die transformierten Klassen – dekompiliere sie, um genau zu sehen, was dein Injektor produziert hat und in welcher Reihenfolge die Injektoren anderer Mods ausgeführt wurden.
  • -Dmixin.checks.interfaces=true überprüft, ob Mixins Interface-Methoden implementieren.
  • Fehler bei der Decodierung (Ziel nicht gefunden, InvalidInjectionException, widersprüchliche Redirects): siehe das fabric-crash-debugging-Skill.

Häufige Fehler

  • Ein Mixin für etwas schreiben, für das Fabric API bereits ein Ereignis hat (HUD, Ticks, Block-Nutzung, Tooltips, Befehle, Loot-Tabellen).
  • Einen Methodennamen oder Deskriptor aus dem Gedächtnis oder einer anderen Version/Mappings erraten. Überprüfe immer zuerst generierte Quellen.
  • Yarn-Namen (MinecraftClient, PlayerEntity) in einem Mojang-named-Projekt verwenden oder umgekehrt.
  • @Redirect wo @WrapOperation/@ModifyExpressionValue ausreichen würden; @Overwrite in einem öffentlichen Mod.
  • cancellable = true + ci.cancel() bei HEAD als Standardmethode, um Verhalten zu "ändern".
  • Vergessen, original.call(...) in @WrapOperation aufzurufen.
  • Falscher CallbackInfoReturnable-Generic (primitiv statt boxed), oder das Fehlen der Zielparameter vor ci, wenn man sie braucht.
  • Mixin-Klasse nicht in der Konfiguration gelistet, im falschen Abschnitt gelistet (mixins vs client), oder in einem anderen Paket als dem package der Konfiguration.
  • Client-Ziele in der gemeinsamen Konfiguration: Dedizierte Server stürzen beim Start ab.
  • Eine Mixin-Klasse direkt von normalem Code referenzieren (new LivingEntityMixin()) oder Nicht-Mixin-Hilfsklassen in das Mixin-Paket legen.
  • Nicht-@Unique, nicht-präfixierte Felder, die mit dem Mixin eines anderen Mods kollidieren.
  • defaultRequire ausschalten, um ein fehlschlagendes Mixin zu "reparieren" – das versteckt den Fehler und liefert einen Mod aus, der stillschweigend nichts tut.
  • Class-Tweaker nur in einer der Dateien build.gradle / fabric.mod.json deklariert oder nicht validiert.

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Verwandte Skills