Mixins Fabric
Mixins aplicam patches no bytecode do Minecraft durante o carregamento. Eles são poderosos e frágeis: cada mixin é uma promessa de que um método específico, com uma forma específica, ainda existe na próxima versão e que nenhum outro mod o altera de forma incompatível. Escreva o mínimo possível, da forma mais restrita possível.
Ordem de decisão - tente estas opções antes de escrever um mixin
- Evento ou registro Fabric API. Pesquise a Fabric API primeiro: eventos de ciclo de vida (
ServerLifecycleEvents,ClientTickEvents), eventos de jogador (UseBlockCallback,AttackEntityCallback,PlayerBlockBreakEvents), HUD (HudElementRegistry), telas (ScreenEvents), loot (LootTableEvents), comandos (CommandRegistrationCallback), rede, dicas de item, listeners de recarregamento de recursos e muitos mais. Navegue em https://github.com/FabricMC/fabric-api (pacoteapide cada módulo) e na documentação do Fabric. Um evento é estável entre versões e compõe-se com outros mods gratuitamente. - Pontos de extensão vanilla. Conteúdo orientado a dados (tags, tabelas de loot, receitas, componentes de dados, registries) geralmente não precisa de nenhum patch de código.
- Accessor / Invoker mixin - quando você só precisa ler/escrever um campo privado ou chamar um método privado.
- Class tweaker (access widener) - quando você precisa criar uma subclasse de uma classe final, sobrescrever um método final ou nomear uma classe privada/de pacote privado.
- Injector mixin - só agora. Prefira injetores MixinExtras ao invés de
@Redirect/@Overwrite.
Anote (em um comentário acima do mixin) por que os passos 1-4 não funcionaram. Os revisores perguntarão.
Antes de escrever qualquer injetor: confirme o alvo
Nomes e assinaturas mudam a cada versão do Minecraft. Nunca escreva um alvo de memória.
./gradlew genSources # então abra a classe no IDE ou pesquise as fontes geradas
- 26.1+ (não ofuscado): mire os nomes Mojang exatamente como aparecem nas fontes.
- Pré-26.1: mire os nomes do mapeamento do seu projeto (Yarn ou Mojang). O processador de anotações de mixin do Loom os remapeia para intermediary durante a compilação; uma incompatibilidade aparece como um aviso "Cannot remap" durante a compilação - trate esse aviso como um erro.
- Copie descritores de método da visualização de bytecode ou de uma ferramenta (plugin MCDev IntelliJ, mcsrc.dev), não manualmente. Referência de descritores: https://docs.fabricmc.net/develop/mixins/bytecode
Config de mixin e convenções
{
"required": true,
"package": "com.example.examplemod.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
"client": [],
"server": [],
"injectors": { "defaultRequire": 1 },
"overwrites": { "requireAnnotations": true }
}
"defaultRequire": 1faz com que um mixin que não corresponde a nada falhe ruidosamente em vez de silenciosamente não fazer nada. Mantenha-o.- Alvos somente cliente (
net.minecraft.client.*) vão na config do cliente (emsrc/client/resources, declarados com"environment": "client"emfabric.mod.json). - Classes mixin nunca são referenciadas a partir de código normal (exceto interfaces accessor, através de um cast).
- Prefixe cada membro adicionado com seu mod id para evitar colisões com outros mods:
examplemod$onTick,@Unique private int examplemod$counter;. - Marque campos/métodos auxiliares que você adicionar com
@Unique. - Métodos handler são
private(não estáticos se o alvo não for estático).
Os alvos nos exemplos abaixo usam nomes Mojang e mostram a forma de cada injetor. Nomes de métodos e descritores variam entre versões - verifique cada um deles em suas fontes geradas antes de reutilizá-lo.
@Inject - executa código em um ponto, 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á sobrecarregado (getJumpPower() e getJumpPower(float)): forneça o descritor.
@Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(cir.getReturnValueF() * 1.5f);
}
}
CallbackInfopara alvosvoid,CallbackInfoReturnable<T>caso contrário (tipo encapsulado).- Declare
cancellable = trueapenas quando você realmente cancelar. Cancelar emHEADpula o método vanilla e toda injeção de outros mods após a sua - é a coisa menos compatível que um@Injectpode fazer. Prefira modificar um valor. - Para ler os parâmetros do alvo, adicione-os à assinatura do handler antes do
CallbackInfo, em ordem:(float amount, CallbackInfo ci). - Valores comuns de
@At:HEAD,RETURN(todo retorno),TAIL(apenas o último retorno),INVOKE(antes de uma chamada; adicioneshift = At.Shift.AFTERpara depois),FIELD,NEW. - Use
ordinalapenas quando necessário; ele quebra assim que a Mojang adiciona outra chamada correspondente.
MixinExtras - use estes em vez de @Redirect e da maioria dos @ModifyVariable
O MixinExtras está incluído no Fabric Loader (0.15+), portanto nenhuma dependência extra é necessária. As importações estão sob com.llamalad7.mixinextras. Documentação: https://github.com/LlamaLad7/MixinExtras/wiki
@ModifyExpressionValue - altera o que uma chamada/leitura de campo retorna
@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;
}
Vários mods podem encadear @ModifyExpressionValue na mesma expressão; cada um recebe o resultado do mod anterior. Um @Redirect na mesma chamada entraria em conflito e quebraria.
@WrapOperation - envolve uma chamada; chama a original (ou não)
@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); // mantenha a cadeia intacta
}
- Parâmetros: o receiver (para chamadas não estáticas), depois os argumentos da chamada e depois
Operation<R>ondeRé o tipo de retorno encapsulado (Voidparavoid). - Sempre chame
original.call(...)a menos que pular a chamada seja a funcionalidade. Não chamá-lo desabilita silenciosamente todos os outros wrappers de mods na mesma chamada.
@WrapWithCondition - condicionalmente pula uma chamada void
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, argumentos da chamada... */) {
return !ExampleConfig.get().hideThing; // false = pular a chamada
}
@Local - lê uma variável local sem @ModifyVariable
@Inject(method = "tick", at = @At(value = "INVOKE", target = "..."))
private void examplemod$readLocal(CallbackInfo ci, @Local(name = "pos") BlockPos pos) { ... }
No 26.1+ o jogo fornece nomes de parâmetros e locais, então @Local(name = ...) é confiável lá. Pré-26.1 (sem nomes locais em jars ofuscados) prefira @Local(ordinal = n) por tipo e verifique com mixin.debug.export.
Outras ferramentas úteis do MixinExtras
@ModifyReturnValue (altera o valor de retorno de um método em RETURN, encadeia com outros mods), @ModifyReceiver, @WrapMethod (envolve um método inteiro), @Share (passa um valor entre dois injetores no mesmo método), @Cancellable (cancela de dentro de um @WrapOperation).
@Redirect e @ModifyVariable - apenas quando nada mais se encaixa
@Redirectsubstitui uma chamada. Apenas um mod pode redirecionar uma determinada chamada; um segundo@Redirectde outro mod (ou um@WrapOperationaplicado sobre uma chamada redirecionada em algumas ordens) falha com uma quebra grave. Use@WrapOperationem vez disso.@ModifyVariablemira um local por tipo e ordinal, que muda entre versões. Use@ModifyExpressionValuena expressão que produz o valor ou@Localpara lê-lo.@Overwritesubstitui o método inteiro: incompatível com qualquer outro mod que o toque. Não use em um 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();
- Métodos accessor de instância recebem o prefixo do mod id; os estáticos (
static T getX() { throw new AssertionError(); }) não precisam. @Mutableem um setter removefinaldo campo alvo.- Invoker de construtor:
@Invoker("<init>") static Foo examplemod$new(...). - Accessors funcionam também em classes de outros mods; class tweakers só funcionam no vanilla.
Class tweakers (antigamente access wideners)
Necessários para criar uma subclasse de uma classe final, sobrescrever um método final ou referenciar uma classe não pública. Exige Loader 0.18+ e Loom 1.12+; configurações mais antigas usam a mesma sintaxe sob o nome "access widener" com um cabeçalho 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;
Conecte-o em ambos os lugares, depois atualize o Gradle e reexecute genSources:
loom { accessWidenerPath = file("src/main/resources/examplemod.classtweaker") }
"accessWidener": "examplemod.classtweaker"
Execute ./gradlew validateAccessWidener - entradas inválidas são silenciosamente ignoradas. Use diretivas transitive- apenas quando mods que dependem do seu precisarem ver a alteração. Detalhes do cabeçalho, recursos v2 (injeção de interface, extensão de enum): https://docs.fabricmc.net/develop/class-tweakers/
Checklist de compatibilidade entre mods
- Injete no ponto mais restrito que funciona; nunca cancele em
HEADde um método amplamente usado (tick,render,hurt,travel) a menos que tenha certeza. - Prefira injetores que encadeiam (
@ModifyExpressionValue,@ModifyReturnValue,@WrapOperation) em vez de injetores que substituem (@Redirect,@Overwrite). - Use
@Unique+ prefixo do mod id para todo estado adicionado. - Evite
ordinale@At("TAIL")quando@At("RETURN")funcionar. - Use
@Mixin(value = X.class, priority = ...)apenas para resolver um conflito conhecido; documente-o. - Mixins opcionais direcionados a outro mod: coloque-os em uma config separada e proteja-os com um plugin de mixin (
IMixinConfigPlugin#shouldApplyMixinverificandoFabricLoader.getInstance().isModLoaded("othermod")), ou use"require": 0nesse injetor. - Patchers pesados conhecidos (Sodium, Iris, Lithium, ImmediatelyFast) substituem grandes partes do renderizador e da lógica do jogo. Se você injetar em renderização ou tick de entidades, teste com eles.
Depurando mixins
Adicione argumentos JVM às configurações de execução de desenvolvimento:
loom {
runs {
configureEach {
vmArg "-Dmixin.debug.export=true" // escreve classes transformadas em run/.mixin.out/
vmArg "-Dmixin.debug.verbose=true"
vmArg "-Dmixin.dumpTargetOnFailure=true"
}
}
}
run/.mixin.out/class/...contém as classes transformadas - descompile-as para ver exatamente o que seu injetor produziu e em qual ordem os injetores de outros mods foram executados.-Dmixin.checks.interfaces=trueverifica se os mixins implementam métodos de interface.- Erro de decodificação (alvo não encontrado,
InvalidInjectionException, redirecionamentos conflitantes): veja a habilidadefabric-crash-debugging.
Erros comuns
- Escrever um mixin para algo que a Fabric API já tem um evento (HUD, ticks, uso de bloco, dicas, comandos, tabelas de loot).
- Adivinhar um nome de método ou descritor de memória ou de uma versão/mapeamento diferente. Sempre verifique as fontes geradas primeiro.
- Usar nomes Yarn (
MinecraftClient,PlayerEntity) em um projeto com nomes Mojang ou vice-versa. @Redirectonde@WrapOperation/@ModifyExpressionValueserviriam;@Overwriteem um mod público.cancellable = true+ci.cancel()emHEADcomo a maneira padrão de "alterar" comportamento.- Esquecer
original.call(...)em@WrapOperation. CallbackInfoReturnablegenérico errado (primitivo em vez de encapsulado) ou parâmetros do alvo faltando antes deciquando você precisar deles.- Classe mixin não listada na config, listada na seção errada (
mixinsvsclient) ou em um pacote diferente dopackageda config. - Alvos cliente na config comum: trava servidores dedicados na inicialização.
- Referenciar uma classe mixin diretamente de código normal (
new LivingEntityMixin()) ou colocar classes auxiliares não mixin dentro do pacote de mixin. - Campos não
@Unique, sem prefixo que colidem com o mixin de outro mod. - Desligar
defaultRequirepara "corrigir" um mixin com falha - isso esconde o bug e envia um mod que silenciosamente não faz nada. - Class tweaker declarado em apenas um dos
build.gradle/fabric.mod.json, ou não validado.