CommunityProgramación y desarrollogithub.com

vvs1off/fabric-mixins

Escribe, revisa o corrige Mixins en un mod de Fabric, eligiendo entre un evento de Fabric API y un mixin, @Inject, @Redirect, @ModifyVariable, los inyectores más seguros de MixinExtras (@WrapOperation, @ModifyExpressionValue, @WrapWithCondition, @Local), accessors/invokers, class tweakers (access wideners), compatibilidad entre mods y depuración con mixin.debug.export. Úsalo cada vez que una tarea necesite cambiar el comportamiento vanilla, leer campos privados, o cuando un mixin no se aplique.

¿Qué es fabric-mixins?

Este skill proporciona una guía completa y autorizada para trabajar con Mixins en mods de Fabric para Minecraft. Cubre todo el proceso, desde la decisión de cuándo usar un mixin (priorizando eventos de Fabric API, puntos de extensión vanilla, accessors y class tweakers) hasta la implementación de inyectores, incluyendo prácticas recomendadas como el uso de MixinExtras para evitar conflictos. Explica en detalle los diferentes tipos de inyectores (@Inject, @ModifyExpressionValue, @WrapOperation, @WrapWithCondition, @Local), accessors, invokers y class tweakers, con ejemplos de código en Java y configuraciones JSON. También incluye una lista de verificación para la compatibilidad entre mods y técnicas de depuración con mixin.debug.export. Es útil para cualquier desarrollador de mods de Fabric que necesite modificar el comportamiento de Minecraft de forma segura y mantenible.

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

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

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

  1. 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 paquete api de cada módulo) y los documentos de Fabric. Un evento es estable entre versiones y se compone con otros mods gratuitamente.
  2. 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.
  3. Accessor / Invoker mixin - cuando solo necesitas leer/escribir un campo privado o llamar a un método privado.
  4. Class tweaker (access widener) - cuando necesitas subclasificar una clase final, sobrescribir un método final, o nombrar una clase privada/paquete-privada.
  5. 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": 1 hace 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 (en src/client/resources, declarado con "environment": "client" en fabric.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);
    }
}
  • CallbackInfo para objetivos void, CallbackInfoReturnable<T> en caso contrario (tipo empaquetado).
  • Solo declara cancellable = true cuando realmente canceles. Cancelar en HEAD salta el método vanilla y la inyección de todos los demás mods después del tuyo - es lo menos compatible que un @Inject puede 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ñade shift = At.Shift.AFTER para después), FIELD, NEW.
  • Usa ordinal solo 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> donde R es el tipo de retorno empaquetado (Void para void).
  • 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

  • @Redirect reemplaza una llamada. Solo un mod puede redirigir una llamada dada; el @Redirect de un segundo mod (o un @WrapOperation aplicado sobre una llamada redirigida en algunos órdenes) falla con un error grave. Usa @WrapOperation en su lugar.
  • @ModifyVariable apunta a una local por tipo y ordinal, lo que cambia entre versiones. Usa @ModifyExpressionValue en la expresión que produce el valor, o @Local para leerlo.
  • @Overwrite reemplaza 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.
  • @Mutable en un setter elimina final del 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 HEAD de 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 ordinal y @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#shouldApplyMixin verificando FabricLoader.getInstance().isModLoaded("othermod")), o usa "require": 0 en 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=true verifica que los mixins implementen métodos de interfaz.
  • Error de decodificación (objetivo no encontrado, InvalidInjectionException, redirecciones conflictivas): consulta el skill fabric-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.
  • @Redirect donde @WrapOperation/@ModifyExpressionValue servirían; @Overwrite en un mod público.
  • cancellable = true + ci.cancel() en HEAD como la forma predeterminada de "cambiar" el comportamiento.
  • Olvidar original.call(...) en @WrapOperation.
  • CallbackInfoReturnable genérico incorrecto (primitivo en lugar de empaquetado), o faltan los parámetros del objetivo antes de ci cuando los necesitas.
  • Clase mixin no listada en la configuración, listada en la sección incorrecta (mixins vs client), o en un paquete diferente al package de 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 @Unique y sin prefijo que colisionan con el mixin de otro mod.
  • Desactivar defaultRequire para "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.

Individual skills in this repo

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

Skills relacionados