如何从零开始使用@Mixin开发你的模组(我的世界1.20.1Forge)
.
.
.
注意:此教程基于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的玛瑙套装效果,原版的效果为随机传送,我们要把他修改为赋予背刺力量效果。
经过反编译得知,该逻辑位于ArmorSetBonusAbilityOnKeyPressedProcedure的execute方法里,但这个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,
原逻辑在类ArmorSetBonusAbilityOnKeyPressedProcedure的execute方法中。
该方法有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” |
所有评论(0)