因为某些原因, 找不到合适的mod, 所以捡起来Minecraft Mod开发, 其实在我刚玩Minecraft的时候也学过一点点, 然后入坑了Java, 后来就放弃了. 时隔多年, 捡起Mod开发, 加上在捡回的这个过程, 发现网上很多资料没有updated, 于是边开发, 边写这个文章.

在部署环境之前, 需要了解下一些知识, 然后这个不是Java教程, 只是一个在我学习中遇到的坑,和一些基本知识, 然后也会记录一些过时教程和现在的区别.

目录

准备工作
开发
新的物品
新的配方
新的熔炼
生成器
新的工具
打包

准备工作

Forge & FML

首先要了解下Forge & Forge Mod Loader, 在MinecraftForge团队的GitHub wiki里能找到这个问题的答案,(虽然那个项目已经被存档(archived)了.

FML was conceived to address the need for Minecraft Forge to have a reliable mod loading component on the server. However it is and will remain independent of Minecraft Forge.

FML has since taken over all modloading responsibility for Minecraft Forge, since about mid 1.2.5, including the client.

Minecraft Forge ships with a recent version of FML. Generally new versions of FML are submitted as pull requests to Minecraft Forge as needed, or occasionally to bundle up minor fixes. 来源

大概什么意思呢, FML前期只是为了Minecraft Forge有一个可靠的服务端mod加载模组, 但是在1.2.5版本中期左右, 也开始负责客户端mod的加载, 这么看似乎FML能代替Forge? 并不, Forge和FML是两个项目.

Forge is an API, of sorts, to allow mods to work without creating conflicts due to all the base-class editing. Instead Forge does it once and mods just use hooks to make the changes they need.

ModLoader is an API, of sorts, that allows mods to work without creating conflicts, blah blah blah, written by different authors with a slightly different feature set.

FML (Forge ModLoader) is an API-API that allows Forge to run ModLoader mods without needing ModLoader installed. 来源

Forge是一个API(Application programming interface), 提供了一个接口能让开发者容易的和Minecraft进行交会并且不会产生冲突. ModLoader也是个API, 可以让很多mod一起运行并不产生冲突, 而FML是让Forge运行那些原本需要ModLoader才能运行的mod运行.

准备部署

在编译之前, 需要准别下文件.

  • JDK (1.8)
  • IDE (推荐IntelliJ IDEA)
  • MDK (Minecraft Dev Kit?)

JDK 请要确保是1.8, 我使用1.10的时候遇到很多错误, (也希望MinecraftForge团队升级下.) 下载地址就请自行解决吧. IntelliJ IDEA是JetBrain家的JAVA/Kotlin开发的IDE, 有社区版. MDK需要从MinecrafForge官网下载, 找到稳定版就好了, 下载完成后 解压到一个目录下即可.

编译

编译部分按照README里的进行操作.

打开命令行进入MDK的目录, 使用命令

1
2
Windows: "gradlew setupDecompWorkspace"
Linux/macOS: "./gradlew setupDecompWorkspace"

在一切结束后, 打开IDEA, 导入这个项目, 然后选择’import project from external model’中的Gradle, 下一步之后选择’Use default gradle wrapper(recommended), 然后IDEA就会自己安装依赖等, 这个过程可能需要一些时间. 在IDEA完成建立索引之后, 关闭IDEA, 使用命令行进入MDK目录, 并使用

1
2
Windows: "gradlew genIntellijRuns"
Linux/macOS: "./gradlew genIntellijRuns"

等命令跑完, 进入IDEA选择右上角的Client并运行, 游戏就会被打开, 并自动安装好了forge以及一个examplemod.

文件结构

我们只需要在意src/中文件就好了, 这里是源码的存放地.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tree src/

src
└── main
├── java
│   └── com
│   └── example
│   └── examplemod
│   └── ExampleMod.java
└── resources
├── mcmod.info
└── pack.mcmeta

6 directories, 3 files

java/ 下放着Mod的源码, resource/里则是Mod的资源文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cat src/main/resource/mcmod.info

[{
"modid": "examplemod",
"name": "Example Mod",
"description": "Example placeholder mod.",
"version": "${version}",
"mcversion": "${mcversion}",
"url": "",
"updateUrl": "",
"authorList": ["ExampleDude"],
"credits": "The Forge and FML guys, for making this example",
"logoFile": "",
"screenshots": [],
"dependencies": []
}]

mcmod.info是Mod的一些基本信息, 名字, id, 版本, 作者, 介绍和依赖.

至于最主要的 ExampleMod.java, 下一章再说吧.

注意事项

在我编译的过程中, 使用的JDK版本为1.10, 然后遇到 “Could not determine java version from ‘10.0.1’.”, 寻求解决方法的过程中, 有提到更改gradle版本

1
2
3
4
5
6
7
8
gradle/wrapper/gradle-wrapper.properties

#Thu Jun 14 13:39:50 AEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip

将其中的distributionUrl后面的版本好改为4.2, 这样确实解决了”Could not determine java version from ‘10.0.1’.”错误, 但在剩下的部分中又遇到了 “Execution failed for task ‘:recompileMc’.”, 这个部分也是寻找了很久的解决方法, 也没有看到问题所在. 后来我把JDK 1.10降级成1.8, 重新编译, 就没有遇到 以上的俩个错误了, 这也是为什么在准别部署的时候特别提到要确保是JDK 1.8版本.

开发

构思

比如说, 我现在要做一个红宝石Mod, 那他就应该包括

  • 红宝石锭
    • 红宝石块 -> 9个
    • 合成 -> 8个红石 + 1个铁锭
  • 红宝石块
    • 合成 -> 9个红宝石锭
  • 红宝石镐
    • 配方如同铁镐(铁换成红宝石), 效率和钻石镐一样.

这样, 完成了这个Mod的一个制作方向, 当然可以在制作的过程中进行修改.

然后就是命名

  • 红宝石 Ruby
  • 红宝石锭 Ruby Ingot
  • 红宝石块 Ruby Block
  • 红宝石镐 Ruby Pickaxe

配置Mod

接下来就是根据以上的信息配置Mod.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tree src/

src
└── main
├── java
│   └── com
│   └── example
│   └── examplemod
│   └── ExampleMod.java
└── resources
├── mcmod.info
└── pack.mcmeta

6 directories, 3 files

首先删掉java/目录中的东西, 并且以自己的域名作为包名, 然后再建立一个包, 名字为modid, 之后新建一个类取名为 Ruby, 这个就是mod的主类, 所有方块, 物品, 实体等都要通过这个类在游戏中进行注册. 接着在resources下新建一个assets文件夹, 里面还有一个modid的文件夹, 我使用的modid就是它的名字 ruby.

所以在java/里我的目录结构就是

1
2
3
4
5
6
7
8
9
$ tree java/

java
└── xyz
└── rankki
└── ruby
└── Ruby.java

3 directories, 1 file

然后来看一下resources/目录的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tree resources/

resources
├── assets
│   └── ruby
│   ├── blockstates
│   ├── models
│   │   ├── block
│   │   └── item
│   └── textures
│      ├── block
│      └── item
├── mcmod.info
└── pack.mcmeta

7 directories, 2 files

接着, 修改mcmod.info.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat mcmod.info

[{
"modid": "examplemod", //你的modid
"name": "Example Mod", //mod的名字
"description": "Example placeholder mod.", //mod的介绍
"version": "${version}", //mod的版本
"mcversion": "${mcversion}", //Minecraft的版本
"url": "", //mod的地址, 比如开源mod会放在GitHub上
"updateUrl": "", // 更新用的地址, 检查mod是否需要更新
"authorList": ["ExampleDude"], //作者
"credits": "The Forge and FML guys, for making this example", //这个不太知道怎么翻译, 就类似于感谢下谁的帮助啊, 谁参与制作了之类的
"dependencies": [] //依赖, 如果你的mod需要其他mod的依赖, 需要在这里表明
}]

那么, 根据以上信息, 就可以完成一下这个mcmod.info, 除了modid为必须项, 其他都是选填.

1
2
3
4
5
6
7
8
9
10
11
12
$ cat resources/mcmod.info

[{
"modid": "ruby",
"name": "Ruby",
"description": "Provide a new ingot, block which are using ruby",
"version": "${version}",
"mcversion": "${mcversion}",
"url": "https://rankki.xyz",
"authorList": ["RanKKI"],
"credits": "RanKKI"
}]

update, version和mcversion是不推荐填写的, 因为在便宜过程中, 它会根据build.gradle设置而改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 processResources {
// this will ensure that this task is redone when the versions change.
inputs.property "version", project.version
inputs.property "mcversion", project.minecraft.version

// replace stuff in mcmod.info, nothing else
from(sourceSets.main.resources.srcDirs) {
include 'mcmod.info'

// replace version and mcversion
expand 'version':project.version, 'mcversion':project.minecraft.version
}

// copy everything else except the mcmod.info
from(sourceSets.main.resources.srcDirs) {
exclude 'mcmod.info'
}
}

然后进入Ruby.java里, 修改一下配置, 让forge可以读取到这个mcmod.info文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@Mod(modid = Ruby.modid, useMetadata = true)
public class Ruby {

public static final String modid = "ruby";

private static Logger logger;

@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
}

@Mod.EventHandler
public void init(FMLInitializationEvent event) {

}
}

@Mod这个Annotation(注释) 是让Forge知道, 这个是一个mod的入口, 之中modid是必填, useMetadata 就是会让mcmod.info的内容覆盖你在这个注释中填写的内容.

这样, 进入游戏


就可以看到我们的mod啦. 上面的信息也都是我们设置的,接下来就是新建一个物品, 红宝石锭.

新的物品

在mod包ruby下新建一个包, 叫做items, 并新建一个类 IngotRuby, 并且继承 net.minecraft.item.Item.

无论是一个方块, 还是物品, 都需要一个 unlocalizedName, 这个名字是用于在语言文件中的key, 然后还需要设置一下在这个东西在游戏中的注册名, 比如minecraft:bucket就是桶在游戏中的注册名. 接着就是设置在创造模式中, 他可以在哪个分类中找到.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class IngotRuby extends Item {

private static final String name = "ingotRuby";
private static final String registryName = Ruby.modid + ":" + name;
private static final String unlocalizedName = Ruby.modid + "." + name;

public IngotRuby() {
setUnlocalizedName(unlocalizedName);
setRegistryName(registryName);
setCreativeTab(CreativeTabs.MATERIALS);
}

}

下一步就是为这个物品添加材质, 这里就直接拿铁锭换色吧.(图片上传不到图床, 等下就直接在游戏中展示吧.)

设置材质的话, 把制作好.png放入resources/assets/modid/textures/item/里, 并重命名为这个物品的名字, 并在models/item/里新建一个物品名字的json文件, ingotruby.json

1
2
3
4
5
6
7
8
ingotRuby.json

{
"parent": "item/generated",
"textures": {
"layer0": "ruby:item/ingotruby" // 这个就是材质了, 名字为 modid:item/<item_name>
}
}

在完成这些之后, 我们还需要在游戏中注册一下这个物品才行, 进入到主类 Ruby.java, 在preinit里添加注册需要的代码.

首先需要将这个物品注册到游戏中, 可以调用ForgeRegistries, 它提供了物品, 方块, 等等等, 各种你需要的注册. 根据自己的风格, 可以选择不同的方法, 但总体来讲, 必须要注册才能在游戏中得到它.

我在IngotRuby这个物品中添加了一个方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class IngotRuby extends Item {

private static final String name = "ingotRuby";
private static final String registryName = Ruby.modid + ":" + name;
private static final String unlocalizedName = Ruby.modid + "." + name;



public IngotRuby() {
setUnlocalizedName(unlocalizedName);
setRegistryName(registryName);
setCreativeTab(CreativeTabs.MATERIALS);
}

public void register() {
ForgeRegistries.ITEMS.register(this);
}

}

所以, 在主类中, 新建一个IngotRuby出来, 并且调用它的register方法.

1
2
3
4
5
6
7
8
9
private static IngotRuby ingotRuby;


@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
ingotRuby = new IngotRuby();
ingotRuby.register();
}

注册模型之前

在注册材质之前, 需要理解一些内容, FML是支持客户端与服务器端的mod加载, 但这并不意味着它需要加载所有东西. 比如说在server端, 他就不需要加载材质, 这是一个很没有必要的东西. 因为材质只属于客户端, 所以就延伸出来了proxy, 字面理解是代理, 实际意思也是代理..

万幸的是Forge提供了接口让开发者可以简单的调用, @SidedProxy, 它有两个参数, clientSide和serverSide, 都是选填.

1
2
3
4
5
6
7
8
9
10
11
12

/**
* The full name of the client side class to load and populate.
* Defaults to the nested class named "ClientProxy" in the current class.
*/
String clientSide() default "";

/**
* The full name of the server side class to load and populate.
* Defaults to the nested class named "ServerProxy" in the current class.
*/
String serverSide() default "";

在为空, 也就是不填入的情况下, 它会默认使用当前类中的 ClientProxy和ServerProxy, 因为现在的mod并不复杂, 所以只要在主类中新建两个类就好了, 使我们的主类看起来像这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

@Mod(modid = Ruby.modid, useMetadata = true)
public class Ruby {

public static final String modid = "ruby";

@SidedProxy
private static ServerProxy proxy;

private static Logger logger;
private static IngotRuby ingotRuby;

@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
ingotRuby = new IngotRuby();
ingotRuby.register();
}

@Mod.EventHandler
public void init(FMLInitializationEvent event) {

}

public static class ServerProxy {
public void loadModel() {}
}

public static class ClientProxy extends ServerProxy {
@Override
public void loadModel() {
super.loadModel();
}
}
}

注册模型

我们要把注册模型这部放入ClientProxy里, 因为前面提到过, 服务端是不需要加载模型的.

在ingotRuby里面加入一个方法, 并在ClientProxy.loadModel调用

1
2
3
4
5
6
public void registerTextures(){
Minecraft.getMinecraft().getRenderItem()
.getItemModelMesher()
.register(this,0,
new ModelResourceLocation(ingotRuby.registryName, "inventory"));
}
1
2
3
4
5
@Override
public void loadModel() {
super.loadModel();
ingotRuby.registerTextures();
}

然后在init里加入 proxy.loadModel();

这时候进入游戏, 在创造模式中的菜单里或者通过指令 /give <Player> ruby:ingotRuby 就可以看到这个新建的红宝石锭了

注册语言

当你把鼠标移动到这个物品的时候, 你会发现它的名字并不是一个‘名字’, 而是类似于代号, 或者说key.

在resources/modid/下建立一个新文件夹 lang/ , 并新建一个文件 en_us.lang.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tree resources/

resources
├── assets
│   └── ruby
│   ├── blockstates
│   ├── lang
│   │   └── en_us.lang
│   ├── models
│   │   ├── block
│   │   └── item
│   │   └── ingotruby.json
│   └── textures
│   └── item
│   └── ingotruby.png
├── mcmod.info
└── pack.mcmeta

9 directories, 5 files

里面的每一行都是一个物品的翻译.

1
2
3
en_us.lang

tile.ruby.ingotRuby.name=Ruby Ingot

格式为 item.modid.<itemname>.name=<Name>

如果这个物品是方块的话, 就需要将前面的item换成 tile

这时候再进游戏, 就可以看到物品已经不是像之前一样了, 而是成为了一个通俗易懂的名称.

一些注意事项

很多过时的教程使用了

GameRegistry.register(block)

但这个方法已经被设置为private了, 根据注释

1
2
3
4
5
6
7
8
/**
* This is now private, you should use either ForgeRegistries constants.
* Or the registry passed in during the RegistryEvent.Register<T> event.
*/
private static <K extends IForgeRegistryEntry<K>> K register(K object)
{
return (K)GameData.register_impl(object);
}

要使用ForgeRegistries或者通过RegistryEvent.Register event.注册

根据 pack.mcmeta

A pack_format of 3 should be used starting with Minecraft 1.11. All resources, including language files, should be lowercase (eg: en_us.lang).

所有文件必须是小写的, 包括语言文件.

新的方块

首先是要在xyz.rankki.ruby 下建立一个blocks包, 使现在的mod源码目录看起来像是这样

1
2
3
4
5
6
7
8
9
10
11
12
$ tree java/

java
└── xyz
└── rankki
└── ruby
├── Ruby.java
├── blocks
└── items
└── ingotRuby.java

5 directories, 2 files

并在blocks里新建一个BlockRuby.java, 并继承net.minecraft.block.Block

1
2
3
4
5
6
7
public class BlockRuby extends Block {

public BlockRuby(Material materialIn) {
super(materialIn);
}

}

这个Material是一个方块必须的. 它是这个方块的材质,(不是那个看的), Material定义了这个方块的属性, 比如是不是液体, 或者可不可以阻挡光线, 可不可以阻挡移动之类的. 只要使用普通的岩石(Material.ROCK)就可以了,

同时, 和普通物品一样, 需要设置一些其他基础的信息,registryName, unlocalizedName之类的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BlockRuby extends Block {

private static final String name = "BlockRuby";
public static final String registryName = Ruby.modid + ":" + name;
private static final String unlocalizedName = Ruby.modid + "." + name;

public BlockRuby() {
this(Material.ROCK);
}

public BlockRuby(Material materialIn) {
super(materialIn);
setUnlocalizedName(unlocalizedName);
setRegistryName(registryName);
setCreativeTab(CreativeTabs.BUILDING_BLOCKS);
}

public void register() {
ForgeRegistries.BLOCKS.register(this); //注意这个, 和Items不同.
}

}

这个时候, 进入游戏你会发现它并没有在游戏里出现. 因为方块需要blockstate, 在部署环境的时候已经建立好了这个位置, 在里面新建一个文件, “blockruby.json”, 并打开, 内容如下 也是根据自己的需求进行更改.

1
2
3
4
5
6
7
{
"variants": {
"normal": {
"model": "ruby:blockruby"
}
}
}

然后在models/block里面新建文件 blockruby.json,

1
2
3
4
5
6
{
"parent": "block/cube_all", //因为是红宝石块, 四面都一样, 所以继承cube_all
"textures": {
"all": "ruby:block/blockruby" // 这个就是材质的位置
}
}

然而, 游戏里还是没有你这个方块, 问什么呢?

首先, 我们要知道, 现在制作的这个方块, 它就是个方块, 玩家拿到的都叫物品, 所以也要为这个方块建立一个物品.

在resources里的models/item里添加一个blockruby.json, 但这个内容和物品的内容则不一样, 直接继承方块.

1
2
3
{
"parent": "ruby:block/blockruby"
}

然后在方块的register方法里注册一个物品.

1
ForgeRegistries.ITEMS.register(new ItemBlock(this).setRegistryName(registryName));

就行了, 接下来就是材质的问题, 在前面已经设置好了材质, 你在游戏里放下这个方块也是有材质的, 但这个物品嘛, 就没有材质了, 所以 根据前一章所说的, 加入个registerTextures方法就行了, 并在loadModel调用.

1
2
3
4
5
6
public void registerTextures() {
Minecraft.getMinecraft().getRenderItem()
.getItemModelMesher()
.register(Item.getItemFromBlock(this), 0,
new ModelResourceLocation(registryName, "inventory"));
}

接着在语言文件中添加这个方块的名字

1
tile.ruby.BlockRuby.name=Ruby Block

最后, 进入游戏.

新的配方

第一种注册方法

在IngotRuby.java里添加一个类, registerRecipe(), 同理在BlockRuby.java也添加一个类, 这个是用来注册配方的.

合成配方需要使用

1
2
net.minecraftforge.oredict.ShapedOreRecipe;
net.minecraftforge.oredict.ShapelessOreRecipe;

两个呢, 区别在于Shaped是要完完全全的一样, Shapeless就是Shaped的反义词, 无定形的, 类似蘑菇汤.

那就从红宝石锭的配方开始吧, 代码都是相似的, 讲一个就行了.

在 IngotRuby.java 中添加方法, 并且new一个ShapedOreRecipe出来.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ShapedOreRecipe(ResourceLocation group, Block result, Object... recipe){ 
this(group, new ItemStack(result), recipe);
}
public ShapedOreRecipe(ResourceLocation group, Item result, Object... recipe){
this(group, new ItemStack(result), recipe);
}
public ShapedOreRecipe(ResourceLocation group, @Nonnull ItemStack result, Object... recipe) {
this(group, result, CraftingHelper.parseShaped(recipe));
}

public ShapedOreRecipe(ResourceLocation group, @Nonnull ItemStack result, ShapedPrimer primer){
this.group = group;
output = result.copy();
this.width = primer.width;
this.height = primer.height;
this.input = primer.input;
this.mirrored = primer.mirrored;
}

new ShapedOreRecipe的时候有四种不同的方式, 最后一种, 我们可以暂时忽略, 更多的是关注第三种,

1
2
3
public ShapedOreRecipe(ResourceLocation group, @Nonnull ItemStack result, Object... recipe) { 
this(group, result, CraftingHelper.parseShaped(recipe));
}

第一个参数可以先忽略, 主要是第二个, ItemStack, Stack这个词是栈, 通俗的理解, 堆, 那这个ItemStack是什么?

在游戏初始化的时候, 物品会被加载到内存, 但如果说玩家有64个这个物品呢, 不可能把64个全部加入内存, 因为是重复的东西, 浪费内存, 这时候ItemStack出现了, 这个stack里包含了玩家有多少这个物品, 每个物品的metadata, 等等.

具体的, 等基础教程完事, 写进阶的再说吧. (立下Flag了…

后面跟着的参数, recipe就是配方了, 这个配方是按找行走的, 比如合成一个稿子, 那它的配方就是 “AAA”,” B ”,” B ”, 注意B前后有空格, 这里面A就是矿石, B就是木棍

ok, 可以开始红石锭的制作了.

在前面的registerRecipe方法里, 新建一个ResourceLocation, 并新建一个ShapedOreRecipe

1
2
3
4
public void registerRecipe() {
ResourceLocation location = new ResourceLocation(Ruby.modid, name);
ShapedOreRecipe recipe = new ShapedOreRecipe(location,this, );
}

然后, IDE会报错, 因为还没有添加具体的配方, 在前面也提到过, 红宝石锭的配方是8个红石围着一个铁锭, 根据上面的提示, 配方应该是这样, “AAA”, “ABA”, “AAA”, A是红石,B是铁锭, 所以把配方加到参数里, 在配方后面跟着的就是定义A,B…是什么了, 注意配方用的是 ", 定义的名字用的', 在java中一个是String一个是char有很大的区别, 然后在用中ForgeRegistries注册这个配方, 对了不要忘记要设置一个registername, 和当前item一样就好了.

1
2
3
4
5
6
7
8
9
ResourceLocation location = new ResourceLocation(Ruby.modid, name);
ShapedOreRecipe recipe = new ShapedOreRecipe(location, this,
"AAA",
"ABA",
"AAA",
'A', Items.REDSTONE,
'B', Items.IRON_INGOT);
recipe.setRegistryName(registryName);
ForgeRegistries.RECIPES.register(recipe);

然后在Item类中的register调用registerRecipe

1
2
3
4
public void register() {
ForgeRegistries.ITEMS.register(this);
registerRecipe();
}

然后看一下整体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class IngotRuby extends Item {

private static final String name = "IngotRuby";
public static final String registryName = Ruby.modid + ":" + name;
private static final String unlocalizedName = Ruby.modid + "." + name;

public IngotRuby() {
setUnlocalizedName(unlocalizedName);
setRegistryName(registryName);
setCreativeTab(CreativeTabs.MATERIALS);
}

public void register() {
ForgeRegistries.ITEMS.register(this);
registerRecipe();
}

public void registerTextures() {
Minecraft.getMinecraft().getRenderItem()
.getItemModelMesher()
.register(this, 0,
new ModelResourceLocation(IngotRuby.registryName, "inventory"));
}

public void registerRecipe() {
ResourceLocation location = new ResourceLocation(Ruby.modid, name);
ShapedOreRecipe recipe = new ShapedOreRecipe(location,
this,
"AAA",
"ABA",
"AAA",
'A', Items.REDSTONE,
'B', Items.IRON_INGOT);
recipe.setRegistryName(registryName);
ForgeRegistries.RECIPES.register(recipe);
}

}

运行游戏, 使用这个配方

同理, 我们写一下Ruby Block的配方.

但是使用自己的物品时, 会有一个问题, 这个配方会需要自己的物品, 它不像Minecraft内部物品可以通过Items.xxx获取,这时候, 进入主类, 把ingotRuby设置成public

1
public static IngotRuby ingotRuby;

然后在BlockRuby中的registerRecipe使用Ruby.ingotRuby使用这个红宝石锭.

1
2
3
4
5
6
7
8
9
10
11
public void registerRecipe() {
ResourceLocation location = new ResourceLocation(Ruby.modid, name);
ShapedOreRecipe recipe = new ShapedOreRecipe(location,
this,
"AAA",
"AAA",
"AAA",
'A', Ruby.ingotRuby);
recipe.setRegistryName(registryName);
ForgeRegistries.RECIPES.register(recipe);
}

然后进入游戏, 测试一下红宝石块的配方.

第二种注册方法

Forge实际上也提供了另一种方法注册配方

这个方法不需要写代码, 而是在resources/assets里新建一个recipes/的文件夹, 并新建json文件. 根据里面的内容, Forge会自动读取, 然后注册到游戏中. 所以名字是无所谓的, 但一定要易读, 什么意思? 就是要看一眼就知道他是干什么的, 因为一个方块可能会有多种合成方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ tree resources/
resources/
├── assets
│   └── ruby
│   ├── blockstates
│   │   └── blockruby.json
│   ├── lang
│   │   └── en_us.lang
│   ├── models
│   │   ├── block
│   │   │   └── blockruby.json
│   │   └── item
│   │   ├── blockruby.json
│   │   └── ingotruby.json
│   ├── recipes
│   │   ├── blockruby.json
│   │   ├── ingotruby_from_compose.json
│   │   └── ingotruby_from_decompose.json
│   └── textures
│   ├── block
│   │   └── blockruby.png
│   └── item
│   └── ingotruby.png
├── mcmod.info
└── pack.mcmeta

11 directories, 11 files

像上面,我的Ruby ingot 可以有两种获取方式, 一个是通过红石+铁锭合成, 另一个是分解Ruby Block获得9个, 所以有一个是compose和一个decompose.

在recipes/里面的文件应该使用下面这个的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ingotruby_from_compose.json
{
"type": "minecraft:crafting_shaped",
"pattern": [
"AAA",
"ASA",
"AAA"
],
"key": {
"A": {
"item": "minecraft:redstone"
},
"S": {
"item": "minecraft:iron_ingot"
}
},
"result": {
"item": "ruby:ingotruby",
"data": 0
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ingotruby_from_decompose.json
{
"type": "minecraft:crafting_shaped",
"pattern": [
"A"
],
"key": {
"A": {
"item": "ruby:blockruby"
}
},
"result": {
"item": "ruby:ingotruby",
"count": 9
}
}

然后, 有人问, 如果这个A可以代表不同的东西呢? 那就要把A改成一个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ingotruby_from_compose.json
{
"type": "minecraft:crafting_shaped",
"pattern": [
"AAA",
"ASA",
"AAA"
],
"key": {
"A": [
{
"item": "minecraft:redstone"
},
{
"item": "minecraft:other_item"
}
],
"S": {
"item": "minecraft:iron_ingot"
}
},
"result": {
"item": "ruby:ingotruby",
"data": 0
}
}

上面给出的是Shaped的, 那Shpaeless的又是怎样呢?

同理, 这个也是在recipes/里面, 只不过文件中的type变成了 “crafting_shapeless”, 类似蘑菇汤就是shapeless

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"type": "crafting_shapeless",
"ingredients": [
{
"item": "minecraft:brown_mushroom"
},
{
"item": "minecraft:red_mushroom"
},
{
"item": "minecraft:bowl"
}
],
"result": {
"item": "minecraft:mushroom_stew"
}
}

区别在于, Shpaed的配方需要pattern和key, 而Shapeless的只需要提供Ingredients(材料).

当然, 更推荐第二种合成方式, 不仅仅是因为当你打开原生版本的jar文件, Minecraft原生使用了这种方法. 同时这种方法更容易管理, 以及改动时, 不需要动代码.

新的熔炼

熔炼也是Minecraft原生中重要的一个部分, 烧铁矿石才能得到铁锭, 所以要新建一个矿石, 红宝石矿石 (Ruby Ore), 在这章之后, 应该会有一章讲如何讲矿石加入世界的生成里, 比如原生中的铁,煤,钻石等

……… 根据前面的内容制作

制作完成之后, 目录结构应该差不多是这样.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ tree src/

src
└── main
├── java
│   └── xyz
│   └── rankki
│   └── ruby
│   ├── Ruby.java
│   ├── blocks
│   │   ├── BlockOreRuby.java
│   │   └── BlockRuby.java
│   └── items
│   └── IngotRuby.java
└── resources
├── assets
│   └── ruby
│   ├── blockstates
│   │   ├── blockoreruby.json
│   │   └── blockruby.json
│   ├── lang
│   │   └── en_us.lang
│   ├── models
│   │   ├── block
│   │   │   ├── blockoreruby.json
│   │   │   └── blockruby.json
│   │   └── item
│   │   ├── blockoreruby.json
│   │   ├── blockruby.json
│   │   └── ingotruby.json
│   ├── recipes
│   │   ├── blockruby.json
│   │   ├── ingotruby_from_compose.json
│   │   └── ingotruby_from_decompose.json
│   └── textures
│   ├── block
│   │   ├── blockoreruby.png
│   │   └── blockruby.png
│   └── item
│   └── ingotruby.png
├── mcmod.info
└── pack.mcmeta

19 directories, 20 files

和配方不同, 熔炼暂时不支持使用recipes/xxx.json代替代码. 据说1.13会支持.

所以在我们新建好的BlockOreRuby.java新加一个方法 registerSmelt然后调用GameRegistry.addSmelting();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void addSmelting(Block input, @Nonnull ItemStack output, float xp)
{
FurnaceRecipes.instance().addSmeltingRecipeForBlock(input, output, xp);
}

public static void addSmelting(Item input, @Nonnull ItemStack output, float xp)
{
FurnaceRecipes.instance().addSmelting(input, output, xp);
}

public static void addSmelting(@Nonnull ItemStack input, @Nonnull ItemStack output, float xp)
{
FurnaceRecipes.instance().addSmeltingRecipe(input, output, xp);
}

本质上都是一样的, 根据需要直接调用

1
GameRegistry.addSmelting(this, new ItemStack(Ruby.ingotRuby), 1f);

最后一个xp, 烧一个金矿也就1f所有这里也先设置成1f.

然后在register方法里调用,并进入游戏.

生成器

GameRegistry.registerWorldGenerator

MinecraftForge提供了一个借口, IWorldGenerator,就是在世界初始化的时候根据种子进行生成世界方块的一个东西.

首先在项目里新建一个包, generator/ 并新建一个类, 就叫OreRubyGeneator了, 使这个红宝石mod的项目看起来像这样.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ tree src/
src/
└── main
├── java
│   └── xyz
│   └── rankki
│   └── ruby
│   ├── Ruby.java
│   ├── blocks
│   │   ├── BlockOreRuby.java
│   │   └── BlockRuby.java
│   ├── generator
│   │   └── OreRubyGenerator.java
│   └── items
│   └── IngotRuby.java
└── resources
├── assets
│   └── ruby
│   ├── blockstates
│   │   ├── blockoreruby.json
│   │   └── blockruby.json
│   ├── lang
│   │   └── en_us.lang
│   ├── models
│   │   ├── block
│   │   │   ├── blockoreruby.json
│   │   │   └── blockruby.json
│   │   └── item
│   │   ├── blockoreruby.json
│   │   ├── blockruby.json
│   │   └── ingotruby.json
│   ├── recipes
│   │   ├── blockruby.json
│   │   ├── ingotruby_from_compose.json
│   │   └── ingotruby_from_decompose.json
│   └── textures
│   ├── block
│   │   ├── blockoreruby.png
│   │   └── blockruby.png
│   └── item
│   └── ingotruby.png
├── mcmod.info
└── pack.mcmeta

20 directories, 21 files

然后在OreRubyGenerator类中引入net.minecraftforge.fml.common.IWorldGenerator.并加入方法

1
2
3
4
5
@Override
public void generate(Random random, int chunkX, int chunkZ, World world,
IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {

}

其中的random是根据世界种子为种子建立出的一个Random对象, chunkX,chunkZ则是这个chunk的位置. world为当前世界对象, chunkGenerator和chunkProvide目前用不到, 就先跳过.

在这份generate方法里是我们需要随机出来3个指, x,y,z 用于生成矿石所在位置, 顺带一提, 每一个chunk是一个16*16的大小.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OreRubyGenerator implements IWorldGenerator {

@Override
public void generate(Random random, int chunkX, int chunkZ, World world,
IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
if (world.provider.getDimension() == 0) {
int x = chunkX * 16 + random.nextInt(16);
int y = 13 + random.nextInt(60);
int z = chunkZ * 16 + random.nextInt(16);

IBlockState blockState = Ruby.blockOreRuby.getDefaultState();
WorldGenMinable worldGenMinable = new WorldGenMinable(blockState, 20);
worldGenMinable.generate(world, random, new BlockPos(x, y, z));
}
}
}

根据world.provider.getDimension()获取的指是代表着这个世界的纬度, 0为普通世界, -1是地狱, 1是末地

1
2
3
int x = chunkX * 16 + random.nextInt(16);
int y = 13 + random.nextInt(50);
int z = chunkZ * 16 + random.nextInt(16);

这是生成x,y,z在世界中的位置, 因为chunkX和chunkZ代表着chunk在世界的位置, 再加上每个chunk是16*16的大小,所以将其乘上16就是在世界中的绝对位置, 在加上0-16中的一个随机数, 随机一下这个位置. y则是看心情, 可以在设置成任意高度的随机数. 13 + 随机数代表了最低高度也是13层. 最高则是63层.

详细的WorldGenMinable

WorldGenMinable这个类也是Minecraft提供的一个结构, 可以生成随机矿石, 具体的算法有兴趣的可以自己看一看, 这里就直接调用了, 在新建一个WorldGenMinalbe对象的时候, 需要提供两个参数

1
2
3
public WorldGenMinable(IBlockState state, int blockCount){
this(state, blockCount, new WorldGenMinable.StonePredicate());
}

IBlcokState和blockcount, 第一个是方块的state, 第二个是一次生成多少个.

但你会发现, 他其实是有三个参数的, 最后一个是Predicate, 根据查看源码,

1
2
3
if (state.getBlock().isReplaceableOreGen(state, worldIn, blockpos, this.predicate)){
worldIn.setBlockState(blockpos, this.oreBlock, 2);
}

在生成矿石算法的最后一步, 会对当前的方块嘴个判断, isReplaceableOreGen 是否可以被矿石替换, 这里就用到了Predicate.

再进一步看源码,

1
2
3
public boolean isReplaceableOreGen(IBlockState state, IBlockAccess world, BlockPos pos, Predicate<IBlockState> target){
return target.apply(state);
}

是根据Predicate target判断是否可以替换矿石的.

在前面没有提供这个target的时候, 默认的WorldGenMinable.StonePredicate().

1
2
3
4
5
6
7
8
9
10
11
12
13
static class StonePredicate implements Predicate<IBlockState>{
private StonePredicate(){
}

public boolean apply(IBlockState p_apply_1_){
if (p_apply_1_ != null && p_apply_1_.getBlock() == Blocks.STONE){
BlockStone.EnumType blockstone$enumtype = (BlockStone.EnumType)p_apply_1_.getValue(BlockStone.VARIANT);
return blockstone$enumtype.isNatural();
}else{
return false;
}
}
}

他是判断了当前的方块是否为石头, 并且判断了这个是石头的类型是否为Natural, 那根据代码, 为Natural的石头只有

1
2
3
4
STONE(0, MapColor.STONE, "stone", true),
GRANITE(1, MapColor.DIRT, "granite", true),
DIORITE(3, MapColor.QUARTZ, "diorite", true),
ANDESITE(5, MapColor.STONE, "andesite", true),

这四个, 如果碰到其他类型的时候, 他就不会替换.

回到主题

根据

1
2
IBlockState blockState = Ruby.blockOreRuby.getDefaultState();
WorldGenMinable worldGenMinable = new WorldGenMinable(blockState, 20);

建立好了一个WorldGenMinalbe对象, 生成的是blockOreRuby, 一次生成20个, 那下一步就是生成了.

1
worldGenMinable.generate(world, random, new BlockPos(x, y, z));

将当前world和random对象传入, 以上面生成的xyz新建一个世界方块位置.

以上就是世界初始化时如何加入自己的矿石的步骤,

注册到游戏

但是只有以上的代码并没有用, 因为还没有注册到游戏.

根据个人代码风格, 无论是在主类直接调用GameRegistry.registerWorldGenerator还是在这个generator里新建一个静态方法调用,(只要用了就行

我实在这个generator里新建了一个静态方法init()

1
2
3
public static void init() {
GameRegistry.registerWorldGenerator(new OreRubyGenerator(), 0);
}

并在主类的注册之中调用, OreRubyGenerator.init();

使用GameRegistry.registerWorldGenerator需要传入两个参数, 第一个自然是generator对象, 第二个是权重.

1
2
3
4
5
6
7
/**
* Register a world generator - something that inserts new block types into the world
*
* @param generator the generator
* @param modGenerationWeight a weight to assign to this generator. Heavy weights tend to sink to the bottom of
* list of world generators (i.e. they run later)
*/

根据注释, 这个权重代表着执行顺序, 越大的数字, 执行顺序就越靠后. (所以设置为0.

使用事件监听器

先上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OreRubyGenerator {

private static WorldGenMinable worldGenMinable = new WorldGenMinable(Blocks.blockOreRuby.getDefaultState(), 20);

public static void init() {
MinecraftForge.ORE_GEN_BUS.register(OreRubyGenerator.class);
}

@SubscribeEvent
public static void onGenerateMinable(OreGenEvent.GenerateMinable event) {
if (event.getType() != OreGenEvent.GenerateMinable.EventType.IRON){
return;
}


if (!TerrainGen.generateOre(event.getWorld(), event.getRand(), worldGenMinable, event.getPos(),
OreGenEvent.GenerateMinable.EventType.CUSTOM)){
return;
}

for (int i = 0; i < 4; i++) {
int posX = event.getPos().getX() + event.getRand().nextInt(16);
int posY = 16 + event.getRand().nextInt(40);
int posZ = event.getPos().getZ() + event.getRand().nextInt(16);
worldGenMinable.generate(event.getWorld(), event.getRand(), new BlockPos(posX, posY, posZ));
}
}
}

在onGenerateMinable这个事件里

  • 第一个if判断是判断生成的是不是铁, 这个可以让这个红宝石矿和铁一起生成.
  • 第二个if是告诉游戏, “诶诶诶, 我现在要生成红宝石矿了”
  • 第三个for是让这个循环四次, 因为在一个chunk了, 不可能只生成一次, 这里就可以靠喜好了.

然后, 进入游戏, 找新矿石吧.

新的工具

新的工具, 需要一个新的材质, 以及一个新的模型和类. 根据以往的经验可以直接新建这些.然后目录结构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ tree src/
src/
└── main
├── java
│   └── xyz
│   └── rankki
│   └── ruby
│   ├── Ruby.java
│   ├── blocks
│   │   ├── BlockOreRuby.java
│   │   └── BlockRuby.java
│   ├── generator
│   │   └── OreRubyGenerator.java
│   └── items
│   ├── IngotRuby.java
│   └── PickaxeRuby.java
└── resources
├── assets
│   └── ruby
│   ├── blockstates
│   │   ├── blockoreruby.json
│   │   └── blockruby.json
│   ├── lang
│   │   └── en_us.lang
│   ├── models
│   │   ├── block
│   │   │   ├── blockoreruby.json
│   │   │   └── blockruby.json
│   │   └── item
│   │   ├── blockoreruby.json
│   │   ├── blockruby.json
│   │   ├── ingotruby.json
│   │   └── pickaxeruby.json
│   ├── recipes
│   │   ├── blockruby.json
│   │   ├── ingotruby_from_compose.json
│   │   └── ingotruby_from_decompose.json
│   └── textures
│   ├── blocks
│   │   ├── blockoreruby.png
│   │   └── blockruby.png
│   └── items
│   ├── ingotruby.png
│   └── pickaxeruby.png
├── mcmod.info
└── pack.mcmeta

20 directories, 24 files

update. 注意, 在models里的pickaxeRuby.json的内容的parent和以往普通物品不同, 它的parent应该是item/handheld

1
2
3
4
5
6
{
"parent": "item/handheld",
"textures": {
"layer0": "ruby:items/pickaxeruby"
}
}

首先, 需要让PickaxeRuby类继net.minecraft.item.ItemPickaxe, 因为这个部分就是设计一个红宝石镐.

ItemPickaxe需要传入一个工具材质, 这里直接用钻石等级的材质.

1
2
3
public PickaxeRuby() {
super(ToolMaterial.DIAMOND);
}

然后在主类和其他物品一样, 新建对象, 调用方法, 并在语言文件中给它一个名字.

另一个类

但这样的话, 这个镐子必须要继承ItemPickaxe所设置的配置, 如果想自定义怎么办, 比如攻击造成的伤害量, 冷却时间之类的, 那这里就需要直接继承ItemTool

1
2
3
4
protected ItemTool(float attackDamageIn, float attackSpeedIn, 
Item.ToolMaterial materialIn, Set<Block> effectiveBlocksIn){

}

第一个浮点值是攻击伤害量, 第二个冷却时间(正数减小冷却,负数增加冷却), 第三个则是物品材质, 第四个是对特殊方块的速度加成.

然后这样还不够, 因为他只是个工具, 必须要让他和镐子一样可以挖掘方块.

1
setHarvestLevel("pickaxe", 3);

让这个工具成为一个第三等级的镐子,

1
2
3
4
5
6
7
8
/**
* @param level Harvest level:
* Wood: 0
* Stone: 1
* Iron: 2
* Diamond: 3
* Gold: 0
*/

继续制作工具

那根据以上的信息,写代码, 并新建register方法们.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PickaxeRuby extends ItemPickaxe {

private static final String name = "PickaxeRuby";
public static final String registryName = Ruby.modid + ":" + name;
private static final String unlocalizedName = Ruby.modid + "." + name;

public PickaxeRuby() {
super(ToolMaterial.DIAMOND);
setRegistryName(registryName);
setUnlocalizedName(unlocalizedName);
}

public void register() {
ForgeRegistries.ITEMS.register(this);
}

public void registerTextures() {
Minecraft.getMinecraft().getRenderItem()
.getItemModelMesher()
.register(this, 0,
new ModelResourceLocation(registryName, "inventory"));
}
}

可能有人会问, 为什么没有设置在创造模式中的菜单位置, 因为在ItemTool类中, 已经调用了this.setCreativeTab(CreativeTabs.TOOLS);

合成配方

在recipes/里新建一个文件, pickaxeruby.json, 根据普通镐子来做配方.

木根的id是minecraft:stick

注意在B的左右是有空格的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"type": "minecraft:crafting_shaped",
"pattern": [
"AAA",
" B ",
" B "
],
"key": {
"A": {
"item": "ruby:ingotruby"
},
"B": {
"item": "minecraft:stick"
}
},
"result": {
"item": "ruby:pickaxeruby"
}
}

然后就是进入游戏.

因为挖矿速度比较快, 截不到图.. 就放一个背包里的图吧.

打包

打包可以说是很重要的一步, 因为做好的mod当然要分享出去, 给别人玩啦. 或者说放到自己的游戏里, 毕竟mdk里的Minecraft是开发用的.

update, 代码被改了些… 更新部分可以到github#296d451查看

Here we go

在完成所有基础部分后, 目录应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ tree src/
src
└── main
├── java
│   └── xyz
│   └── rankki
│   └── ruby
│   ├── Ruby.java
│   ├── blocks
│   │   ├── BlockOreRuby.java
│   │   └── BlockRuby.java
│   ├── generator
│   │   └── OreRubyGenerator.java
│   └── items
│   ├── IngotRuby.java
│   └── PickaxeRuby.java
└── resources
├── assets
│   └── ruby
│   ├── blockstates
│   │   ├── blockoreruby.json
│   │   └── blockruby.json
│   ├── lang
│   │   └── en_us.lang
│   ├── models
│   │   ├── block
│   │   │   ├── blockoreruby.json
│   │   │   └── blockruby.json
│   │   └── item
│   │   ├── blockoreruby.json
│   │   ├── blockruby.json
│   │   ├── ingotruby.json
│   │   └── pickaxeruby.json
│   ├── recipes
│   │   ├── blockruby.json
│   │   ├── ingotruby_from_compose.json
│   │   ├── ingotruby_from_decompose.json
│   │   └── pickaxeruby.json
│   └── textures
│   ├── blocks
│   │   ├── blockoreruby.png
│   │   └── blockruby.png
│   └── items
│   ├── ingotruby.png
│   └── pickaxeruby.png
├── mcmod.info
└── pack.mcmeta

20 directories, 25 files

然后打开根目录下的build.gradle,找到

1
2
3
version = "1.0"
group = "xyz.rankki.ruby" // http://maven.apache.org/guides/mini/guide-naming-conventions.html
archivesBaseName = "Ruby"

并根据需要进行更改, 然后在mdk目录下, 输入

1
$ ./gradlew build

等待它配置并编译完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
To honour the JVM settings for this build a new JVM will be forked. 
Please consider using the daemon: https://docs.gradle.org/2.14/userguide/gradle_daemon.html.
This mapping 'snapshot_20171003' was designed for MC 1.12! Use at your own peril.
#################################################
ForgeGradle 2.3-SNAPSHOT-ee3e3df
https://github.com/MinecraftForge/ForgeGradle
#################################################
Powered by MCP
http://modcoderpack.com
by: Searge, ProfMobius, R4wk, ZeuX
Fesh0r, IngisKahn, bspkrs, LexManos
#################################################
:deobfCompileDummyTask
:deobfProvidedDummyTask
:sourceApiJava
:compileApiJava UP-TO-DATE
:processApiResources UP-TO-DATE
:apiClasses UP-TO-DATE
:sourceMainJava
:compileJava
:processResources
:classes
:jar
:sourceTestJava
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:extractMcpData SKIPPED
:extractMcpMappings SKIPPED
:getVersionJson
:extractUserdev UP-TO-DATE
:genSrgs SKIPPED
:reobfJar
:extractAnnotationsJar
:extractRangemapReplacedMain
:retromapReplacedMain
remapping source...
:sourceJar
:assemble
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 32.661 secs

进入根目录的build/文件夹, 这里就是编译完的mod文件们, 包括了源码, 编译好的.class文件, recourses文件和打包好的libs文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
$ tree build/
build
├── classes
│   └── main
│   └── xyz
│   └── rankki
│   └── ruby
│   ├── Ruby$ClientProxy.class
│   ├── Ruby$ServerProxy.class
│   ├── Ruby.class
│   ├── blocks
│   │   ├── BlockOreRuby.class
│   │   └── BlockRuby.class
│   ├── generator
│   │   └── OreRubyGenerator.class
│   └── items
│   ├── IngotRuby.class
│   └── PickaxeRuby.class
├── dependency-cache
├── libs
│   ├── Ruby-1.0-sources.jar
│   └── Ruby-1.0.jar
├── resources
│   └── main
│   ├── assets
│   │   └── ruby
│   │   ├── blockstates
│   │   │   ├── blockoreruby.json
│   │   │   └── blockruby.json
│   │   ├── lang
│   │   │   └── en_us.lang
│   │   ├── models
│   │   │   ├── block
│   │   │   │   ├── blockoreruby.json
│   │   │   │   └── blockruby.json
│   │   │   └── item
│   │   │   ├── blockoreruby.json
│   │   │   ├── blockruby.json
│   │   │   ├── ingotruby.json
│   │   │   └── pickaxeruby.json
│   │   ├── recipes
│   │   │   ├── blockruby.json
│   │   │   ├── ingotruby_from_compose.json
│   │   │   ├── ingotruby_from_decompose.json
│   │   │   └── pickaxeruby.json
│   │   └── textures
│   │   ├── blocks
│   │   │   ├── blockoreruby.png
│   │   │   └── blockruby.png
│   │   └── items
│   │   ├── ingotruby.png
│   │   └── pickaxeruby.png
│   ├── mcmod.info
│   └── pack.mcmeta
├── retromapping
│   ├── replacedMain.rangemap
│   └── retromappedReplacedMain.jar
├── sources
│   ├── api
│   │   └── java
│   ├── main
│   │   └── java
│   │   └── xyz
│   │   └── rankki
│   │   └── ruby
│   │   ├── Ruby.java
│   │   ├── blocks
│   │   │   ├── BlockOreRuby.java
│   │   │   └── BlockRuby.java
│   │   ├── generator
│   │   │   └── OreRubyGenerator.java
│   │   └── items
│   │   ├── IngotRuby.java
│   │   └── PickaxeRuby.java
│   └── test
│   └── java
├── taskLogs
│   ├── extractRangemapReplacedMain.log
│   └── retromapReplacedMain.log
└── tmp
├── compileJava
│   └── emptySourcePathRef
├── extractAnnotationsJar
├── jar
│   └── MANIFEST.MF
├── reobfJar
├── retromapReplacedMain
│   └── rangelog7060861889199480268.txt
└── sourceJar
└── MANIFEST.MF

46 directories, 42 files

这里主要是专注于libs/文件夹里的两个文件

1
2
3
4
5
6
$ tree build/libs/
build/libs
├── Ruby-1.0-sources.jar
└── Ruby-1.0.jar

0 directories, 2 files

带sources的是源码, 另一个是编译好的class文件. 只要把Ruby-1.0.jar复制出来分享到网络上或者放在在自己的游戏文件中的mods文件夹中就好了.