本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-DataGen-1.20.1

本篇教程目标

  • 学会使用数据生成
  • 学会利用原版已有数据生成方法改写自定义的数据生成方法

注意事项

请确保在第一期模板生成已经选择了Data Generation选项,不然你得自己去配置数据生成相关的内容

fabric.mod.json文件中的entrypoints要有fabric-datagen及其对应的数据生成类

模组的数据生成主类实现DataGeneratorEntrypoint接口,并重写onInitializeDataGenerator方法

各个数据生成类

前述

数据生成是我们模组开发中一个非常好用的工具,用于帮助我们生成各种各样的json文件

不知道大家前面写的物品方块模型文件、方块状态文件、语言文件、战利品列表配方等等json文件,
有没有写得头大,往往会写错命名空间或者方块、物品的名字,导致加载失败

那么现在,我们使用数据生成就可以避免这些问题

不过,为什么说我们的教程并不是一开始就来讲数据生成呢?因为你终归还是要理解json才能来写数据生成的东西

虽然本期教程用的都是原版的数据生成方法和Fabric API,但当我们加入自己的一些东西时,
比如我们未来会讲的方块状态,在你没有理解方块状态文件的各种参数之前,那是写不出来的;
方块实体的自定义配方同样如此

像战利品列表中的随机池,里面的参数有一堆,复合抽取项单一抽取项特殊抽取项,它们下面还有各个不同的值;如果你这些没能理解,
那自然也不会写

那么在你理解json文件的格式之后,你不仅可以自己去写各种数据生成方法,同
样也可以使用其他语言来写json文件——你觉得模组开发中的数据生成类不够直观,
引入图形界面来辅助(当然,其实有网站已经实现了)

所以,终归还是得理解各类json的基本格式,才能更好地使用数据生成,而不是我一上来就讲数据生成

源代码的话其实没什么好讲的,因为其实质就是拿Java生成json文件,而且本期要讲的东西稍微有点多,
所以我们直接来讲数据生成

Tag数据生成

那么Tag分为很多,用到的最多的是方块物品的标签,这里我们先写两个类

ModBlockTagsProvider

方块的标签我们在前面写战利品列表的时候已经接触过了,我们这里创建一个ModBlockTagsProvider类,
继承FabricTagProvider.BlockTagProvider类,并实现super方法

还有要重写configure方法

1
2
3
4
5
6
7
8
9
10
public class ModBlockTagsProvider extends FabricTagProvider.BlockTagProvider {
public ModBlockTagsProvider(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registriesFuture) {
super(output, registriesFuture);
}

@Override
protected void configure(RegistryWrapper.WrapperLookup wrapperLookup) {

}
}

那么接下来就是在configure方法中写数据生成方法

这里我们使用getOrCreateTagBuilder方法来写

1
2
3
4
5
6
7
8
9
@Override
protected void configure(RegistryWrapper.WrapperLookup wrapperLookup) {
getOrCreateTagBuilder(BlockTags.PICKAXE_MINEABLE)
.add(ModBlocks.ICE_ETHER_BLOCK)
.add(ModBlocks.ICE_ETHER_ORE);

getOrCreateTagBuilder(BlockTags.NEEDS_IRON_TOOL)
.add(ModBlocks.ICE_ETHER_ORE);
}

getOrCreateTagBuilder要传入的参数是一个BlockTag,BlockTags里面注册有原版所有的方块标签,
这里是镐子能挖掘的方块标签,其对应的就是我们之前写的mineable文件夹下的pickaxe.json(这个你看BlockTags在它的注册名就知道了)

再后面就是所以add方法,将我们模组的方块传入

另一个需要铁质工具采集的方块标签同样如此

另外,和add类似的还有一个forceAddTag方法,相比之下,这个方法是加入Tag的,而不是加入方块的

在本系列重制的教程中,探矿器这个例子去掉了,旧教程中还有,它是与后面一期的Tag配合起来的,
其中,在数据生成中,就使用了forceAddTag方法,将原版的各种矿石加入

虽然我们这里还没到Tag的教程,不过感兴趣的同学可以去看看旧教程

探矿器旧教程:探矿器

Tag旧教程:Tag

ModItemTagsProvider

那么和方块标签类似的,物品的也差不多

我们创建一个ModItemTagsProvider类,继承FabricTagProvider.ItemTagProvider类,
并实现super方法

这里要注意,用IDEA来补全super方法时,选择的构造函数,一定一定要选择形参只有两个的构造函数!!!

那三个形参的最后一个是给BlockTag

另外也是重写configure方法

1
2
3
4
5
6
7
8
9
10
public class ModItemTagsProvider extends FabricTagProvider.ItemTagProvider {
public ModItemTagsProvider(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> completableFuture) {
super(output, completableFuture);
}

@Override
protected void configure(RegistryWrapper.WrapperLookup wrapperLookup) {

}
}

不过,这里我们暂时还没有物品的标签要写,所以就先空着好了

语言文件

那么现在,我们讲语言文件的生成,这里就讲美式英语语言文件和简体中文语言文件的编写

美式英语

这里我们创建一个ModEnUsLangProvider类,继承FabricLanguageProvider类,实现super方法

不过,这里我们选择构造函数的时候,可以只选择只有一个形参的,然后在super方法中指定语言文件的名字

1
2
3
4
5
6
7
8
9
10
public class ModEnUsLangProvider extends FabricLanguageProvider {
public ModEnUsLangProvider(FabricDataOutput dataOutput) {
super(dataOutput, "en_us");
}

@Override
public void generateTranslations(TranslationBuilder translationBuilder) {

}
}

另外重写generateTranslations方法

generateTranslations方法中,我们就可以来写语言文件的数据生成方法,
使用translationBuilder.add方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void generateTranslations(TranslationBuilder translationBuilder) {
translationBuilder.add(ModItems.ICE_ETHER, "Ice Ether");
translationBuilder.add(ModItems.RAW_ICE_ETHER, "Raw Ice Ether");
translationBuilder.add(ModItems.CARDBOARD, "Cardboard");

translationBuilder.add(ModBlocks.ICE_ETHER_BLOCK, "Ice Ether Block");
translationBuilder.add(ModBlocks.RAW_ICE_ETHER_BLOCK, "Raw Ice Ether Block");
translationBuilder.add(ModBlocks.ICE_ETHER_ORE, "Ice Ether Ore");

translationBuilder.add(ModItemGroups.TUTORIAL_GROUP, "Tutorial Group");
translationBuilder.add("itemGroup.tutorial_group2", "Tutorial Group2");
}

add有一堆重载方法,第一个参数可以接受物品方块物品栏注册键字符串等,
第二个参数为字符串,也是翻译后的内容

在这里,我们可以发现创造模式物品栏那期教程中,我特地提到的它们的区别

第一个参数只能接受物品栏的注册键RegistryKey<ItemGroup>),而不能是物品栏ItemGroup

所以我们使用第二种简化方法写的,也就只能拿翻译键来翻译了

简体中文

那简体中文的语言文件也是一样的,只是在super中,指定的是zh_cn

我们创建ModZhCnLangProvider类,继承FabricLanguageProvider类,实现super方法

1
2
3
4
5
6
7
8
9
10
public class ModZhCnLangProvider extends FabricLanguageProvider {
public ModZhCnLangProvider(FabricDataOutput dataOutput) {
super(dataOutput, "zh_cn");
}

@Override
public void generateTranslations(TranslationBuilder translationBuilder) {

}
}

战利品列表文件

接下来我们创建ModLootTableProvider类,继承FabricBlockLootTableProvider类,实现super方法

还要重写generate方法

1
2
3
4
5
6
7
8
9
10
public class ModLootTableProvider extends FabricBlockLootTableProvider {
public ModLootTableProvider(FabricDataOutput dataOutput) {
super(dataOutput);
}

@Override
public void generate() {

}
}

同样的,我们在generate方法中来写

1
2
3
4
5
6
@Override
public void generate() {
addDrop(ModBlocks.ICE_ETHER_BLOCK);
addDrop(ModBlocks.RAW_ICE_ETHER_BLOCK);
addDrop(ModBlocks.ICE_ETHER_ORE, oreDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER));
}

这里我们使用addDrop方法来为方块添加战利品列表

其中我们为矿石再加入一个矿石的战利品列表生成方法,这个生成方法返回类型是LootTable.Builder
这也是addDrop方法中第二个形参的类型

这个oreDrops方法,第一个参数是方块,第二个参数是掉落物
另外精准采集这个附魔的特殊情况也会在最终文件中生成

但是,这个矿石的掉落只有一个,而我们想像铜矿石青金石等这样能够掉落多个的,怎么办呢?
找源代码中的方法

1
2
3
4
5
6
7
8
9
10
11
public LootTable.Builder copperOreDrops(Block drop) {
return dropsWithSilkTouch(
drop,
(LootPoolEntry.Builder<?>)this.applyExplosionDecay(
drop,
ItemEntry.builder(Items.RAW_COPPER)
.apply(SetCountLootFunction.builder(UniformLootNumberProvider.create(2.0F, 5.0F)))
.apply(ApplyBonusLootFunction.oreDrops(Enchantments.FORTUNE))
)
);
}

我们在BlockLootTableGenerator类中可以找到这么一个方法

dropsWithSilkTouch方法,即为在生成时加上精准采集附魔的情况,这是它的原方法(得追个几层)

1
2
3
4
public static LootTable.Builder dropsWithSilkTouch(ItemConvertible drop) {
return LootTable.builder()
.pool(LootPool.builder().conditionally(WITH_SILK_TOUCH).rolls(ConstantLootNumberProvider.create(1.0F)).with(ItemEntry.builder(drop)));
}

再往后,applyExplosionDecay方法,即爆炸衰减,这个是我们在之前的战利品列表那期也探讨过

它接受的参数是方块掉落物,这里的掉落物是粗铜

这里的掉落物使用ItemEntry.builder来创建,里面填掉落物,后面的apply则写我们在之前教程中讲到过的各个function,即物品修饰器

第一个那自然是修改数量的,这里让粗铜掉落2~5

第二个是根据时运附魔来修改掉落物数量

然鹅,我们想用还不能用,因为这里的ItemEntry.builder(Items.RAW_COPPER)是写死的粗铜,
另外的青金石红石也是这样,所以我们不妨自己写一个类似的

在我们数据生成类,创建一个likeCopperOreDrops方法

1
2
3
4
5
6
7
8
9
10
11
public LootTable.Builder likeCopperOreDrops(Block drop, Item item, float min, float max) {
return dropsWithSilkTouch(
drop,
(LootPoolEntry.Builder<?>)this.applyExplosionDecay(
drop,
ItemEntry.builder(item)
.apply(SetCountLootFunction.builder(UniformLootNumberProvider.create(min, max)))
.apply(ApplyBonusLootFunction.oreDrops(Enchantments.FORTUNE))
)
);
}

这里我们把ItemEntry.builder中的Items.RAW_COPPER改为item,后面两个参数为掉落物数量的最小值最大值

并在形参中加入相关的参数,这样,我们就可以自定义掉落物掉落数量

然后我们就可以在generate方法中使用这个方法了

1
2
// addDrop(ModBlocks.ICE_ETHER_ORE, oreDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER));
addDrop(ModBlocks.ICE_ETHER_ORE, likeCopperOreDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER, 2.0f, 5.0f));

那么,游戏中其他的,非一般的战利品列表同样如此,大家可以自行探索

模型文件

随后我们创建ModModelsProvider类,继承FabricModelProvider,实现super方法

还要重写generateBlockStateModelsgenerateItemModels方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ModModelsProvider extends FabricModelProvider {
public ModModelsProvider(FabricDataOutput output) {
super(output);
}

@Override
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {

}

@Override
public void generateItemModels(ItemModelGenerator itemModelGenerator) {

}
}

generateBlockStateModels方法,显然易见,是用于生成方块的方块状态文件的,不过,
它也会生成方块方块物品的模型文件

1
2
3
4
5
6
@Override
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.ICE_ETHER_BLOCK);
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.RAW_ICE_ETHER_BLOCK);
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.ICE_ETHER_ORE);
}

使用blockStateModelGenerator.registerSimpleCubeAll来生成我们模组的方块相关文件,
这里的CubeAll其实就是我们前面写的最简单的方块模型,即六面相同

这个方法只要将方块传入即可

未来我们还将遇到更多的方法,用于生成带有不同属性的方块状态文件

generateItemModels方法,则是用于生成物品的模型文件的

1
2
3
4
5
6
@Override
public void generateItemModels(ItemModelGenerator itemModelGenerator) {
itemModelGenerator.register(ModItems.ICE_ETHER, Models.GENERATED);
itemModelGenerator.register(ModItems.RAW_ICE_ETHER, Models.GENERATED);
itemModelGenerator.register(ModItems.CARDBOARD, Models.GENERATED);
}

类似的,使用itemModelGenerator.register来生成

这里面传入两个参数,第一个是物品,第二个是模型类型,这里我们使用Models.GENERATED
这也是我们之前写过的,最简单的物品模型

配方文件

最后,我们就来到了配方文件,创建ModRecipesProvider类,继承FabricRecipeProvider,实现super方法

还要重写generate方法

1
2
3
4
5
6
7
8
9
10
public class ModRecipesProvider extends FabricRecipeProvider {
public ModRecipesProvider(FabricDataOutput output) {
super(output);
}

@Override
public void generate(Consumer<RecipeJsonProvider> exporter) {

}
}

generate方法,用于生成我们模组的配方文件

9->1 | 1 -> 9 可逆合成配方

我们先来写1个方块合成9个物品和9个物品合成1个方块的配方,这个直接用原版已经封装好的方法即可

1
2
3
4
offerReversibleCompactingRecipes(exporter, RecipeCategory.MISC, ModItems.ICE_ETHER,
RecipeCategory.BUILDING_BLOCKS, ModBlocks.ICE_ETHER_BLOCK);
offerReversibleCompactingRecipes(exporter, RecipeCategory.MISC, ModItems.RAW_ICE_ETHER,
RecipeCategory.BUILDING_BLOCKS, ModBlocks.RAW_ICE_ETHER_BLOCK);

这里我们直接写两个,用offerReversibleCompactingRecipes方法来创建,一个方法即可生成两个配方

这个方法接受5个参数,第一个是配方导出器,后面的分别的配方类型合成产物
两两一块一起看——即前面两个是合成ICE ETHER时,所属的配方类型和合成产物(ICE ETHER);
后面两个是合成ICE ETHER BLOCK时,所属的配方类型和合成产物(ICE ETHER BLOCK

另一个也是同样的

熔炼配方

熔炉高炉的配方也有封装好的方法,但在此之前,我们得先写一个列表

1
public static final List<ItemConvertible> ICE_ETHER = List.of(ModItems.RAW_ICE_ETHER, ModBlocks.ICE_ETHER_ORE);

这个列表是List<ItemConvertible>类型,其存放的是都可以熔炼为ICE ETHER的物品(结合下方的)

之后,我们就可以利用这个List来创建熔炼配方了

1
2
offerSmelting(exporter, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER, 0.7f, 200, "ice_ether");
offerBlasting(exporter, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER, 0.7f, 100, "ice_ether");

offerSmelting方法,用于创建熔炉配方,接受6个参数,
分别是配方导出器,可以熔炼为ICE ETHER物品列表配方类型加工产物
产物掉落物的经验值熔炼时间,以及group配方组的名字

offerBlasting方法,用于创建高炉配方,接受6个参数,同上

有序合成

接下来我们看有序合成

1
2
3
4
5
ShapedRecipeJsonBuilder.create(RecipeCategory.FOOD, Items.SUGAR, 3)
.pattern("###")
.input('#', Items.BEETROOT)
.criterion(hasItem(Items.BEETROOT), conditionsFromItem(Items.BEETROOT))
.offerTo(exporter, new Identifier(TutorialMod.MOD_ID, "sugar_from_beetroot"));

有序合成我们使用ShapedRecipeJsonBuilder.create来创建

方法接受3个参数,分别是配方类型配方产物产物数量

下面的patterninput就与前面写过的类似了

值得注意的是,input方法接受的第一个参数是字符型(char)的变量,而不是字符串类型(string)的,
所以要使用单引号

criterion方法,用于设置解锁配方的条件,接受2个参数,第一个是字符串,第二个是解锁条件

这个拿它最终生成的进度文件来看可能会更好理解一点

1
2
3
4
5
6
7
8
9
10
11
12
"has_beetroot": {
"conditions": {
"items": [
{
"items": [
"minecraft:beetroot"
]
}
]
},
"trigger": "minecraft:inventory_changed"
}

这是其中一部分,在criterion方法中,hasItem(Items.BEETROOT)对应的是其中的has_beetroot,也就是这里的键;
而后面的conditionsFromItem(Items.BEETROOT)对应的是后面的一串

inventory_changed表示的是物品栏改变,这里合起来就变成了当甜菜进入玩家物品栏时,则解锁配方

最后的offerTo方法,用于将配方导出,接受2个参数,第一个是配方导出器,第二个是配方名字

无序合成

无序合成也是类似的

1
2
3
4
5
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, ModBlocks.ICE_ETHER_ORE, 1)
.input(ModItems.RAW_ICE_ETHER)
.input(Items.STONE)
.criterion(hasItem(ModItems.RAW_ICE_ETHER), conditionsFromItem(ModItems.RAW_ICE_ETHER))
.offerTo(exporter, new Identifier(TutorialMod.MOD_ID, "ice_ether_ore"));

它也有对应的ShapelessRecipeJsonBuilder.create方法

它和有序合成的方法不同的,就在于其input方法,这里的input就对应的是之前配方中的ingredients内容

食物类烹饪配方(以营火为例)

那么,如果说我们要写营火或者烟熏炉的配方该怎么办呢?

营火烟熏炉这样的用于食物的烹饪配方,并没有像熔炼高炉那样有封装好的方法,这个时候,我们就得去看看源代码了

在原版的RecipeProvider类中,我们可以看到这一堆

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
public static void generateCookingRecipes(
Consumer<RecipeJsonProvider> exporter, String cooker, RecipeSerializer<? extends AbstractCookingRecipe> serializer, int cookingTime
) {
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.BEEF, Items.COOKED_BEEF, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.CHICKEN, Items.COOKED_CHICKEN, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.COD, Items.COOKED_COD, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.KELP, Items.DRIED_KELP, 0.1F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.SALMON, Items.COOKED_SALMON, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.MUTTON, Items.COOKED_MUTTON, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.PORKCHOP, Items.COOKED_PORKCHOP, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.POTATO, Items.BAKED_POTATO, 0.35F);
offerFoodCookingRecipe(exporter, cooker, serializer, cookingTime, Items.RABBIT, Items.COOKED_RABBIT, 0.35F);
}

public static void offerFoodCookingRecipe(
Consumer<RecipeJsonProvider> exporter,
String cooker,
RecipeSerializer<? extends AbstractCookingRecipe> serializer,
int cookingTime,
ItemConvertible input,
ItemConvertible output,
float experience
) {
CookingRecipeJsonBuilder.create(Ingredient.ofItems(input), RecipeCategory.FOOD, output, experience, cookingTime, serializer)
.criterion(hasItem(input), conditionsFromItem(input))
.offerTo(exporter, getItemPath(output) + "_from_" + cooker);
}

这里有一个offerFoodCookingRecipe,原版是把所有食物烹饪的配方写在这里了

不过问题是,像其中的cookerserializercookingTime这样的参数,我们应该怎么写呢?

我们可以在另外一个地方找到generateCookingRecipes方法调用的地方

1
2
generateCookingRecipes(exporter, "smoking", RecipeSerializer.SMOKING, 100);
generateCookingRecipes(exporter, "campfire_cooking", RecipeSerializer.CAMPFIRE_COOKING, 600);

那么在这里,我们就可以找到所需要的参数,不过营火和烟熏炉的是分开的,它们的cookerserializer参数是对应的

那么,我们就可以根据以上内容写我们自己的食物烹饪配方

1
2
offerFoodCookingRecipe(exporter, "campfire_cooking", RecipeSerializer.CAMPFIRE_COOKING,
600, ModItems.RAW_ICE_ETHER, ModItems.ICE_ETHER, 0.35f);

数据生成调用

虽然上面的一堆类写好了,但是我们还是无法生成我们的数据,因为这些类现在也还没有被调用

接下来我们要到TutorialModDataGenerator类中调用这些类,这个类是一开始就随我们的模板文件一起生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();

pack.addProvider(ModBlockTagsProvider::new);
pack.addProvider(ModItemTagsProvider::new);
pack.addProvider(ModEnUsLangProvider::new);
pack.addProvider(ModLootTableProvider::new);
pack.addProvider(ModModelsProvider::new);
pack.addProvider(ModRecipesProvider::new);
pack.addProvider(ModZhCnLangProvider::new);

}

我们先写一句FabricDataGenerator.Pack pack = fabricDataGenerator.createPack()

然后,在addProvider方法中,我们就可以调用我们之前写好的类了

不过要注意的是,每个类的构造函数访问修饰符都要改成public,否则无法调用

随后,在运行我们的数据生成之前,我们要将原来已经写好的各种json文件删除,因为是不能有重复文件的,
会报错

当然我更建议的是找一个地方移动过去,不然数据生成出问题了,而原来的东西没了,还得重新写

运行数据生成

我们找到右上偏中间位置的配置栏,找到其中的ata Generation,点击运行

当运行窗口中输出Build Successful时,就表示数据生成成功了

我们也可以看到它生成所需的时间,一般生成也就几百毫秒,而我们前面写了多长时间呢?

生成好的数据在generated文件夹下,里面的格式也是和我们之前写的一样的

之后,我们就可以启动游戏进行测试了