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
- É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 packageapide chaque module) et la documentation Fabric. Un événement est stable à travers les versions et se compose gratuitement avec d'autres mods. - 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.
- Mixin accesseur / invokeur - lorsque vous avez seulement besoin de lire/écrire un champ privé ou d'appeler une méthode privée.
- 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é.
- 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": 1fait 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 (danssrc/client/resources, déclarées avec"environment": "client"dansfabric.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);
}
}
CallbackInfopour les ciblesvoid,CallbackInfoReturnable<T>sinon (type boxé).- Déclarez
cancellable = trueseulement lorsque vous annulez réellement. Annuler àHEADsaute 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@Injectpuisse 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
@Atcourantes :HEAD,RETURN(chaque retour),TAIL(dernier retour seulement),INVOKE(avant un appel; ajoutezshift = At.Shift.AFTERpour après),FIELD,NEW. - Utilisez
ordinalseulement 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ùRest le type de retour boxé (Voidpourvoid). - 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
@Redirectremplace un appel. Un seul mod peut rediriger un appel donné ; un second mod avec@Redirect(ou un@WrapOperationappliqué par-dessus un appel redirigé dans certains ordres) échoue avec un crash dur. Utilisez@WrapOperationà la place.@ModifyVariablecible une locale par type et ordinal, ce qui change entre les versions. Utilisez@ModifyExpressionValuesur l'expression qui produit la valeur, ou@Localpour la lire.@Overwriteremplace 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. @Mutablesur un setter retirefinaldu 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 à
HEADd'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
ordinalet@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#shouldApplyMixinvérifiantFabricLoader.getInstance().isModLoaded("othermod")), ou utilisez"require": 0sur 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=truevérifie que les mixins implémentent les méthodes d'interface.- Erreur de décodage (cible non trouvée,
InvalidInjectionException, redirections conflictuelles) : voir le skillfabric-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. @Redirectlà où@WrapOperation/@ModifyExpressionValueferait l'affaire ;@Overwritedans un mod public.cancellable = true+ci.cancel()àHEADcomme 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 avantcilorsque vous en avez besoin. - Classe de mixin non listée dans la configuration, listée dans la mauvaise section (
mixinsvsclient), ou dans un package différent dupackagede 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
defaultRequirepour "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é.