.
.
.

注意:此教程基于forge1.20.1环境编写,其他版本不代表一定可行。
但其他版本可以参考

一、 Mixin是什么,使用场景

*Mixin 是一个字节码注入框架不修改源代码、不替换游戏文件的前提下,向类、方法、字段中动态插入自定义代码,实现对游戏逻辑的修改、扩展、劫持

*那么什么场景要使用Mixin呢?

  • 当API 不提供修改入口时,用 Mixin 直接改写方法
  • 劫持 / 拦截原类行为,阻止原类逻辑执行,或替换成自己的逻辑
  • 修复 Minecraft 原版 BUG或者用 Mixin 打补丁优化逻辑

需要注意的是:Mixin不光可以针对原版代码,也可注入其他模组类,最常见的注入其他模组案例就是,优化模组将代码注入到sodium或oculus模组中。


二、环境配置

1、首先确保你的idea中安装了Minecraft Development插件
我的世界开发插件
2、找到build.gradle,在plugins中加入

    id 'org.spongepowered.mixin' version '0.7.+'

如图:
在这里插入图片描述
之后在build.gradle中添加新条目mixin

mixin {
    // 【必要】Mixin引用映射表(核心文件,无则崩溃)
    add sourceSets.main, 'mixins.<你的模组id>.refmap.json'

    // 【必要】指定Mixin配置文件(无则Mixin不加载)
    config 'mixins.<你的模组id>.json'

    // 【可选】调试:输出详细Mixin日志
    debug.verbose = true

    // 【可选】调试:导出注入后的原版class文件(方便查错)
    debug.export = true

    // 【可选】调试:Mixin注入失败时转储错误信息
    dumpTargetOnFailure = true

    // 【可选】静默模式:减少Gradle日志输出
    quiet
}

接下来,在build.gradle的dependencies中添加

    annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'

之后,我们build.gradle的配置算是完成了。

接下来需要将目录看向resources,在这里创建mixins.<模组名>.json

{
  "required": true,
  "package": "net.<包名>",                  //这里填Mixin类所在的包路径
  "compatibilityLevel": "JAVA_17",         //这里填JAVA版本号
  "mixins": [
      ""                                   //这里添加所有Mixin的类名。
  ],
  "client": [],
  "minVersion": "0.8"
}

之后刷新gradle,接下来就可以创建Mixin类编写了!


三、Mixin类和全限定名

Mixin类的特点有:

  • 抽象类
  • 类头带有@Mixin注解
  • 不能含有public方法

1、Mixin类框架

如下一个Mixin的样例:

/**
 * 类级别的Mixin头,表示该类是一个Mixin类,remap表示是否作映射。
 * 如果不理解映射是什么,只需记住,原版类不需要写remap = false,
 * forge方法和模组方法需要些remap = false
 */
@Mixin(value = ForgeHooks.class, remap = false)
public abstract class AdventureBlocksMixin {

/**
 * 方法级别Mixin头,任何需要修改原类方法的方法都要编写Mixin头。
 * 方法级Mixin在后面会做讲解,是最重要的部分。
 */
    @Inject(
            method = "isCorrectToolForDrops",
            at = @At("RETURN"),
            cancellable = true
    )
    private static void injectCustomHarvestCheck(BlockState state, Player player, CallbackInfoReturnable<Boolean> cir) {
        // 1. 检查是否为我们的目标矿石
        if (state.is(AdventureBlocks.MYTHRIL_ORE.get()) || state.is(AdventureBlocks.ANCIENT_MYTHRIL_ORE.get())) {
            // 2. 获取原本的判定结果 (铁镐等级检查)
            boolean originalCanHarvest = cir.getReturnValue();

            // 3. 如果原本就挖不动,直接返回 false,不需要检查后续逻辑
            if (!originalCanHarvest) {
                return;
            }

            // 4. 执行我们的自定义检查 (PlusOneOrBetterProcedure)
            // 注意:这里需要确保 player 不为 null,虽然在挖掘上下文中通常不会为 null
            boolean customCheckPassed = (player != null) && PlusOneOrBetterProcedure.execute(player);

            // 5. 只有当原版检查通过 且 自定义检查通过时,才返回 true
            cir.setReturnValue(customCheckPassed);
        }
    }
}

新建的Mixin类需要加入mixin.<模组id>.json中
在这里插入图片描述
如图,如果你安装了Minecraft Development插件,那么在写了Mixin头时会提醒你添加 (标黄),这时只需要Add to Mixin config即可。

在这里插入图片描述
如上图。


2、全限定名与注入定位

我们接下来看一个样例:
我们要修改某个模组的某些武器伤害。
以下是它的Mixin头

//这个Mixin头之前讲过
@Mixin(value = BrutalityItems.class, remap = false)
public abstract class BrutalityItemsMixin {
//	    ……省略一堆代码

//这个就是注入头之一,现在还不需要知道它是什么
	@Redirect(
	            // 匹配所有调用 DeferredRegister.register 的方法(也可指定具体类,比如你的物品注册类)
	            method = "<clinit>",  //操作的代码在该类静态代码块中
	            at = @At(
	                    value = "INVOKE",
	                    // 正确的 register 方法全限定名(核心!)
	                    target = "Lnet/minecraftforge/registries/DeferredRegister;register(Ljava/lang/String;Ljava/util/function/Supplier;)Lnet/minecraftforge/registries/RegistryObject;"
	            )
	    )
	    private static <I extends Item> RegistryObject<I> brutality$modifyWoodenRulerSupplier(
	            DeferredRegister<Item> instance,  // DeferredRegister 实例(原方法的 this)
	            String name,                      // 第一个参数:物品名
	            Supplier<? extends I> originalSup // 第二个参数:原 Supplier
	    ) {}
	    
//	    ……省略一堆代码
}

我们看到target这里有一个很长的东西,它就是全限定名。

既然Mixin是改代码的,那就需要知道是哪个代码
全限定名就是用来匹配方法的。

如果全限定名写的没错,我们可以通过Ctrl+左键点击它跳转。
在这里插入图片描述
如图,这里出现了多个匹配到的结果,那么为什么会这样呢?

既然是用来修改武器数值的,
因此我要在BrutalityItem调用武器注册时,让它调用我的代码,来修改数值。
因此找到BrutalityItems.class,看到它的注册方法。
在这里插入图片描述
如图所示,因此Mixin匹配到了很多个register方法,所以才会像上面一样。
这意味着我的代码会在所有register中调用。
在这里插入图片描述
由于匹配到的register过多,因此我在这里加了判断,用来限定特定物品。

所以我是怎么让Mixin知道我要修改BrutalityItems类中,静态代码块中的register方法调用的呢?

首先是BrutalityItems类,刚才讲过,通过类级别Mixin定位,修改模组类,remap = false

@Mixin(value = BrutalityItems.class, remap = false)

定位到BrutalityItems中静态代码块,用的是method = “< clinit >”

@Redirect(
            method = "<clinit>",  //"<clinit>表示静态代码块"
            //at = @At()为核心的位置注解,所有方法级Mixin都要写
            at = @At(
                    value = "INVOKE",  //at的value参数,填写在哪里注入,INVOKE表示被调用的地方
                    // target参数为目标。这里的意思是: 注入在INVOKE register的地方
                    // 正确的 register 方法全限定名(核心!)
                    target = "Lnet/minecraftforge/registries/DeferredRegister;register(Ljava/lang/String;Ljava/util/function/Supplier;)Lnet/minecraftforge/registries/RegistryObject;"
            )
    )

这里插一嘴:method可以使用全限名,非全限名,以及<init>和<clinit>
<init>是类的构造函数里,<clinit>是类中的静态代码块static{},非全限名很好用
一一个类中没有多态函数,就可以使用非全限名,写法是只写函数名,无括号
例如:method = "execute"
如果类中有多个execute方法,这时候需要全限名匹配。

register的全限定名就是这一长串:

target = "Lnet/minecraftforge/registries/DeferredRegister;register(Ljava/lang/String;Ljava/util/function/Supplier;)Lnet/minecraftforge/registries/RegistryObject;"

全限格式:L包名/用/斜杠/分隔/类名;方法名(参数1描述符参数2描述符)返回值描述符
接下来逐步拆解样例。
1、Lnet/minecraftforge/registries/DeferredRegister;,表示register方法在DefferedRegister类中。开头的L是必要的
2、register(……),方法名照抄
3、register中的参数:Ljava/lang/String;Ljava/util/function/Supplier;
在这里插入图片描述
Ctrl+左键BrutalityItems的register,定位到原定义,可以看到我们匹配的register参数是String和Supplier,因此我们写
register(Ljava/lang/String;Ljava/util/function/Supplier;)
Supplier包路径可以Ctrl+左键Supplier查看原类
4、Lnet/minecraftforge/registries/RegistryObject; 表示返回值,register返回的是RegisterObject。
现在再来看全限名:

target = "Lnet/minecraftforge/registries/DeferredRegister;register(Ljava/lang/String;Ljava/util/function/Supplier;)Lnet/minecraftforge/registries/RegistryObject;"

L 包名 ; register(参数; 参数; 参数;……) 返回值;
(注意分号。)
那么int这种基础类型该怎么写呢?
JVM 用单个字母代表基础数据类型,是固定规则:
表格

void V
boolean Z
int i
float F
double D
long J
数组 [
例子:
double [] [D

例如,ChunkHolder中
public void sectionLightChanged(LightLayer p_140037_, int p_140038_)
的全限名如下:

"Lnet/minecraft/server/level/ChunkHolder;sectionLightChanged(Lnet/minecraft/world/level/LightLayer;I)V"

定位方法,牢记三点:

修改哪个类的,什么方法里的,哪一段代码

现在有些晕还不要紧,接着看就会明白。

三、@Overwrite

刚才只讲解了如何定位方法,接下来讲如何修改。

修改代码的方法有好几种,它们被不同的注入头区分开来,其中**@Overwrite**是最简单的注入头。

@Overwrite的作用是覆盖原类的方法,
需保证
1、被@Overwrite注释的方法和原类的方法名和形参一致
2、编写方法的文档级注解

	/**
	 * @author 
	 * @reason 
	 */

举个例子:


    /**
	 * @author FantasyClouds
	 * @reason Fix error when use /reload command
	 * <ol>
	 * <li>using Modifier to be more safer</li>
	 * </ol>
	 */
	@Overwrite
	public static void execute(LevelAccessor world, Entity entity) {
		if (!(((LivingEntity) entity).getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.ARMOR)
			.hasModifier((new AttributeModifier(UUID.fromString("80ca198b-f167-4083-ae99-82372718ad89"), "story_of_forgotten_land.bauble_fix", 2, AttributeModifier.Operation.ADDITION)))))
		((LivingEntity) entity).getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.ARMOR)
				.addTransientModifier((new AttributeModifier(UUID.fromString("80ca198b-f167-4083-ae99-82372718ad89"), "story_of_forgotten_land.bauble_fix", 2, AttributeModifier.Operation.ADDITION)));
    }

原类是这样的(使用idea的反编译获得):


public class DiamondGauntletBaubleIsEquippedProcedure {
    public DiamondGauntletBaubleIsEquippedProcedure() {
    }

    public static void execute(LevelAccessor world, Entity entity) {
        if (entity != null) {
            if (!world.m_5776_()) {
                ((LivingEntity)entity).m_21051_(Attributes.f_22284_).m_22100_(((LivingEntity)entity).m_21051_(Attributes.f_22284_).m_22115_() + 2.0);
            }

        }
    }
}

可以看到原类在佩戴饰品时直接修改属性,这样做往往很不安全,例如在玩家在使用/reload指令,重新进入存档时,由于游戏会再次让饰品重新佩戴,这会导致属性再次增加
由于原类方法简单,功能单一,因此我选用了Overwrite修改代码,完全覆盖它的代码。

这里提一嘴,在MC模组开发中,对属性的操作最好都要用属性修饰符Modifier,这是唯一标准且安全的方法。

四、@Inject

@Inject的作用是向原类代码中注入新的代码,通常会向函数的HEAD,TAIL处注入,或是用来修改返回值。

1、三要素

@Inject有三要素:method、at和cancellable
我们来看一个例子,我们想修改原版锋利附魔的数值
原类相关的代码是这样的(idea反编译获得):

package net.minecraft.world.item.enchantment;

public class DamageEnchantment extends Enchantment {
	……省略
    public float getDamageBonus(int p_44635_, MobType p_44636_) {
        if (this.type == 0) {
            return 1.0F + (float)Math.max(0, p_44635_ - 1) * 0.5F;
        } else if (this.type == 1 && p_44636_ == MobType.UNDEAD) {
            return (float)p_44635_ * 2.5F;
        } else {
            return this.type == 2 && p_44636_ == MobType.ARTHROPOD ? (float)p_44635_ * 2.5F : 0.0F;
        }
    }
    ……省略
}

Code 4.1.1-原版锋利伤害代码

我们的Mixin这么写:


@Mixin(DamageEnchantment.class)
public abstract class SharpnessMixin {
    @Inject(
            method = "getDamageBonus", // 要修改的原版方法名
            at = @At("RETURN"), // 注入时机:方法返回前
            cancellable = true // 允许修改返回值
    )
    private void modifySharpnessDamage(int level, MobType mobType, CallbackInfoReturnable<Float> cir) {
        DamageEnchantment enchantment = (DamageEnchantment) (Object) this;

        if (enchantment.type == 0) {
            float newDamage = (float) Math.max(0, level) * 1.5F;
            cir.setReturnValue(newDamage);
        }
    }
}

Code 4.1.2-SharpnessMixin

这里 @Inject头的三要素:method、at和cancellable 分别是

  • getDamageBonus ——对应注入的是getDamageBonus方法。
  • at = @At(“RETURN”)—— at = @At(……)是固定写法,表示注入在getDamageBonus的方法返回前。
  • cancellable——表示允许修改返回值。

2、签名的形参

通过观察Code 4.1.2可以发现,Mixin方法的形参比原类的形参多了一,这是 @Inject所特有的
@Inject的形参规则是:

  • 1、形参列表需要有原类method方法中所有的形参。
  • 2、在1的基础上,添加一个CallbackInfo或者CallbackInfoReturnable<?>参数。
  • 3、对于不拦截返回值的,写CallbackInfo,否则写CallbackInfoReturnable<?>
  • 2和3中的?填写原类返回值的类型。

3、用@Inject修改返回值的用法

在之前的样例里,我们想修改锋利附魔的伤害,我们希望修改为每一级+1.5伤害,且不关心原来的逻辑。那么使用 @Inject修改返回值 是很好的选择。

在Code4.1.2中,我们写了@Inject的三要素,方法签名,之后用 **

(DamageEnchantment) (Object) this;**进行强制类型转换,骗过编译器获取原类本身,之后编写我们的逻辑:锋利附魔的公式修改为level×1.5

之后调用cir.setReturnValue(newDamage);,这表示修改返回值。

至此就是用@Inject修改返回值的用法的流程。

4、用@Inject插入代码

(1)TAIL

刚才的样例,只是 at = @At(“RETURN”) 的情况,那有没有@At(其他)呢?

有的兄弟有的,这样的样例还有三种。

at参数除了RETURN,还有HEAD, TAIL和INVOKE

HEAD表示在函数一开始,TAIL表示在函数结尾,INVOKE表示在某函数调用时。

接下来的样例,我们希望修改Terramity模组,幽邃套的套装主动效果。

由于套装效果是通过按键主动触发的,我们不能通过事件监听来添加我们的效果,除非我们注册一个和它一样的按键,但这样做玩家更改按键要改两个,这太不优雅了。

经过考虑之后,我决定使用Mixin注入到原模组的逻辑中。

首先获取原类:(idea反编译)


public class DimliteArmorSetAbilityProcedure {
    public DimliteArmorSetAbilityProcedure() {
    }

    public static void execute(LevelAccessor world, double x, double y, double z, Entity entity) {
        if (entity != null) {
            double raytrace_distance = 0.0;
            entity.m_20256_(new Vec3(entity.m_20154_().f_82479_ * -1.45, entity.m_20154_().f_82480_ * -1.45, entity.m_20154_().f_82481_ * -1.45));
            if (!world.m_5776_() && world instanceof Level) {
                Level _level = (Level)world;
                if (!_level.m_5776_()) {
                    _level.m_5594_((Player)null, BlockPos.m_274561_(x, y, z), (SoundEvent)ForgeRegistries.SOUND_EVENTS.getValue(new ResourceLocation("entity.warden.sonic_boom")), SoundSource.NEUTRAL, 2.0F, 1.0F);
                } else {
                    _level.m_7785_(x, y, z, (SoundEvent)ForgeRegistries.SOUND_EVENTS.getValue(new ResourceLocation("entity.warden.sonic_boom")), SoundSource.NEUTRAL, 2.0F, 1.0F, false);
                }
            }

            raytrace_distance = -0.8;

            for(int index0 = 0; index0 < 6; ++index0) {
                ++raytrace_distance;
                if (world instanceof ServerLevel) {
                    ServerLevel _level = (ServerLevel)world;
                    _level.m_8767_(ParticleTypes.f_235902_, (double)entity.m_9236_().m_45547_(new ClipContext(entity.m_20299_(1.0F), entity.m_20299_(1.0F).m_82549_(entity.m_20252_(1.0F).m_82490_(raytrace_distance)), Block.COLLIDER, Fluid.NONE, entity)).m_82425_().m_123341_(), (double)entity.m_9236_().m_45547_(new ClipContext(entity.m_20299_(1.0F), entity.m_20299_(1.0F).m_82549_(entity.m_20252_(1.0F).m_82490_(raytrace_distance)), Block.COLLIDER, Fluid.NONE, entity)).m_82425_().m_123342_(), (double)entity.m_9236_().m_45547_(new ClipContext(entity.m_20299_(1.0F), entity.m_20299_(1.0F).m_82549_(entity.m_20252_(1.0F).m_82490_(raytrace_distance)), Block.COLLIDER, Fluid.NONE, entity)).m_82425_().m_123343_(), 1, 0.0, 0.0, 0.0, 0.0);
                }

                Vec3 _center = new Vec3((double)entity.m_9236_().m_45547_(new ClipContext(entity.m_20299_(1.0F), entity.m_20299_(1.0F).m_82549_(entity.m_20252_(1.0F).m_82490_(raytrace_distance)), Block.COLLIDER, Fluid.NONE, entity)).m_82425_().m_123341_(), (double)entity.m_9236_().m_45547_(new ClipContext(entity.m_20299_(1.0F), entity.m_20299_(1.0F).m_82549_(entity.m_20252_(1.0F).m_82490_(raytrace_distance)), Block.COLLIDER, Fluid.NONE, entity)).m_82425_().m_123342_(), (double)entity.m_9236_().m_45547_(new ClipContext(entity.m_20299_(1.0F), entity.m_20299_(1.0F).m_82549_(entity.m_20252_(1.0F).m_82490_(raytrace_distance)), Block.COLLIDER, Fluid.NONE, entity)).m_82425_().m_123343_());
                List<Entity> _entfound = world.m_6443_(Entity.class, (new AABB(_center, _center)).m_82400_(0.75), (e) -> {
                    return true;
                }).stream().sorted(Comparator.comparingDouble((_entcnd) -> {
                    return _entcnd.m_20238_(_center);
                })).toList();
                Iterator var13 = _entfound.iterator();

                while(var13.hasNext()) {
                    Entity entityiterator = (Entity)var13.next();
                    if (entityiterator != entity) {
                        entityiterator.m_6469_(new DamageSource(world.m_9598_().m_175515_(Registries.f_268580_).m_246971_(DamageTypes.f_268679_), entity), 8.0F);
                    }
                }
            }

            if (entity instanceof LivingEntity) {
                LivingEntity _entity = (LivingEntity)entity;
                _entity.m_21195_((MobEffect)TerramityModMobEffects.SLAM_STATE.get());
            }

        }
    }
}

可以看到由于原功能为产生音爆并向后推动玩家,这个功能比较复杂,我们不好找新功能该插在哪一行,这时我们可以将代码插在尾部,将条件判断重新判一遍,这样虽然多了一点性能开销,但由于这不是tick事件,这一点性能也无所谓了,比寻找具体的插入点要简单。

代码如下:


@Mixin(value =  DimliteArmorSetAbilityProcedure.class, remap = false)
public abstract class DimliteArmorSetAbilityProcedureMixin {
    @Inject(
            method="execute",
            at = @At(
                    value = "TAIL"
            )
    )
    private static void dimliteArmorSet(LevelAccessor world, double x, double y, double z, Entity entity, CallbackInfo ci){
        if (entity == null) return;
        if (world.isClientSide()) return;
        if (!(world instanceof Level)) return;
        if (!(entity instanceof LivingEntity livingEntity)) return;

        boolean hasOriginalEffect = livingEntity.hasEffect(MobEffects.DAMAGE_BOOST);
        if (!hasOriginalEffect) {
            MobEffectInstance rangedStrengthEffect = new MobEffectInstance(
                    MobEffects.DAMAGE_BOOST,
                    200,
                    2
            );
            livingEntity.addEffect(rangedStrengthEffect);
        }
    }
}

我想应该不用讲解,大家也能看懂了。

(2)HEAD

而使用@At(“HEAD”)也是同理,例如以下这个Mixin类:


    // ========== 核心:替换自身发光度(0-15,解决发亮的关键) ==========
    @Inject(
            method = "getLightEmission()I",
            at = @At("HEAD"),
            cancellable = true,
            remap = true
    )
    @OnlyIn(Dist.CLIENT)
    private void replaceLightEmission(CallbackInfoReturnable<Integer> cir) {
        // 父类this可安全转换为BlockState(因为子类只有BlockState)
        BlockState currentState = (BlockState) (Object) this;
        // 检查是否被cloak
        if (ClientRevelationHolder.isCloaked(currentState)) {
            BlockState targetState = ClientRevelationHolder.getCloakTarget(currentState);
            // 强制返回目标方块的发光度
            cir.setReturnValue(targetState.getLightEmission());
        }
    }

这个类旨在解决forge1.20.1——EventHorizon模组的bug:在替换方块贴图时,并不会替换方块亮度,这似的萤石伪装成下界岩时,仍会发光。
其中ClientRevelationHolder.isCloaked就是EventHorizon的判断是否伪装的方法。

(3)INVOKE

该方案最难,也最不常用。

接下来看一个案例:
我们需要修改too_many_bows模组的生命箭的回血量,原版回血量直接为伤害量。
我们需要修改为max(伤害量×0.1, 1)。


public class VitalityArrow extends AbstractArrow {
    private Consumer<LivingEntity> onHitCallback;

    public VitalityArrow(EntityType<? extends VitalityArrow> entityType, Level level) {
        super(entityType, level);
    }

    public VitalityArrow(Level level, LivingEntity shooter) {
        super((EntityType)EntityRegistry.VITALITY_ARROW.get(), shooter, level);
    }

    public void setOnHitCallback(Consumer<LivingEntity> callback) {
        this.onHitCallback = callback;
    }

    protected void m_5790_(EntityHitResult result) {
        super.m_5790_(result);
        if (!this.m_9236_().m_5776_()) {
            Entity var3 = result.m_82443_();
            if (var3 instanceof LivingEntity) {
                LivingEntity target = (LivingEntity)var3;
                LivingEntity shooter = this.m_19749_() instanceof LivingEntity ? (LivingEntity)this.m_19749_() : null;
                if (shooter != null) {
                    if (this.onHitCallback != null) {
                        this.onHitCallback.accept(target);
                    }

                    float maxHeal = (float)(this.m_36789_() * 0.5);
                    float actualHeal = Math.min(maxHeal, shooter.m_21233_() - shooter.m_21223_());
                    if (actualHeal > 0.0F) {
                        shooter.m_5634_(actualHeal);
                        this.m_9236_().m_6263_((Player)null, target.m_20185_(), target.m_20186_(), target.m_20189_(), SoundEvents.f_12275_, SoundSource.PLAYERS, 0.8F, 1.4F);
                    }
                }
            }
        }

        this.m_146870_();
    }

    protected ItemStack m_7941_() {
        return new ItemStack(Items.f_42412_);
    }

    public Packet<ClientGamePacketListener> m_5654_() {
        return new ClientboundAddEntityPacket(this);
    }
}

Code 4.3.1 VitalityArrow

如Code4.3.1,生命箭的回复量为actualHeal ,执行了shooter.m_5634_(actualHeal)方法。
这边使用的方法为精确@Inject在shooter.m_5634_()之前,执行我们的逻辑。


@Mixin(value = VitalityArrow.class, remap = false)
public abstract class VitalityArrowMixin {


    // 核心Mixin注入方法:修改治疗量
    // 注入点位:在 执行射手回血(shooter.heal) 之前 执行我们的逻辑
    @Inject(
            method = "m_5790_", // 目标方法:VitalityArrow的命中实体方法(混淆名)
            at = @At(
                    value = "INVOKE",
                    target = "Lnet/minecraft/world/entity/LivingEntity;m_5634_(F)V", // 精准定位:heal(float)回血方法执行前
                    shift = At.Shift.BEFORE
            ),
            cancellable = true // 取消原有的回血逻辑,我们自己执行新的回血
    )
    private void modifyHealAmount(EntityHitResult result, CallbackInfo ci) {
        // 1. 复制原类的前置判断逻辑,保证和原代码一致
        VitalityArrow arrow = (VitalityArrow) (Object) this;
        Entity hitEntity = result.getEntity();
        if (hitEntity instanceof LivingEntity target) {
            Entity owner = arrow.getOwner();
            if (owner instanceof LivingEntity shooter) {
                Level level = arrow.level();
                if (!level.isClientSide()) { // 只在服务端执行,避免双端同步问题

                    float maxHeal = (float) (Math.min(arrow.getBaseDamage() * 0.1F, 1.0F));

                    // 保留原逻辑:实际治疗量 = 计算出的治疗量 和 射手缺失血量 取最小值(不会回血溢出)
                    float actualHeal = Math.min(maxHeal, shooter.getMaxHealth() - shooter.getHealth());

                    // 执行我们修改后的回血逻辑
                    if (actualHeal > 0.0F) {
                        shooter.heal(actualHeal);
                        // 保留原有的治疗音效,无需修改
                        level.playSound(null, target.getX(), target.getY(), target.getZ(), SoundEvents.ARROW_HIT_PLAYER, SoundSource.PLAYERS, 0.8F, 1.4F);
                    }

                    // 取消原有的回血逻辑,避免执行两次治疗
                    ci.cancel();
                }
            }
        }
    }
}

Code4.3.2 VitalityArrowMixin

如上面代码,三要素中的at使用的是INVOKE,这里的shift = At.Shift.BEFORE表示注入在INVOKE之前,如果是shift = At.Shift.AFTER则是注入在INVOKE之后。

五、@Redirect

@Redirect和Inject不同,它不往代码里插入代码,而是将某个调用**“偷天换日”**。
如果一个函数体里有int a(1);的调用,我们用@Redirect锁定它,之后就可以让它变成int b(1);。

在以下例子里,我们想要修改terramity的玛瑙套装效果,原版的效果为随机传送,我们要把他修改为赋予背刺力量效果。

经过反编译得知,该逻辑位于ArmorSetBonusAbilityOnKeyPressedProcedureexecute方法里,但这个execue非常长,包含了多种套装效果的逻辑,有几百行。

首先肯定不能用@Overwrite方法,一是模组后续更新新套装效果会失效,二是冗长的逻辑我们不好复刻。
也不能用Inject,由于玛瑙套位于中间,不能@Inject在它之前,然后强行返回

所以就引出了@Redirect的用法:


@Mixin(value = ArmorSetBonusAbilityOnKeyPressedProcedure.class, remap = false)
public abstract class ArmorSetBonusAbilityOnKeyPressedProcedureMixin {
	@Redirect(
            method="execute",
            at = @At(
                    value = "INVOKE",
                    target = "Lnet/mcreator/terramity/procedures/RandomTeleportProcedure;execute(Lnet/minecraft/world/level/LevelAccessor;DDDLnet/minecraft/world/entity/Entity;)V",
                    ordinal = 0
            )
    )
    private static void redirectOnyxSetEffect(LevelAccessor world, double x, double y, double z, Entity entity){

        if (entity == null) return;
        if (world.isClientSide()) return;
        if (!(world instanceof Level)) return;
        if (!(entity instanceof LivingEntity livingEntity)) return;

        boolean hasOriginalEffect = livingEntity.hasEffect(BackstabMobEffect.BACKSTABSTRENGTH.get());
        if (!hasOriginalEffect) {
            MobEffectInstance rangedeffect = new MobEffectInstance(
                    BackstabMobEffect.BACKSTABSTRENGTH.get(),
                    100,
                    1
            );
            livingEntity.addEffect(rangedeffect);
        }
    }
}

Code5.1.1 Redirect的Mixin类

如上,@Redirect捕捉了ArmorSetBonusAbilityOnKeyPressedProcedure.execute方法中的RandomTeleportProcedure.execute方法,并将它替换成了我们自己的函数体

@Redirect这种用法叫做重定向,特别适合需要删除并修改某一条调用的情景。

@Redirect同样具有三要素,分别是method、at和ordinal 。method和at之前都讲过,这里不在赘述。
ordinal是用来定位第几次调用的。 如果一个方法中存在多次调用,我们可以用ordinal定位到具体的那一条。其中0表示第一个。

六、@ModifyArg

当我们想修改某个函数调用的参数,该如何修改呢?
以下有一个案例,我们想修改莱特兰恶意中,虚空之眼和逐日之翼下限的配置文件,这样我们可以让虚空之眼在虚空之上掉落,逐日之翼可以在建筑高度上限之内掉落。
经过反编译,原类的注册如下:

public class LCConfig {
	……省略一堆代码
    public static class Common {
        public final ForgeConfigSpec.IntValue belowVoid;
        public final ForgeConfigSpec.IntValue phantomHeight;
    	……省略一堆代码
    	
        Common(ForgeConfigSpec.Builder builder) {
        	this.belowVoid = builder.comment("Requirement for void eye drop").defineInRange("belowVoid", 16, 0, 128);
            
        	this.phantomHeight = builder.comment("Requirement for sun membrane drop").defineInRange("phantomHeight", 200, 0, 10000);
        	……省略一堆代码
            
        }
    }
}

Code6.1.1 LCConfig

我们发现我们需要修改的是LCConfig内部类Common 的构造方法中的defineInRange调用的参数
我们通过Ctrl+左键点击defineInRange,跳转到了方法的定义处:

        public DoubleValue defineInRange(String path, double defaultValue, double min, double max) {
            return this.defineInRange(ForgeConfigSpec.split(path), defaultValue, min, max);
        }

Code6.1.2 defineInRange定义

接下来我们就可以着手写Mixin类了


@Mixin(value = LCConfig.Common.class, remap = false)
public abstract class L2complementsConfigMixin {
    @ModifyArg(
        method = "<init>(Lnet/minecraftforge/common/ForgeConfigSpec$Builder;)V",
        at = @At(
            value = "INVOKE",
            target = "Lnet/minecraftforge/common/ForgeConfigSpec$Builder;defineInRange(Ljava/lang/String;III)Lnet/minecraftforge/common/ForgeConfigSpec$IntValue;"
        ),
        index = 2 //表示修改的哪个形参,2是第三个形参。
    )
    private int modifyConfigMinValue(String name, int defaultValue, int originalMin, int max) {
        // 根据配置项名称修改最小值
        if ("belowVoid".equals(name)) {
            return -128; // 自定义负值下界(可根据需求调整)
        } else if ("phantomHeight".equals(name)) {
            return -10000; // 自定义负值下界(可根据需求调整)
        }
        // 其他配置项保持原最小值
        return originalMin;
    }
}

Code6.1.3 ModifyArg 的 Mixin类

注意Mixin头中LCConfig.Common.class的用法。
@ModifyArg同样有三要素:method、at和index。method和at是老生常谈,index表示的是修改形参列表的哪个参数。这里我们要修改下限让其可以为负,所有index为2。

七、@ModifyVariable

如果我们需要修改方法体中的局部变量,能否修改呢?答案是可以的,这是要用到**@ModifyVariable** 注解。

以下有一个场景,我们需要修改改莱特兰恶意中,焚烧配方的概率。作者本人查找修改了模组数据包的json文件,发现并没有起作用,因此我就翻到了源代码,打算从Mixin入手。

以下是反编译到的原类:

public class MaterialEventHandler {
	……省略一堆代码

    public static void onItemKill(Level level, Entity entity, ItemStack stack) {
        BurntRecipe.Inv inv = new BurntRecipe.Inv();
        inv.m_6836_(0, stack);
        List<BurntRecipe> opt = level.m_7465_().m_44056_((RecipeType)LCRecipes.RT_BURNT.get(), inv, level);
        Iterator var5 = opt.iterator();

        while(var5.hasNext()) {
            BurntRecipe r = (BurntRecipe)var5.next();
            ItemStack result = r.assemble(inv, level.m_9598_());
            int chance = r.chance;
            int trial = stack.m_41613_();
            int det = trial / chance;
            trial %= chance;
            if (level.f_46441_.m_188503_(chance) < trial) {
                ++det;
            }

            det *= result.m_41613_();

            while(det > 0) {
                int sup = Math.min(det, result.m_41741_());
                det -= sup;
                ItemStack copy = result.m_41777_();
                copy.m_41764_(sup);
                level.m_7967_(new ItemEntity(level, entity.m_20185_(), entity.m_20186_(), entity.m_20189_(), copy, 0.0, 0.5, 0.0));
            }
        }

    }
}

Code 7.1.1 焚烧转化配方代码

我想把所有的焚烧概率提高64倍,通过观察代码得知,这里的chance就是概率的分母,因此只需要 将其除以64, 焚烧转化的概率就提高了64倍。

有了思路之后,我们可以着手编写代码:

@Mixin(value = MaterialEventHandler.class, remap = false)
public abstract class L2ComplementsBurnt2Mixin {

    /**
     * 拦截`onItemKill`方法中声明的`chance`变量,将其修改为原值的1/64
     */
    @ModifyVariable(
            method = "onItemKill(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/item/ItemStack;)V",
            at = @At(value = "STORE"), // 拦截变量被赋值并存储的时刻
            ordinal = 0, // 方法中第一个声明的int类型变量"chance"
            name = "chance" // 目标变量名
    )
    private static int modifyChance(int originalChance) {
        // 将原chance除64(注意:若需浮点精度可改为 (float) originalChance / 64.0F,根据原逻辑类型调整)
        return originalChance / 64;
    }
}

Code 7.1.2 焚烧转化Mixin代码

@ModifyVariable注解有四要素method ,at,ordinal 和name
method是老生常谈,at中我们经常使用"STORE"作为拦截变量赋值的时刻。而ordinal表示第几个声明为chance的变量,name就是变量的名称

我想认真读到这里的读者们,已经能很轻松看懂这段代码了,所以我就不解释了。

八、@ModifyConstant

如果我们只需要修改模组中的硬编码,比如字符串,技能冷却时间,不对其他做过多修改,该使用什么方法最方便呢?

这时就可以考虑使用**@ModifyConstant**了。

例如,我们想修改Terramity中幽邃套装的冷却时间,将8s提高到60s,
原逻辑在类ArmorSetBonusAbilityOnKeyPressedProcedureexecute方法中。

该方法有500多行代码,我们不能直接Overwrite,也不方便Redirect,Inject也不是适合,而ModifyArg由于原方法中多次调用过addEffect方法,且关于冷却的逻辑并不在addEffect方法中,而是在MobEffectInstance实例的创建中。因此ModifyArg需要新建MobEffectInstance实例,略显麻烦。

_entity.m_7292_(new MobEffectInstance((MobEffect)TerramityModMobEffects.ARMOR_SET_ABILITY_COOLDOWN.get(), (int)(160.0 * PrismaticRingMathProcedure.execute(entity)), 0));

Code 8.1.1 幽邃套添加冷却效果的逻辑


    @ModifyConstant(
            method = "execute(Lnet/minecraft/world/level/LevelAccessor;DDDLnet/minecraft/world/entity/Entity;)V",

            constant = @Constant(doubleValue = 160.0)
    )

    private static double modifyDimliteArmorCooldown(double original) {
        return 2400;
    }

Code 8.1.2 修改幽邃套冷却时长的Mixin方法

注解参数四要素

@ModifyConstant(
    method = "目标方法名/全限定名",  // 1. 要修改的方法
    at = @At("CONSTANT"),           // 2. 固定写法:匹配常量
    constant = @Constant(类型 = 原值), // 3. 核心:匹配要改的常量
    ordinal = 0                     // 4. 第几个匹配到的常量(0=第一个)
)

@Constant 常量匹配规则:

常量类型 写法示例
整数 int intValue = 10
浮点数 float floatValue = 0.5F
双精度 double doubleValue = 1.0D
布尔值 boolean boolValue = true
字符串 String stringValue = “abc”
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。