本帖最后由 crow02531 于 2022-9-28 22:50 编辑

概要:
以新米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
  1. buildscript {
  2. repositories {
  3. maven { url = 'https://maven.minecraftforge.net/' }
  4. mavenCentral()
  5. }
  6. dependencies {
  7. classpath 'net.minecraftforge.gradle:ForgeGradle:3.+'
  8. }
  9. }

  10. apply plugin: 'net.minecraftforge.gradle'
  11. // Only edit below this line, the above code adds and enables the necessary things for Forge to be setup.
  12. apply plugin: 'eclipse'
  13. apply plugin: 'maven-publish'
  14. ...
复制代码
很长一串文本(Groovy语言,不去管他),接下来我们要对build.gradle文件进行一系列修改

按照上图进行修改,一共6处修改,务必确保每处修改都已完成。教程不对gradle进行深入阐述,但了解一些gradle相关的内容(不必完全掌握)会对你mod的项目管理有很大帮助,也能让你在部署开发环境的时候少走些弯路。

2. 运行gradle

接下来启动cmd,进入build.gradle所在的文件夹(我的话就是D:\LocalCodes\Minecraft\Example),调用命令
  1. gradlew build
复制代码
初次运行会跑相当长一段时间,其间会下载各种游戏资源,并进行反编译。如果运行失败,并在log中显示了timeout字样,网络问题,多试几次说不定就好了。


运行成功后会出现 BUILD SUCCESSFUL 字样。上图是我运行gradlew build时的情况,因为之前已经下载过相关资源并反编译,所以速度很快。

注:你可能阅读过很多forge环境配置的教程,而每一篇都叫你调用gradlew eclipse之类的命令,然后说mod发布的时候调用gradlew build。一上来就build是在干什么?众所周知gradlew build是将src里的源码编译成能在生产环境(玩家的运行环境)下运行的jar,但运行gradlew build时还会在没配置过环境的电脑上“下载各种游戏资源,并进行反编”,教程正是利用了这个特点来配置无IDE的开发环境。

II. 编写你的第一个Mixin+Forge Mod
(你不知道一句绿油油的 BUILD SUCCESSFUL 之后有着多少血与泪)。接下来我们将写一个简单的Mixin+Forge模组作为案例。

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,用记事本打开,复制以下内容
  1. package com.example.examplemod.mixin;

  2. import org.spongepowered.asm.mixin.Mixin;
  3. import org.spongepowered.asm.mixin.injection.At;
  4. import org.spongepowered.asm.mixin.injection.Redirect;

  5. import org.objectweb.asm.Opcodes;

  6. import net.minecraft.block.BlockPortal;
  7. import net.minecraft.block.Block;
  8. import net.minecraft.init.Blocks;

  9. @Mixin(BlockPortal.Size.class)
  10. public abstract class MixinBlockPortal$Size {

  11. @Redirect(method = "getDistanceUntilEdge", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
  12. private Block proxy_getDistanceUntilEdge_getStatic_OBSIDIAN() {
  13. return Blocks.LOG;
  14. }

  15. @Redirect(method = "calculatePortalHeight", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
  16. private Block proxy_calculatePortalHeight_getStatic_OBSIDIAN() {
  17. return Blocks.LOG;
  18. }
  19. }
复制代码

这样我们的第一个mixin就写好了,教程不会详细讲述mixin如何写,如果对这块内容有需求,参见文末的拓展阅读。

3. mixins.example.json

有了mixin文件,我们还需要告诉mixin library该mixin的存在,这样它才能应用你的mixin。首先在src/main/resources下新建文件mixins.example.json,复制以下内容
  1. {
  2. "required": true,
  3. "compatibilityLevel": "JAVA_8",
  4. "package": "com.example.examplemod.mixin",
  5. "mixins": [
  6. "MixinBlockPortal$Size"
  7. ],
  8. "client": [
  9. ],
  10. "server": [
  11. ],
  12. "injectors": {
  13. "defaultRequire": 1
  14. },
  15. "refmap": "mixins.example.refmap.json"
  16. }
复制代码
接着在build.gradle的最后加上代码
  1. mixin {
  2. add sourceSets.main, 'mixins.example.refmap.json'
  3. config 'mixins.example.json'
  4. }
复制代码

我们先暂且不管mixins.example.json以及这次对build.gradle的修改作用是什么,这块内容将在Part III 2.1(modid-1.0.jar结构分析)时提及。

4. 第二个mixin

现在写第二个mixin,在MixinBlockPortal$Size.java所处的文件夹下(即文件夹mixin)新建文件MixinTeleporter.java,复制以下内容
  1. package com.example.examplemod.mixin;

  2. import org.spongepowered.asm.mixin.Mixin;
  3. import org.spongepowered.asm.mixin.injection.At;
  4. import org.spongepowered.asm.mixin.injection.Redirect;

  5. import org.objectweb.asm.Opcodes;

  6. import net.minecraft.world.Teleporter;
  7. import net.minecraft.block.Block;
  8. import net.minecraft.init.Blocks;

  9. @Mixin(Teleporter.class)
  10. public abstract class MixinTeleporter {

  11. @Redirect(method = "makePortal", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
  12. private Block proxy_makePortal_getStatic_OBSIDIAN() {
  13. return Blocks.LOG;
  14. }

  15. @Redirect(method = "placeInPortal", at = @At(value = "FIELD", target = "net.minecraft.init.Blocks.OBSIDIAN:Lnet/minecraft/block/Block;", opcode = Opcodes.GETSTATIC))
  16. private Block proxy_placeInPortal_getStatic_OBSIDIAN() {
  17. return Blocks.LOG;
  18. }
  19. }
复制代码

接着修改mixins.example.json,在 "MixinBlockPortal$Size" 后加入 , "MixinTeleporter" ,使之变为 "MixinBlockPortal$Size" , "MixinTeleporter"

III. 发布!
别着急,mc模组从开发环境到生产环境(玩家的运行环境)之间还有一段路要走,视情况可能比开发环境配置还棘手。不过,在一般情况下就是一句gradlew build的事。

1. Build看看

cmd中执行
  1. 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,用记事本打开
  1. Manifest-Version: 1.0
  2. TweakOrder: 0
  3. ForceLoadAsMod: true
  4. Implementation-Title: Example
  5. MixinConfigs: mixins.example.json
  6. Implementation-Version: 1.0
  7. Specification-Vendor: examplemodsareus
  8. Specification-Title: examplemod
  9. Implementation-Timestamp: 2022-09-26T12:28:25+0800
  10. TweakClass: org.spongepowered.asm.launch.MixinTweaker
  11. Specification-Version: 1
  12. 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文件)的时候为了防止 我们这些人 心怀鬼胎的某些人反编译(将class文件还原为java文件),便将编译出的class文件做了名为混淆的进一步处理。这种处理,简而言之就是把原来具有可读性的类名、方法名、变量名,在不破坏代码结构的前提下,换成不具可读性的类名、方法名、变量名。通过这种方法,让你反编译出来也看不懂他在写什么,举个例子:

在Mojang的工作室里,你所熟知的EntityPlayer.java可能是长这样的
  1. public class net.minecraft.world.entity.player.Player
  2. public void onTick();
  3. ...
复制代码
经过编译+混淆后,它变成这样的一个class文件
  1. public class abc
  2. public void d();
  3. ...
复制代码
如果直接反编译,你会得到这样的java文件
  1. public class abc
  2. public void d();
  3. ...
复制代码
鬼才看得懂,就跟你直接打开minecraft.jar看到的东西一样。Mojang就是通过这种方法保护它的代码的。

为了看懂Mojang在写些什么鬼,就需要“反混淆”,将不可读的类名、方法名、变量名,在不破坏代码结构的前提下,变成具有可读性的类名、方法名、变量名。将abc.class反混淆后,便得到了你熟知的EntityPlayer.java
  1. public class net.minecraft.entity.player.EntityPlayer
  2. public void onUpdate();
  3. ...
复制代码

可以看到,你所熟知的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开发环境。

输入命令
  1. gradlew eclipse
复制代码
成功后再输入
  1. gradlew genEclipseRuns
复制代码
至此eclipse开发环境搭建完毕。打开eclipse,File->Import->Existing Projects into Workspace即可。

注意,如果你对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的客户端里体验一下。

source.zip (75.13 KB, 下载次数: 31)
modid-1.0.jar (871.13 KB, 下载次数: 14)