vvs1off/fabric-mixins

Écrire, réviser ou corriger des mixins dans un mod Fabric : choisir entre un événement Fabric API et un mixin, @Inject, @Redirect, @ModifyVariable, les injecteurs plus sûrs de MixinExtras (@WrapOperation, @ModifyExpressionValue, @WrapWithCondition, @Local), les accesseurs/invokeurs, les class tweakers (access wideners), la compatibilité cross-mod et le débogage avec mixin.debug.export. À utiliser dès qu'une tâche nécessite de modifier le comportement vanilla, de lire des champs privés, ou lorsqu'un mixin ne s'applique pas.

Qu'est-ce que fabric-mixins ?

Ce skill fournit un guide complet pour l'écriture de mixins Fabric, en mettant l'accent sur la sécurité et la maintenabilité. Il couvre le processus de décision pour choisir entre un événement Fabric API, un accesseur, un class tweaker ou un injecteur, en privilégiant toujours les solutions les moins intrusives. Le skill détaille l'utilisation des injecteurs standards (@Inject, @Redirect, @ModifyVariable) et des alternatives plus sûres de MixinExtras (@WrapOperation, @ModifyExpressionValue, @WrapWithCondition, @Local) qui permettent le chaînage entre mods sans conflit. Il inclut également les bonnes pratiques pour la configuration des mixins, les accesseurs et invokeurs, les class tweakers (access wideners), la compatibilité cross-mod (notamment avec Sodium, Iris, Lithium), et le débogage via l'export des classes transformées. Le skill est conçu pour les développeurs de mods Fabric utilisant Claude Code, Cursor, Codex ou d'autres assistants IA, sur n'importe quelle version de Minecraft, en précisant comment gérer les différences de mappings (Mojang vs Yarn).

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Mixins Fabric

Les mixins patch le bytecode de Minecraft au chargement. Ils sont puissants et fragiles : chaque mixin est une promesse qu'une méthode spécifique, avec une forme spécifique, existe encore dans la version suivante et qu'aucun autre mod ne la modifie de manière incompatible. Écrivez-en le moins possible, aussi étroits que possible.

Ordre de décision - essayez ceci avant d'écrire un mixin

  1. Événement ou registre Fabric API. Cherchez d'abord dans Fabric API : événements de cycle de vie (ServerLifecycleEvents, ClientTickEvents), événements joueur (UseBlockCallback, AttackEntityCallback, PlayerBlockBreakEvents), HUD (HudElementRegistry), écrans (ScreenEvents), butin (LootTableEvents), commandes (CommandRegistrationCallback), réseau, infobulles d'objets, écouteurs de rechargement de ressources, et bien d'autres. Parcourez https://github.com/FabricMC/fabric-api (le package api de chaque module) et la documentation Fabric. Un événement est stable à travers les versions et se compose gratuitement avec d'autres mods.
  2. Points d'extension vanilla. Le contenu piloté par les données (tags, tables de butin, recettes, composants de données, registres) ne nécessite souvent aucune modification de code.
  3. Mixin accesseur / invokeur - lorsque vous avez seulement besoin de lire/écrire un champ privé ou d'appeler une méthode privée.
  4. Class tweaker (access widener) - lorsque vous avez besoin de sous-classer une classe finale, de redéfinir une méthode finale, ou de nommer une classe privée/package-privé.
  5. Mixin injecteur - seulement maintenant. Préférez les injecteurs MixinExtras à @Redirect/@Overwrite.

Notez (dans un commentaire au-dessus du mixin) pourquoi les étapes 1 à 4 n'ont pas fonctionné. Les relecteurs le demanderont.

Avant d'écrire un injecteur : confirmer la cible

Les noms et signatures changent à chaque version de Minecraft. N'écrivez jamais une cible de mémoire.

./gradlew genSources        # puis ouvrez la classe dans l'IDE ou cherchez dans les sources générées
  • 26.1+ (non obscurci): ciblez les noms Mojang exactement comme ils apparaissent dans les sources.
  • Pré-26.1 : ciblez les noms des mappings de votre projet (Yarn ou Mojang). Le processeur d'annotations de mixin de Loom les remappe vers intermédiaire à la compilation ; une discordance apparaît comme un avertissement "Cannot remap" à la compilation - traitez cet avertissement comme une erreur.
  • Copiez les descripteurs de méthodes depuis la vue bytecode ou un outil (plugin MCDev IntelliJ, mcsrc.dev), pas à la main. Référence des descripteurs : https://docs.fabricmc.net/develop/mixins/bytecode

Configuration et conventions des mixins

{
  "required": true,
  "package": "com.example.examplemod.mixin",
  "compatibilityLevel": "JAVA_25",
  "mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
  "client": [],
  "server": [],
  "injectors": { "defaultRequire": 1 },
  "overwrites": { "requireAnnotations": true }
}
  • "defaultRequire": 1 fait qu'un mixin qui ne correspond à rien échoue bruyamment au lieu de ne rien faire silencieusement. Gardez-le.
  • Les cibles côté client (net.minecraft.client.*) vont dans la configuration client (dans src/client/resources, déclarées avec "environment": "client" dans fabric.mod.json).
  • Les classes de mixin ne sont jamais référencées depuis le code normal (sauf les interfaces accesseurs, via un cast).
  • Préfixez chaque membre ajouté avec votre identifiant de mod pour éviter les collisions avec d'autres mods : examplemod$onTick, @Unique private int examplemod$counter;.
  • Marquez les champs/méthodes auxiliaires que vous ajoutez avec @Unique.
  • Les méthodes de gestion sont private (non-statiques si la cible est non-statique).

Les cibles dans les exemples ci-dessous utilisent les noms Mojang et montrent la forme de chaque injecteur. Les noms de méthodes et les descripteurs dérivent entre les versions - revérifiez chacun d'eux dans vos sources générées avant de les réutiliser.

@Inject - exécuter du code à un point, éventuellement annuler

@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 est surchargé (getJumpPower() et getJumpPower(float)) : donnez le descripteur.
    @Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
    private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
        cir.setReturnValue(cir.getReturnValueF() * 1.5f);
    }
}
  • CallbackInfo pour les cibles void, CallbackInfoReturnable<T> sinon (type boxé).
  • Déclarez cancellable = true seulement lorsque vous annulez réellement. Annuler à HEAD saute la méthode vanilla et toutes les injections des autres mods après la vôtre - c'est la chose la moins compatible qu'un @Inject puisse faire. Préférez modifier une valeur.
  • Pour lire les paramètres de la cible, ajoutez-les à la signature du gestionnaire avant le CallbackInfo, dans l'ordre : (float amount, CallbackInfo ci).
  • Valeurs @At courantes : HEAD, RETURN (chaque retour), TAIL (dernier retour seulement), INVOKE (avant un appel; ajoutez shift = At.Shift.AFTER pour après), FIELD, NEW.
  • Utilisez ordinal seulement en dernier recours ; cela casse dès que Mojang ajoute un autre appel correspondant.

MixinExtras - utilisez-les à la place de @Redirect et de la plupart des @ModifyVariable

MixinExtras est inclus dans Fabric Loader (0.15+), donc aucune dépendance supplémentaire n'est nécessaire. Les imports se font sous com.llamalad7.mixinextras. Documentation : https://github.com/LlamaLad7/MixinExtras/wiki

@ModifyExpressionValue - modifier ce qu'un appel/lecture de champ retourne

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

Plusieurs mods peuvent chaîner @ModifyExpressionValue sur la même expression ; chacun reçoit le résultat du mod précédent. Un @Redirect sur le même appel entrerait en conflit et planterait.

@WrapOperation - encapsuler un appel ; appeler l'original (ou pas)

@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("{} a subi {} points de dégâts", self, amount);
    original.call(self, level, source, amount);   // garder la chaîne intacte
}
  • Paramètres : le receveur (pour les appels non-statiques), puis les arguments de l'appel, puis Operation<R> où R est le type de retour boxé (Void pour void).
  • Appelez toujours original.call(...) sauf si sauter l'appel est la fonctionnalité. Ne pas l'appeler désactive silencieusement tous les autres wrappers de mods sur le même appel.

@WrapWithCondition - sauter conditionnellement un appel void

@WrapWithCondition(
    method = "render",
    at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, arguments de l'appel... */) {
    return !ExampleConfig.get().hideThing;   // false = sauter l'appel
}

@Local - lire une variable locale sans @ModifyVariable

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

En 26.1+ le jeu fournit les noms des paramètres et des variables locales, donc @Local(name = ...) est fiable. Pré-26.1 (pas de noms locaux dans les jars obscurcis) préférez @Local(ordinal = n) par type, et vérifiez-le avec mixin.debug.export.

Autres outils MixinExtras utiles

@ModifyReturnValue (modifier la valeur de retour d'une méthode à RETURN, chaîne avec d'autres mods), @ModifyReceiver, @WrapMethod (encapsuler une méthode entière), @Share (passer une valeur entre deux injecteurs dans la même méthode), @Cancellable (annuler depuis l'intérieur d'un @WrapOperation).

@Redirect et @ModifyVariable - seulement quand rien d'autre ne convient

  • @Redirect remplace un appel. Un seul mod peut rediriger un appel donné ; un second mod avec @Redirect (ou un @WrapOperation appliqué par-dessus un appel redirigé dans certains ordres) échoue avec un crash dur. Utilisez @WrapOperation à la place.
  • @ModifyVariable cible une locale par type et ordinal, ce qui change entre les versions. Utilisez @ModifyExpressionValue sur l'expression qui produit la valeur, ou @Local pour la lire.
  • @Overwrite remplace la méthode entière : incompatible avec tout autre mod qui la touche. Ne l'utilisez pas dans un mod public.

Accesseurs et invokeurs

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();
}

// utilisation
int delay = ((LivingEntityAccessor) entity).examplemod$getNoJumpDelay();
  • Les méthodes accesseurs d'instance reçoivent le préfixe de l'identifiant du mod ; les méthodes statiques (static T getX() { throw new AssertionError(); }) n'en ont pas besoin.
  • @Mutable sur un setter retire final du champ cible.
  • Invokeur de constructeur : @Invoker("<init>") static Foo examplemod$new(...).
  • Les accesseurs fonctionnent aussi sur les classes d'autres mods ; les class tweakers fonctionnent seulement sur le vanilla.

Class tweakers (anciennement access wideners)

Nécessaire pour sous-classer une classe finale, redéfinir une méthode finale, ou référencer une classe non publique. Requiert Loader 0.18+ et Loom 1.12+ ; les configurations plus anciennes utilisent la même syntaxe sous le nom "access widener" avec un en-tête accessWidener v2 named.

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;

Configurez-le dans les deux endroits, puis rafraîchissez Gradle et réexécutez genSources :

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

Exécutez ./gradlew validateAccessWidener - les entrées invalides sont sinon ignorées silencieusement. Utilisez les directives transitive- seulement lorsque les mods dépendant du vôtre doivent voir le changement. Détails de l'en-tête, fonctionnalités v2 (injection d'interface, extension d'énumération) : https://docs.fabricmc.net/develop/class-tweakers/

Liste de vérification de la compatibilité cross-mod

  • Injectez au point le plus étroit qui fonctionne ; n'annulez jamais à HEAD d'une méthode largement utilisée (tick, render, hurt, travel) sauf si vous êtes sûr.
  • Préférez les injecteurs qui chaînent (@ModifyExpressionValue, @ModifyReturnValue, @WrapOperation) aux injecteurs qui remplacent (@Redirect, @Overwrite).
  • Utilisez @Unique + préfixe d'identifiant de mod pour tout état ajouté.
  • Évitez ordinal et @At("TAIL") quand @At("RETURN") fonctionne.
  • Utilisez @Mixin(value = X.class, priority = ...) seulement pour résoudre un conflit connu ; documentez-le.
  • Mixins optionnels ciblant un autre mod : placez-les dans une configuration séparée et conditionnez-les avec un plugin de mixin (IMixinConfigPlugin#shouldApplyMixin vérifiant FabricLoader.getInstance().isModLoaded("othermod")), ou utilisez "require": 0 sur cet injecteur.
  • Les mods lourds connus (Sodium, Iris, Lithium, ImmediatelyFast) remplacent de grandes parties du rendu et de la logique du jeu. Si vous injectez dans le rendu ou le ticking d'entités, testez avec eux.

Débogage des mixins

Ajoutez des arguments JVM aux configurations d'exécution de développement :

loom {
    runs {
        configureEach {
            vmArg "-Dmixin.debug.export=true"     // écrit les classes transformées dans run/.mixin.out/
            vmArg "-Dmixin.debug.verbose=true"
            vmArg "-Dmixin.dumpTargetOnFailure=true"
        }
    }
}
  • run/.mixin.out/class/... contient les classes transformées - décompilez-les pour voir exactement ce que votre injecteur a produit et dans quel ordre les injecteurs des autres mods ont été exécutés.
  • -Dmixin.checks.interfaces=true vérifie que les mixins implémentent les méthodes d'interface.
  • Erreur de décodage (cible non trouvée, InvalidInjectionException, redirections conflictuelles) : voir le skill fabric-crash-debugging.

Erreurs courantes

  • Écrire un mixin pour quelque chose pour lequel Fabric API a déjà un événement (HUD, ticks, utilisation de bloc, infobulles, commandes, tables de butin).
  • Deviner un nom de méthode ou un descripteur de mémoire ou d'une version/ensemble de mappings différents. Vérifiez toujours les sources générées d'abord.
  • Utiliser des noms Yarn (MinecraftClient, PlayerEntity) dans un projet nommé Mojang ou vice versa.
  • @Redirect là où @WrapOperation/@ModifyExpressionValue ferait l'affaire ; @Overwrite dans un mod public.
  • cancellable = true + ci.cancel() à HEAD comme méthode par défaut pour "modifier" le comportement.
  • Oublier original.call(...) dans @WrapOperation.
  • Mauvais générique CallbackInfoReturnable (primitif au lieu de boxé), ou paramètres manquants de la cible avant ci lorsque vous en avez besoin.
  • Classe de mixin non listée dans la configuration, listée dans la mauvaise section (mixins vs client), ou dans un package différent du package de la configuration.
  • Cibles client dans la configuration commune : plante les serveurs dédiés au démarrage.
  • Référencer une classe de mixin directement depuis le code normal (new LivingEntityMixin()), ou mettre des classes auxiliaires non-mixin dans le package de mixin.
  • Champs non-@Unique, non préfixés qui entrent en collision avec un autre mixin d'un autre mod.
  • Désactiver defaultRequire pour "corriger" un mixin qui échoue - cela cache le bug et livre un mod qui ne fait rien silencieusement.
  • Class tweaker déclaré dans un seul des build.gradle / fabric.mod.json, ou non validé.

Individual skills in this repo

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

Skills associés