Fabric mixins
Los Mixins parchean el bytecode de Minecraft en tiempo de carga. Son potentes y frágiles: cada mixin es una promesa de que un método específico, con una forma específica, aún existe en la siguiente versión y que ningún otro mod lo cambia de forma incompatible. Escribe lo menos posible, lo más estrecho posible.
Orden de decisión - prueba estas opciones antes de escribir un mixin
- Evento o registro de Fabric API. Busca en Fabric API primero: eventos del ciclo de vida
(
ServerLifecycleEvents,ClientTickEvents), eventos de jugador (UseBlockCallback,AttackEntityCallback,PlayerBlockBreakEvents), HUD (HudElementRegistry), pantallas (ScreenEvents), botín (LootTableEvents), comandos (CommandRegistrationCallback), redes, tooltips de objetos, listeners de recarga de recursos, y muchos más. Navega por https://github.com/FabricMC/fabric-api (el paqueteapide cada módulo) y los documentos de Fabric. Un evento es estable entre versiones y se compone con otros mods gratuitamente. - Puntos de extensión vanilla. Contenido basado en datos (tags, tablas de botín, recetas, componentes de datos, registros) a menudo no necesita parcheo de código.
- Accessor / Invoker mixin - cuando solo necesitas leer/escribir un campo privado o llamar a un método privado.
- Class tweaker (access widener) - cuando necesitas subclasificar una clase final, sobrescribir un método final, o nombrar una clase privada/paquete-privada.
- Injector mixin - solo ahora. Prefiere los inyectores de MixinExtras sobre
@Redirect/@Overwrite.
Escribe (en un comentario encima del mixin) por qué los pasos 1-4 no funcionaron. Los revisores preguntarán.
Antes de escribir cualquier inyector: confirma el objetivo
Los nombres y las firmas cambian en cada versión de Minecraft. Nunca escribas un objetivo de memoria.
./gradlew genSources # luego abre la clase en el IDE o busca en los fuentes generados
- 26.1+ (sin ofuscar): apunta a los nombres de Mojang exactamente como aparecen en los fuentes.
- Pre-26.1: apunta a los nombres de los mapeados de tu proyecto (Yarn o Mojang). El procesador de anotaciones de mixin de Loom los remapea a intermediary en tiempo de compilación; una discrepancia aparece como una advertencia "Cannot remap" en tiempo de compilación - trata esa advertencia como un error.
- Copia los descriptores de método desde la vista de bytecode o una herramienta (plugin MCDev IntelliJ, mcsrc.dev), no a mano. Referencia de descriptores: https://docs.fabricmc.net/develop/mixins/bytecode
Configuración de mixin y convenciones
{
"required": true,
"package": "com.example.examplemod.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
"client": [],
"server": [],
"injectors": { "defaultRequire": 1 },
"overwrites": { "requireAnnotations": true }
}
"defaultRequire": 1hace que un mixin que no coincide con nada falle de forma ruidosa en lugar de no hacer nada en silencio. Mantenlo.- Los objetivos solo de cliente (
net.minecraft.client.*) van en la configuración de cliente (ensrc/client/resources, declarado con"environment": "client"enfabric.mod.json). - Las clases mixin nunca se referencian desde código normal (excepto las interfaces accessor, mediante un cast).
- Prefija cada miembro añadido con tu mod id para evitar colisiones con otros mods:
examplemod$onTick,@Unique private int examplemod$counter;. - Marca los campos/métodos auxiliares que añadas con
@Unique. - Los métodos handler son
private(no estáticos si el objetivo no es estático).
Los objetivos en los ejemplos a continuación usan nombres de Mojang y muestran la forma de cada inyector. Los nombres de método y los descriptores cambian entre versiones - verifica cada uno de ellos en tus fuentes generados antes de reutilizarlo.
@Inject - ejecuta código en un punto, opcionalmente cancela
@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á sobrecargado (getJumpPower() y getJumpPower(float)): proporciona el descriptor.
@Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(cir.getReturnValueF() * 1.5f);
}
}
CallbackInfopara objetivosvoid,CallbackInfoReturnable<T>en caso contrario (tipo empaquetado).- Solo declara
cancellable = truecuando realmente canceles. Cancelar enHEADsalta el método vanilla y la inyección de todos los demás mods después del tuyo - es lo menos compatible que un@Injectpuede hacer. Prefiere modificar un valor. - Para leer los parámetros del objetivo, añádelos a la firma del handler antes del
CallbackInfo, en orden:(float amount, CallbackInfo ci). - Valores comunes de
@At:HEAD,RETURN(cada retorno),TAIL(solo el último retorno),INVOKE(antes de una llamada; añadeshift = At.Shift.AFTERpara después),FIELD,NEW. - Usa
ordinalsolo cuando sea necesario; se rompe tan pronto como Mojang añada otra llamada coincidente.
MixinExtras - usa estos en lugar de @Redirect y la mayoría de @ModifyVariable
MixinExtras viene incluido en Fabric Loader (0.15+), por lo que no se necesita dependencia adicional. Las importaciones están
bajo com.llamalad7.mixinextras. Documentos: https://github.com/LlamaLad7/MixinExtras/wiki
@ModifyExpressionValue - cambia lo que devuelve una llamada/lectura de campo
@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;
}
Varios mods pueden encadenar @ModifyExpressionValue en la misma expresión; cada uno recibe el
resultado del mod anterior. Un @Redirect en la misma llamada entraría en conflicto y fallaría.
@WrapOperation - envuelve una llamada; llama a la original (o no)
@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("{} took {} damage", self, amount);
original.call(self, level, source, amount); // mantén la cadena intacta
}
- Parámetros: el receptor (para llamadas no estáticas), luego los argumentos de la llamada, luego
Operation<R>dondeRes el tipo de retorno empaquetado (Voidparavoid). - Siempre llama a
original.call(...)a menos que saltarse la llamada sea la característica. No llamarla deshabilita silenciosamente el envoltorio de todos los demás mods en la misma llamada.
@WrapWithCondition - salta condicionalmente una llamada void
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, argumentos de llamada... */) {
return !ExampleConfig.get().hideThing; // false = saltar la llamada
}
@Local - lee una variable local sin @ModifyVariable
@Inject(method = "tick", at = @At(value = "INVOKE", target = "..."))
private void examplemod$readLocal(CallbackInfo ci, @Local(name = "pos") BlockPos pos) { ... }
En 26.1+ el juego incluye nombres de parámetros y locales, por lo que @Local(name = ...) es fiable allí.
Pre-26.1 (sin nombres locales en jars ofuscados) prefiere @Local(ordinal = n) por tipo, y compruébalo
con mixin.debug.export.
Otras herramientas útiles de MixinExtras
@ModifyReturnValue (cambia el valor de retorno de un método en RETURN, se encadena con otros mods),
@ModifyReceiver, @WrapMethod (envuelve un método completo), @Share (pasa un valor entre dos
inyectores en el mismo método), @Cancellable (cancela desde dentro de un @WrapOperation).
@Redirect y @ModifyVariable - solo cuando nada más encaja
@Redirectreemplaza una llamada. Solo un mod puede redirigir una llamada dada; el@Redirectde un segundo mod (o un@WrapOperationaplicado sobre una llamada redirigida en algunos órdenes) falla con un error grave. Usa@WrapOperationen su lugar.@ModifyVariableapunta a una local por tipo y ordinal, lo que cambia entre versiones. Usa@ModifyExpressionValueen la expresión que produce el valor, o@Localpara leerlo.@Overwritereemplaza el método completo: es incompatible con cualquier otro mod que lo toque. No lo uses en un mod público.
Accessors e invokers
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();
}
// uso
int delay = ((LivingEntityAccessor) entity).examplemod$getNoJumpDelay();
- Los métodos accessor de instancia obtienen el prefijo del mod id; los estáticos (
static T getX() { throw new AssertionError(); }) no lo necesitan. @Mutableen un setter eliminafinaldel campo objetivo.- Invoker de constructor:
@Invoker("<init>") static Foo examplemod$new(...). - Los accessors también funcionan en clases de otros mods; los class tweakers solo funcionan en vanilla.
Class tweakers (anteriormente access wideners)
Necesario para subclasificar una clase final, sobrescribir un método final, o hacer referencia a una clase no pública.
Requiere Loader 0.18+ y Loom 1.12+; configuraciones más antiguas usan la misma sintaxis bajo el nombre
"access widener" con un encabezado 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;
Conéctalo en ambos lugares, luego actualiza Gradle y vuelve a ejecutar genSources:
loom { accessWidenerPath = file("src/main/resources/examplemod.classtweaker") }
"accessWidener": "examplemod.classtweaker"
Ejecuta ./gradlew validateAccessWidener - las entradas inválidas se ignoran silenciosamente. Usa
las directivas transitive- solo cuando los mods que dependen del tuyo deban ver el cambio. Detalles del encabezado,
características de v2 (inyección de interfaces, extensión de enums): https://docs.fabricmc.net/develop/class-tweakers/
Lista de verificación de compatibilidad entre mods
- Inyecta en el punto más estrecho que funcione; nunca canceles en
HEADde un método ampliamente usado (tick,render,hurt,travel) a menos que estés seguro. - Prefiere inyectores que se encadenen (
@ModifyExpressionValue,@ModifyReturnValue,@WrapOperation) sobre inyectores que reemplacen (@Redirect,@Overwrite). - Usa
@Unique+ prefijo del mod id para todo el estado añadido. - Evita
ordinaly@At("TAIL")cuando@At("RETURN")funcione. - Usa
@Mixin(value = X.class, priority = ...)solo para resolver un conflicto conocido; documéntalo. - Mixins opcionales que apunten a otro mod: colócalos en una configuración separada y condicionalos con un
plugin de mixin (
IMixinConfigPlugin#shouldApplyMixinverificandoFabricLoader.getInstance().isModLoaded("othermod")), o usa"require": 0en ese inyector. - Parcheadores pesados conocidos (Sodium, Iris, Lithium, ImmediatelyFast) reemplazan grandes partes del renderizador y la lógica del juego. Si inyectas en renderizado o tick de entidades, prueba con ellos.
Depuración de mixins
Añade argumentos JVM a las configuraciones de ejecución de desarrollo:
loom {
runs {
configureEach {
vmArg "-Dmixin.debug.export=true" // escribe las clases transformadas en run/.mixin.out/
vmArg "-Dmixin.debug.verbose=true"
vmArg "-Dmixin.dumpTargetOnFailure=true"
}
}
}
run/.mixin.out/class/...contiene las clases transformadas - descompílalas para ver exactamente lo que produjo tu inyector y en qué orden se ejecutaron los inyectores de otros mods.-Dmixin.checks.interfaces=trueverifica que los mixins implementen métodos de interfaz.- Error de decodificación (objetivo no encontrado,
InvalidInjectionException, redirecciones conflictivas): consulta el skillfabric-crash-debugging.
Errores comunes
- Escribir un mixin para algo para lo que Fabric API ya tiene un evento (HUD, ticks, uso de bloques, tooltips, comandos, tablas de botín).
- Adivinar un nombre de método o descriptor de memoria o de una versión diferente/conjunto de mapeados distintas. Siempre verifica primero los fuentes generados.
- Usar nombres Yarn (
MinecraftClient,PlayerEntity) en un proyecto con nombres Mojang o viceversa. @Redirectdonde@WrapOperation/@ModifyExpressionValueservirían;@Overwriteen un mod público.cancellable = true+ci.cancel()enHEADcomo la forma predeterminada de "cambiar" el comportamiento.- Olvidar
original.call(...)en@WrapOperation. CallbackInfoReturnablegenérico incorrecto (primitivo en lugar de empaquetado), o faltan los parámetros del objetivo antes decicuando los necesitas.- Clase mixin no listada en la configuración, listada en la sección incorrecta (
mixinsvsclient), o en un paquete diferente alpackagede la configuración. - Objetivos de cliente en la configuración común: hace que los servidores dedicados fallen al iniciar.
- Referenciar una clase mixin directamente desde código normal (
new LivingEntityMixin()), o poner clases auxiliares que no son mixin dentro del paquete de mixin. - Campos no
@Uniquey sin prefijo que colisionan con el mixin de otro mod. - Desactivar
defaultRequirepara "arreglar" un mixin que falla - esto oculta el error y entrega un mod que silenciosamente no hace nada. - Class tweaker declarado solo en uno de
build.gradle/fabric.mod.json, o no validado.