概要:
以新米modder为受众讲述如何在forge中使用mixin,包含如何把做好的mod给build出来。为了更加注重本质以及让教程更具普适性,教程纯用记事本+命令行进行,不使用IDE(但在Part IV会讲述如何使用IDE)。哪怕不理解内容,只要按步骤走一定没问题(网络不行除外)。此外,教程使用mc1.12.2,其他版本的mc流程基本一致。标星号’*‘的部分为选读内容。
正文:
I. 开发环境配置
我们需要一个带mixin的forge开发环境,这一步相当折磨人,网络不好的话半天时间都得砸这上面,但是不要灰心,过了这个坎接下来基本是顺风顺水。
1. 下载资源 & 修改build.gradle
首先去 forge官网 下载一个1.12.2的mdk,将其解压
我选择解压到D:\LocalCodes\Minecraft\Example中,注意到解压出来的内容中有build.gradle和gradlew.bat两个文件,其他文件我们暂且忽略不管。先用记事本打开build.gradle
-
buildscript {
-
repositories {
-
maven { url = 'https://maven.minecraftforge.net/' }
-
mavenCentral()
-
}
-
dependencies {
-
classpath 'net.minecraftforge.gradle:ForgeGradle:3.+'
-
}
-
}
-
-
apply plugin: 'net.minecraftforge.gradle'
-
// Only edit below this line, the above code adds and enables the necessary things for Forge to be setup.
-
apply plugin: 'eclipse'
-
apply plugin: 'maven-publish'
- ...
按照上图进行修改,一共6处修改,务必确保每处修改都已完成。教程不对gradle进行深入阐述,但了解一些gradle相关的内容(不必完全掌握)会对你mod的项目管理有很大帮助,也能让你在部署开发环境的时候少走些弯路。
2. 运行gradle
接下来启动cmd,进入build.gradle所在的文件夹(我的话就是D:\LocalCodes\Minecraft\Example),调用命令
- gradlew build
运行成功后会出现 BUILD SUCCESSFUL 字样。上图是我运行gradlew build时的情况,因为之前已经下载过相关资源并反编译,所以速度很快。
注:你可能阅读过很多forge环境配置的教程,而每一篇都叫你调用gradlew eclipse之类的命令,然后说mod发布的时候调用gradlew build。一上来就build是在干什么?众所周知gradlew build是将src里的源码编译成能在生产环境(玩家的运行环境)下运行的jar,但运行gradlew build时还会在没配置过环境的电脑上“下载各种游戏资源,并进行反编”,教程正是利用了这个特点来配置无IDE的开发环境。
II. 编写你的第一个Mixin+Forge Mod
1. 写什么好呢?
我打算写个把地狱门边框从黑曜石改为木头的模组,大体效果是这样的
原先是搭黑曜石框点火启动,现在是搭木头框点火启动。
为了写出这个mod,我们得先知道mc里地狱门是怎么一回事,玩过mc的都知道怎么建地狱门,用黑曜石搭一个框,中间留出2x3的空白,然后用打火石点火即可,有经验的玩家还知道打雷引发的火,或者火焰弹引发的火也可以启动地狱门。事实上,用黑曜石搭一个合适的框,只要是火方块就能启动地狱门(在框内的空白中填充传送门方块)。教程专注于如何编写mixin+forge模组,在这里不对机制的代码基础进行阐述,相关内容参阅BlockFire.java、BlockPortal.java及Teleporter.java。
2. 第一个mixin
你会发现mixin指代了很多东西,有时候它指spongepowered开发的mixin library,同时它又指mixin library定义的一类文件mixin,所谓的mixin文件可以被理解为一种特殊的class文件,一定被@Mixin修饰,且在运行时被mixin library特殊处理。
我们先写个mixin对BlockPortal$Size.class进行修改,在src/main/java/com/example/examplemod下新建文件夹mixin,其下新建文件MixinBlockPortal$Size.java,用记事本打开,复制以下内容
-
package com.example.examplemod.mixin;
-
-
import org.spongepowered.asm.mixin.Mixin;
-
import org.spongepowered.asm.mixin.injection.At;
-
import org.spongepowered.asm.mixin.injection.Redirect;
-
-
import org.objectweb.asm.Opcodes;
-
-
import net.minecraft.block.BlockPortal;
-
import net.minecraft.block.Block;
-
import net.minecraft.init.Blocks;
-
-
@Mixin(BlockPortal.Size.class)
-
public abstract class MixinBlockPortal$Size {
-
-
@Redirect(method = "getDistanceUntilEdge", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
-
private Block proxy_getDistanceUntilEdge_getStatic_OBSIDIAN() {
-
return Blocks.LOG;
-
}
-
-
@Redirect(method = "calculatePortalHeight", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
-
private Block proxy_calculatePortalHeight_getStatic_OBSIDIAN() {
-
return Blocks.LOG;
-
}
- }
这样我们的第一个mixin就写好了,教程不会详细讲述mixin如何写,如果对这块内容有需求,参见文末的拓展阅读。
3. mixins.example.json
有了mixin文件,我们还需要告诉mixin library该mixin的存在,这样它才能应用你的mixin。首先在src/main/resources下新建文件mixins.example.json,复制以下内容
-
{
-
"required": true,
-
"compatibilityLevel": "JAVA_8",
-
"package": "com.example.examplemod.mixin",
-
"mixins": [
-
"MixinBlockPortal$Size"
-
],
-
"client": [
-
],
-
"server": [
-
],
-
"injectors": {
-
"defaultRequire": 1
-
},
-
"refmap": "mixins.example.refmap.json"
- }
-
mixin {
-
add sourceSets.main, 'mixins.example.refmap.json'
-
config 'mixins.example.json'
- }
我们先暂且不管mixins.example.json以及这次对build.gradle的修改作用是什么,这块内容将在Part III 2.1(modid-1.0.jar结构分析)时提及。
4. 第二个mixin
现在写第二个mixin,在MixinBlockPortal$Size.java所处的文件夹下(即文件夹mixin)新建文件MixinTeleporter.java,复制以下内容
-
package com.example.examplemod.mixin;
-
-
import org.spongepowered.asm.mixin.Mixin;
-
import org.spongepowered.asm.mixin.injection.At;
-
import org.spongepowered.asm.mixin.injection.Redirect;
-
-
import org.objectweb.asm.Opcodes;
-
-
import net.minecraft.world.Teleporter;
-
import net.minecraft.block.Block;
-
import net.minecraft.init.Blocks;
-
-
@Mixin(Teleporter.class)
-
public abstract class MixinTeleporter {
-
-
@Redirect(method = "makePortal", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
-
private Block proxy_makePortal_getStatic_OBSIDIAN() {
-
return Blocks.LOG;
-
}
-
-
@Redirect(method = "placeInPortal", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
-
private Block proxy_placeInPortal_getStatic_OBSIDIAN() {
-
return Blocks.LOG;
-
}
- }
接着修改mixins.example.json,在 "MixinBlockPortal$Size" 后加入 , "MixinTeleporter" ,使之变为 "MixinBlockPortal$Size" , "MixinTeleporter" 。
III. 发布!
别着急,mc模组从开发环境到生产环境(玩家的运行环境)之间还有一段路要走,视情况可能比开发环境配置还棘手。不过,在一般情况下就是一句gradlew build的事。
1. Build看看
cmd中执行
- gradlew build
成功build,到build/libs中拿结果(modid-1.0.jar文件)。
2.1. modid-1.0.jar结构分析
让我们详细看看到底build出了什么,用压缩包打开modid-1.0.jar。
modid-1.0.jar
│ mcmod.info
│ mixins.example.json
│ mixins.example.refmap.json
│ pack.mcmeta
│
├─com
│ └─example
│ └─examplemod
│ │ ExampleMod.class
│ │
│ └─mixin
│ MixinBlockPortal$Size.class
│ MixinTeleporter.class
│
└─META-INF
MANIFEST.MF
有两个文件不是我们写的而是系统生成的,分别是mixins.example.refmap.json以及MANIFEST.MF,先说说MANIFEST.MF,用记事本打开
-
Manifest-Version: 1.0
-
TweakOrder: 0
-
ForceLoadAsMod: true
-
Implementation-Title: Example
-
MixinConfigs: mixins.example.json
-
Implementation-Version: 1.0
-
Specification-Vendor: examplemodsareus
-
Specification-Title: examplemod
-
Implementation-Timestamp: 2022-09-26T12:28:25+0800
-
TweakClass: org.spongepowered.asm.launch.MixinTweaker
-
Specification-Version: 1
- Implementation-Vendor: examplemodsareus
注意到 TweakOrder: 0 , ForceLoadAsMod: true , MixinConfigs: mixins.example.json , TweakClass: org.spongepowered.asm.launch.MixinTweaker 这四行,整个文件最重要的就这四行,其他都是一些删掉也没关系的信息。注意到MixinConfigs项,mixin library在加载的时候会扫描classpath里的所有文件,如果在META-INF/MANIFEST.MF中发现MixinConfigs项,便会读取该项指向的文件,也即我们编写的mixins.example.json,按mixins.example.json中的内容加载mixin文件。
MixinConfigs项可以指向多个文件,例如 MixinConfigs: mixins.example.json,mixins.another_example.json 。mixin library会分别读取mixins.example.json、mixins.another_example.json,按其中的内容加载mixin文件。MixinConfigs项是系统生成的,还记得我们在Part II.3(mixins.example.json)中给build.gradle的最后加上的代码吗,其中 config 'mixins.example.json' 这行就能让MixinConfigs项生成的时候带上mixins.example.json,如果我想让生成的MixinConfigs项再带上mixins.another_example.json的话,只需在 config 'mixins.example.json' 之下添加一行新代码 config 'mixins.another_example.json' 即可。
其他项的介绍不在教程范围内,现阶段可以不去理解它的涵义(同时你也一定不要删掉它们),但我也简要的提及一下,TweakClass项是让FML把org.spongepowered.asm.launch.MixinTweaker(mixin library内置的tweaker)载到launchwrapper里,初始化mixin(mixin的初始化方法很多,这只是其中一个),TweakOrder项是用来控制载入的tweaker的优先级的,ForceLoadAsMod用来让带tweaker的mod依然被作为fml mod载入。
*2.2. 混淆名
我们还有mixins.example.refmap.json的作用没有解析,但就算不清楚它的作用也不会影响你在forge中使用mixin,因而这块以及下一块仅作为教程的选读内容,吃不消的话完全可以跳过。
在具体讲述refmap之前,我们需要先了解“混淆名”,这里只做必要的简略介绍,忽略了一些更进一步的细节(例如多套反混淆名),关于mc里的混淆,网上文章很多,读者诺已有混淆和反混淆方面的经验可直接跳过此部分。
Minecraft是商业软件,Mojang在发布(将java文件编译为class文件)的时候为了防止
在Mojang的工作室里,你所熟知的EntityPlayer.java可能是长这样的
-
public class net.minecraft.world.entity.player.Player
-
public void onTick();
- ...
-
public class abc
-
public void d();
- ...
-
public class abc
-
public void d();
- ...
为了看懂Mojang在写些什么鬼,就需要“反混淆”,将不可读的类名、方法名、变量名,在不破坏代码结构的前提下,变成具有可读性的类名、方法名、变量名。将abc.class反混淆后,便得到了你熟知的EntityPlayer.java
-
public class net.minecraft.entity.player.EntityPlayer
-
public void onUpdate();
- ...
可以看到,你所熟知的EntityPlayer,在Mojang里叫Player,minecraft.jar里叫abc,而你所熟知的EntityPlayer里的onUpdate()方法,在Mojang里叫onTick(),minecraft.jar里叫d()。因为混淆和反混淆,同一个“东西”具有了三个名字,我们把Mojang里的名字叫official名,minecraft.jar里的叫混淆名,通过反混淆得到的叫反混淆名。因为混淆和反混淆都不破坏代码结构,official名、混淆名、反混淆名之间有一个一一对应的关系,比方说,net.minecraft.world.entity.player.Player(official名)<-> abc(混淆名)<-> net.minecraft.entity.player.EntityPlayer(反混淆名)。
除了以上提及的几种名称之外,一个“东西”还具有一些“中间名”(看作不遵守可读性的反混淆名),例如SRG NAME,“中间名”主要是为了方便程序处理而存在的。SRG NAME的例子有
net.minecraft.world.entity.player.Player(official名)<-> abc(混淆名)<-> net.minecraft.entity.player.EntityPlayer(SRG NAME)<-> net.minecraft.entity.player.EntityPlayer(反混淆名)
net.minecraft.world.entity.player.Player#onTick()(official名)<-> abc#d()(混淆名)<-> net.minecraft.entity.player.EntityPlayer#method_00012_d()(SRG NAME)<-> net.minecraft.entity.player.EntityPlayer#onUpdate()(反混淆名)
*2.3. refmap
接下来我们开始解析refmap的作用,同样你也可以跳过本部分(但你不要把refmap删掉,也不要修改mixins.example.json里的refmap项)。
编写中...
3. 生产环境
终于给build出来了。找个1.12.2装了forge的mc,把modid-1.0.jar扔到mods文件里,启动游戏——直接崩,以下是崩溃报告(在run/logs/latest.log)。
注:该问题高版本不会出现,原因是forge自带了mixin(已加入豪华套餐)。
JVM在加载org.spongepowered.asm.launch.MixinTweaker类的时候找不到该类,就是说classpath里没有mixin library,mixin作为一个第三方工具forge以及mc并没有自带,而你的modid-1.0.jar里也没有mixin,自然会ClassNotFoundException了。一个简单粗暴的解决方法是把mixin.jar内的所有内容塞进modid-1.0.jar中。
注:塞mixin.jar的时候不要把mixin.jar的MANIFEST.MF扔进去,保留你的MANIFEST.MF,也不要复制META-INF里的MUMFREY.SF与MUMFREY.RSA。另外,mixin.jar在'C:\Users\用户名\.gradle\caches\modules-2\files-2.1\org.spongepowered\mixin'里。
再次启动游戏,无崩溃,进入单人测试功能,无问题。可将modid-1.0.jar发布至社区。
*IV. 我想用IDE
最后的最后,教程以eclipse为例教你在完成先前步骤的情况下搭eclipse开发环境。
输入命令
- gradlew eclipse
- gradlew genEclipseRuns
注意,如果你对build.gradle进行修改的话,例如我在mixin块(Part II.3中添加的)里增减了代码,记得重新运行gradlew genEclipseRuns,具体原因为何打开runClient(或runServer)的Run Configurations->Arguments一目了然。
拓展阅读:
Mixin官方wiki
ForgeGradle官方wiki
Mixins on forge
Gradle官方文档
附件:
这里有教程使用的源码以及modid-1.0.jar,你可以把modid-1.0.jar扔到某个装了forge的1.12.2的客户端里体验一下。
