Mixin与AT
当你的mod开发到一定程度时,很有可能会遇到需要修改原版已有内容的情况。在这一章,我们将会介绍两个有用的工具,来帮助你达成这个目标。
Mixin
Mixin是一个非常强大的工具,它基于ASM,帮助你方便地修改原版内容。
对了,Mixin的正确读法是mix-in,而不是mi-xin。虽然你非要读后者也没事。
你应该尽量避免使用Mixin。请先确认已经没有其他方法(例如监听事件或继承子类),或是替代方法非常不划算后再使用Mixin。Mixin的加入可能会极大地提高错误出现的可能性和诊断错误的难度。
在Minecraft模组编写时,Mixin的作用范围仅限于MC本身和其他mod,不能对其他依赖进行修改。比如说,net.minecraft.commands.arguments.coordinates.Vec3Argument
是一个有效的Mixin目标,而com.mojang.brigadier.arguments.DoubleArgumentType
则不行。
如果你确实需要对DoubleArgumentType
这样的第三方类进行修改,那不妨考虑继承后再@Override等其他方法。
Mixin在理论上确实可以用于其他非Minecraft项目。
所有的Mixin类应该放且仅放在mixin包中。mixin包中也不应该包含普通类,比如你的方块或者实体什么的。
当然,你也可以分好几个包,但多数情况下这没有必要。
Mixin通过各式各样的注解和背后的注解处理器来完成大多数工作。以下先介绍一些常用或者常见的注解说明。
@Mixin
标记这是一个Mixin类,并在参数中指定目标类。
关于Mixin类
、目标类
、父类
、目标方法
的澄清:
假设你现在需要修改Zombie
类中的foo()
方法中的某一行,那么
Mixin类
:你写出来的进行修改的代码存在的地方,即ZombieMixin
;目标类
:被修改的类,即Zombie
;父类
:目标类的父类,即Monster
;目标方法
:被修改的方法,即Zombie.foo()
。
-
@Inject
标记这是一个回调方法
,这个方法所含的内容将会被注入指定的位置。这大概是最常用的了。 -
@At
写在@Inject
和其他注解内,标记具体的作用位置。 -
@Redirect
标记这是一个重定向方法
,指定的方法调用将会被重定向到此方法。 -
@Overwrite
标记这是一个覆盖方法
,在目标类中的对应方法将会被完全替换为此方法。
不建议使用@Overwrite
,它可能会导致更大的兼容性问题。
-
@Shadow
标记一个占位字段或方法,真实的字段或方法存在于目标类中。 -
@Accessor
标记一个访问器
方法,允许你访问指定的字段。 -
@Invoker
标记一个调用器
方法,允许你调用指定的方法。 -
@Modify...
标记对某种量的修改,可以是参数、变量或常量。
关于Mixin的工作原理,你可以在Mixin的JavaDoc找到更详细的说明,Mixin的GitHub wiki对Mixin的工作原理和使用有详细的解释,mouse0w0的博客有翻译版本。本章不探讨Mixin的工作原理,只讲如何使用之。
Fabric Wiki对Mixin的使用有详细的介绍。Mixin并不是Forge或者Fabric的一部分,故与Mixin有关的内容在大多数情况下可以通用。
下面,我们将以一些场景为例,向你讲述如何使用Mixin修改原版内容。你可以只看你需要用到的情况所对应的例子,也不必看具体的修改内容,只关注文字提到的有关内容。你也可以选择先跳过这些示例,继续看后面的内容。
示例:修改
如果你想要先看看这一堆注解的参数解释而不是使用例,你可以先查看下文注入点
一节 。
如果你的目标类/方法是在生产环境中不会被混淆的类/方法,比如其他mod中的内容,你需要在@Mixin
或@Inject
中加入remap = false
来告诉Mixin不要进行混淆。
你所有的方法都应当在方法名前加上你的modid,或者以其他方式明显地标示这个方法属于哪个mod。
Inject - 在Brighter中修改方块光衰减的方式
这样就可以减少衰减而增加扩散了。Brighter(1.20以前), MixinBlockLightEngine, GPLv3:
@Mixin(BlockLightEngine.class)
public abstract class MixinBlockLightEngine extends LayerLightEngine<FakeBlockLightSectionStorage.FakeBlockDataLayerStorageMap, FakeBlockLightSectionStorage> {
public MixinBlockLightEngine(LightChunkGetter p_75640_, LightLayer p_75641_, FakeBlockLightSectionStorage p_75642_) {
super(p_75640_, p_75641_, p_75642_);
}
@Shadow
protected abstract int getLightEmission(long p_75509_);
@Inject(method = "computeLevelFromNeighbor", at = @At("HEAD"), cancellable = true)
private void brighterGetEdgeLevelHead(long startPos, long endPos, int startLevel, CallbackInfoReturnable<Integer> cir) {
if (endPos == Long.MAX_VALUE) {
cir.setReturnValue(15);
} else if (startPos == Long.MAX_VALUE) {
cir.setReturnValue(startLevel + 15 - this.getLightEmission(endPos));
} else if (startLevel >= 15) {
cir.setReturnValue(startLevel);
} else {
int i = Integer.signum(BlockPos.getX(endPos) - BlockPos.getX(startPos));
int j = Integer.signum(BlockPos.getY(endPos) - BlockPos.getY(startPos));
int k = Integer.signum(BlockPos.getZ(endPos) - BlockPos.getZ(startPos));
Direction direction = Direction.fromNormal(i, j, k);
if (direction == null) {
cir.setReturnValue(15);
} else {
MutableInt mutableint = new MutableInt();
BlockState blockstate = this.getStateAndOpacity(endPos, mutableint);
if (mutableint.getValue() >= 15) {
cir.setReturnValue(15);
} else {
BlockState blockstate1 = this.getStateAndOpacity(startPos, (MutableInt) null);
VoxelShape voxelshape = this.getShape(blockstate1, startPos, direction);
VoxelShape voxelshape1 = this.getShape(blockstate, endPos, direction.getOpposite());
int l = 1;
long prevPos = startPos + (startPos - endPos);
try {
int prevLevel = this.getLevel(prevPos);
if (prevLevel == startLevel - 1&& startLevel != 14) {
l = 0;
}
} catch (Exception ignored) {
}
l += mutableint.getValue();
cir.setReturnValue(Shapes.faceShapeOccludes(voxelshape, voxelshape1) ? 15
: startLevel + l
//Math.max(1, mutableint.getValue())
);
}
}
}
}
}
如上所示,在需要使用目标类的方法时,你可以使用@Shadow
来标记一个Mixin类中的同名方法,这样Mixin就会知道去调用目标类中的对应方法;
在需要使用父类的方法时,你可以让Mixin类(即你看到的这一大坨MixinBlockLightEngine
)继承目标类(即BlockLightEngine
)的父类(即LayerLightEngine
)。
继承父类后可能会要求创建匹配的构造方法,直接alt+enter
就可以,此处的构造方法仅起到保证语法的作用。也可能会要求实现某些抽象方法,你可以将Mixin类标记为抽象类来省去这个麻烦,这样做也是只为了起到保证语法的作用。
相似地,你可以把这个方法标记为抽象方法,来省去一对大括号。
如果这是一个final方法/字段,记得加上@Final
。
你也可以使用((目标类)(Object)this)
来访问目标类的方法或字段。当然,这种方法比@Shadow
一定程度上更简便,却会受到访问控制的限制。
关于其他不同的注入位置如何指定,请参照上文提到的Fabric Wiki中的示例 。
如果你不确定方法参数应该怎样填写,alt+enter
让Minecraft Development插件
来完成就好了。
对于有返回值的目标方法,你可以使用cir.setReturnValue(...)
来指定返回值并返回;如果方法没有返回值,你可以使用ci.cancel()
来结束目标方法。如果你以上两个都不做,目标方法就会在执行完你的注入内容后继续执行剩下的内容;如果你使用的是return
,那只代表结束目前的注入方法,目标方法剩下的内容会继续执行。
如果你要如上使目标方法返回的话,记得在@Inject
中加上cancellable = true
。
Accessor/Invoker - 在MadParticle中访问并执行私有字段和方法
@Mixin(ParticleEngine.class)
public interface ParticleEngineAccessor {
@Accessor("spriteSets")
Map<ResourceLocation, SpriteSet> getSpriteSets();
@Invoker
<T extends ParticleOptions> Particle callMakeParticle(T pParticleData, double pX, double pY, double pZ, double pXSpeed, double pYSpeed, double pZSpeed);
}
在使用时,需要将获取到的ParticleEngine
转为此接口后再使用:
Particle particle = ((ParticleEngineAccessor) Minecraft.getInstance().particleEngine).callMakeParticle(particleOptions, 0, 0, 0, 0, 0, 0);
Minecraft Development
插件也可以帮你自动地生成访问器和调用器:将光标放到目标字段或方法,alt+insert
或右键选择生成
,Generate Accessor/Invoker
即可。
在使用@Accessor
和@Invoker
时,如果你没有改变默认生成的方法名,则不需要在其后指定具体的作用对象。比如在这个例子中,("spriteSets")
是不必要的。