本篇教程的视频

(待发布)

本篇教程的源代码

(待发布)

简介

本篇教程我们利用数据生成来生成各种数据文件,包括方块状态语言模型配方战利品列表等等

这里我简单介绍一下数据生成这个东西

本质呢就是利用Java来跑各种json

为什么要用数据生成?原因很简单,因为它真的很方便,而且不容易出错

比如你有成百上千个东西要添加,光是物品的模型就得写一堆,配方、战利品列表就不用说了,
而且有时候很容易写错东西,比如modid注册名等等

而你如果用数据生成,几百毫秒到几秒钟的时间,就能给你完成这些文件的生成

好,问题来了,咋不一开始就讲数据生成呢,而是讲完前面那一堆文件之后,在来搞这个东西?

好问题,原因很简单,你得知道这些文件里面有什么,然后你才能知道你要什么

数据生成虽然是用代码来跑json文件,但它的参数还是和原本的json文件息息相关的,
所以终归是要了解原本的那些参数有什么是什么怎么写

数据生成

这里我们长话短说,直接来写数据生成吧

同样的,NeoForge的数据生成与Forge的类似,比Fabric的要复杂一些,注意看仔细

ModLootTablesProvider

这里我们先创建一个ModLootTablesProvider类,这个类是用来生成战利品列表的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ModLootTablesProvider extends BlockLootSubProvider {

public ModLootTablesProvider(HolderLookup.Provider registries) {
super(Set.of(), FeatureFlags.REGISTRY.allFlags(), registries);
}

@Override
protected void generate() {

}

@Override
protected Iterable<Block> getKnownBlocks() {
return null;
}
}

这里我们继承了BlockLootSubProvider类,然后重写了generate方法和getKnownBlocks方法

创建一个super函数,然后对其进行改写

1
2
3
public ModLootTablesProvider(HolderLookup.Provider registries) {
super(Set.of(), FeatureFlags.REGISTRY.allFlags(), registries);
}

将构造函数原本的参数去掉,然后在里面直接指定

根据其父类的构造函数,第一个参数是爆炸中不会破化的物品集合,第二个参数是启用特性的集合

但具体是什么意思我就不知道了,如果有大佬知道可以直接在评论区交流

再往下,我们重写getKnownBlocks方法

1
2
3
4
5
@Override
@Override
protected Iterable<Block> getKnownBlocks() {
return ModBlocks.BLOCKS.getEntries().stream().map(Holder::value)::iterator;
}

ModBlocks.BLOCKS.getEntries()返回一个包含所有注册方块的集合,
BLOCKS就是前面写的延迟注册器,这里面存了我们注册的方块

然后我们用stream()方法将其转换为一个流,然后用map()方法将其映射到对应的方块

最后我们用iterator()方法将其转换为一个迭代器,返回一个可迭代的方块集合

这个东西大概是一个用来检测你是否为每一个方块都配置了战利品列表的方法,
这个方法会在数据生成时调用,如果有方块的战利品列表没有写,则直接抛出异常

不过,如果你有些方块确实是没有战利品列表的,那么可以在注册时给它加上noLootTable方法,
这样即便你没有写战利品列表,也不会抛出异常了

最后来看generate方法,这个方法才是真正用来数据生成的

1
2
dropSelf(ModBlocks.ICE_ETHER_BLOCK.get());
dropSelf(ModBlocks.RAW_ICE_ETHER_BLOCK.get());

一般的方块就直接用dropSelf方法,也就是掉落方块本身

1
2
add(ModBlocks.ICE_ETHER_ORE.get(),
block -> createOreDrop(ModBlocks.ICE_ETHER_ORE.get(), ModItems.RAW_ICE_ETHER.get()));

对于矿石,我们可以用add方法,在第二个参数这里调用createOreDrop方法,
第一个参数是矿石,第二个参数是掉落物

但是这个方法只能掉落一个矿石,要像铜矿青金石那样掉落多个,就要用到别的方法

我们不妨先看看原版的铜矿石怎么写的

1
2
3
4
5
6
7
8
9
10
11
12
protected LootTable.Builder createCopperOreDrops(Block block) {
HolderLookup.RegistryLookup<Enchantment> registrylookup = this.registries.lookupOrThrow(Registries.ENCHANTMENT);
return this.createSilkTouchDispatchTable(
block,
(LootPoolEntryContainer.Builder<?>)this.applyExplosionDecay(
block,
LootItem.lootTableItem(Items.RAW_COPPER)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(2.0F, 5.0F)))
.apply(ApplyBonusCount.addOreBonusCount(registrylookup.getOrThrow(Enchantments.FORTUNE)))
)
);
}

显然易见,很可惜,它是硬编码的,也就是你不论传入什么方块,最终掉落的是铜矿石

但我们不妨把这个方法复制一下,再改写一下

1
2
3
4
5
6
7
8
9
10
11
12
protected LootTable.Builder createCopperOreLikeDrops(Block block, Item item) {
HolderLookup.RegistryLookup<Enchantment> registrylookup = this.registries.lookupOrThrow(Registries.ENCHANTMENT);
return this.createSilkTouchDispatchTable(
block,
(LootPoolEntryContainer.Builder<?>)this.applyExplosionDecay(
block,
LootItem.lootTableItem(item)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(2.0F, 5.0F)))
.apply(ApplyBonusCount.addOreBonusCount(registrylookup.getOrThrow(Enchantments.FORTUNE)))
)
);
}

这里我们创建了一个createCopperOreLikeDrops方法,第一个参数是方块,第二个参数是掉落物,
将原本的粗铜换成一个参数传入

你也可以进一步拓展,比如传入最大最小值,将UniformGenerator.between中的数值也进行替换

然后我们就可以调用这个方法了

1
2
add(ModBlocks.ICE_ETHER_ORE.get(),
block -> createCopperOreLikeDrops(ModBlocks.ICE_ETHER_ORE.get(), ModItems.RAW_ICE_ETHER.get()));

ModBlockStatesProvider

接下来我们创建一个ModBlockStatesProvider类,这个类是用来生成方块状态的

1
2
3
4
5
6
7
8
9
10
11
public class ModBlockStatesProvider extends BlockStateProvider {

public ModBlockStatesProvider(PackOutput output, ExistingFileHelper exFileHelper) {
super(output, TutorialMod.MOD_ID, exFileHelper);
}

@Override
protected void registerStatesAndModels() {

}
}

这里我们继承了BlockStateProvider类,然后重写了registerStatesAndModels方法

创建super函数的时候,我们同样要改一些参数

1
2
3
public ModBlockStatesProvider(PackOutput output, ExistingFileHelper exFileHelper) {
super(output, TutorialMod.MOD_ID, exFileHelper);
}

第一个参数是PackOutput,输出目标;第二个参数是modid
第三个参数是ExistingFileHelper,用于处理已有文件

然后我们就可以在registerStatesAndModels方法里面写数据生成的代码了

1
2
3
4
5
6
@Override
protected void registerStatesAndModels() {
simpleBlockWithItem(ModBlocks.ICE_ETHER_BLOCK.get(), cubeAll(ModBlocks.ICE_ETHER_BLOCK.get()));
simpleBlockWithItem(ModBlocks.RAW_ICE_ETHER_BLOCK.get(), cubeAll(ModBlocks.RAW_ICE_ETHER_BLOCK.get()));
simpleBlockWithItem(ModBlocks.ICE_ETHER_ORE.get(), cubeAll(ModBlocks.ICE_ETHER_ORE.get()));
}

我们之前创建的方块状态文件都是最简单的cube_all

所以这里我们用cubeAll方法来创建它们

simpleBlockWithItem方法是创建一个方块并创建其对应的方块物品,
这个方法的话就连带着将方块状态方块模型物品模型一起生成了

ModBlockTagsProvider

接下来,我们创建一个ModBlockTagsProvider类,这个类是用来生成方块标签的

1
2
3
4
5
6
7
8
9
10
11
public class ModBlockTagsProvider extends BlockTagsProvider {

public ModBlockTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider, @Nullable ExistingFileHelper existingFileHelper) {
super(output, lookupProvider, TutorialMod.MOD_ID, existingFileHelper);
}

@Override
protected void addTags(HolderLookup.Provider provider) {

}
}

这里我们继承了BlockTagsProvider类,然后重写了addTags方法

创建super函数的时候,我们同样要改一些参数

1
2
3
public ModBlockTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider, @Nullable ExistingFileHelper existingFileHelper) {
super(output, lookupProvider, TutorialMod.MOD_ID, existingFileHelper);
}

第三个参数直接传入我们的modid

然后我们就可以在addTags方法里面写数据生成的代码了

当然我们实际上并没有讲到Tag,也就是标签,但我们之前写战利品列表的时候,其实已经涉及了,
这里我们生成一下原版的那些和战利品列表有关系的标签

1
2
3
4
5
6
@Override
protected void addTags(HolderLookup.Provider provider) {
tag(BlockTags.MINEABLE_WITH_PICKAXE)
.add(ModBlocks.ICE_ETHER_BLOCK.get())
.add(ModBlocks.ICE_ETHER_ORE.get());
}

这里我们用tag方法来创建标签,传入具体的标签,然后调用add方法来添加方块

ModItemTagsProvider

物品标签也类似,但我们还没讲到标签这个教程,所以就先创建好了

1
2
3
4
5
6
7
8
9
10
11
12
public class ModItemTagsProvider extends ItemTagsProvider {

public ModItemTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider,
CompletableFuture<TagLookup<Block>> blockTags, @Nullable ExistingFileHelper existingFileHelper) {
super(output, lookupProvider, blockTags, TutorialMod.MOD_ID, existingFileHelper);
}

@Override
protected void addTags(HolderLookup.Provider provider) {

}
}

它要继承的是ItemTagsProvider类,然后重写addTags方法

super函数中同样要传入modid

ModEnUsLangProvider

接下来是语言文件的数据生成类,我们创建一个ModEnUsLangProvider类,这个类是用来生成英文语言文件的

1
2
3
4
5
6
7
8
9
10
11
public class ModEnUsLangProvider extends LanguageProvider {

public ModEnUsLangProvider(PackOutput output) {
super(output, TutorialMod.MOD_ID, "en_us");
}

@Override
protected void addTranslations() {

}
}

这里我们继承了LanguageProvider类,然后重写了addTranslations方法

创建super函数的时候,我们可以只留一个参数,super中第二个参数是modid
第三个参数是语言代码,这里我们传入en_us表示英文

然后我们就可以在addTranslations方法里面写数据生成的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void addTranslations() {
add(ModItems.ICE_ETHER.get(), "Ice Ether");
add(ModItems.RAW_ICE_ETHER.get(), "Raw Ice Ether");
add(ModItems.CARDBOARD.get(), "CardBoard");

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

add("itemGroup.tutorial_tab", "Tutorial Tab");
add("itemGroup.materials", "Material");
}

除了物品栏之外,其他都可以直接用我们之前注册的那些字段

它们的第二个参数是对应的翻译

ModZhCnLangProvider

那么中文的也是类似

1
2
3
4
5
6
7
8
9
10
public class ModZhCnLangProvider extends LanguageProvider {
public ModZhCnLangProvider(PackOutput output) {
super(output, TutorialMod.MOD_ID, "zh_cn");
}

@Override
protected void addTranslations() {

}
}

只不过super函数中第三个参数是zh_cn表示简体中文

ModItemModelsProvider

接下来是物品模型的数据生成类,我们创建一个ModItemModelsProvider类,这个类是用来生成物品模型的

1
2
3
4
5
6
7
8
9
10
11
public class ModItemModelsProvider extends ItemModelProvider {

public ModItemModelsProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
super(output, TutorialMod.MOD_ID, existingFileHelper);
}

@Override
protected void registerModels() {

}
}

这里我们继承了ItemModelProvider类,然后重写了registerModels方法

super函数中第二个参数是modid

然后我们就可以在registerModels方法里面写数据生成的代码了

1
2
3
4
5
6
@Override
protected void registerModels() {
basicItem(ModItems.ICE_ETHER.get());
basicItem(ModItems.RAW_ICE_ETHER.get());
// basicItem(ModItems.CARDBOARD.get());
}

这里我们用basicItem方法来生成物品模型,传入具体的物品

但是这有一个问题,我们写的CARDBOARD注册的时候还写了material/的,
而在数据生成时,它并不会在item下创建material文件夹,
反而在与item平级的目录下创建material文件夹

我不知道ForgeNeoForge是怎么搞的,隔壁的Fabric就不会这样,如果有大佬知道也可以提一下

所以我现在开发模组,也都从Fabric开始,再移植过来,数据文件都直接复制了

ModRecipesProvider

来到最后一个类,ModRecipesProvider,这个类是用来生成配方文件的

1
2
3
4
5
6
7
8
9
10
11
public class ModRecipesProvider extends RecipeProvider implements IConditionBuilder {

public ModRecipesProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries);
}

@Override
protected void buildRecipes(RecipeOutput recipeOutput) {

}
}

这里我们继承了RecipeProvider类,实现IConditionBuilder接口,然后重写了buildRecipes方法

然后我们就直接写配方的内容

熔炉/高炉

熔炉和高炉的配方首先要搬一些方法过来,因为那些方法并没有我们的命名空间,所以要改

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
protected static void oreSmelting(
RecipeOutput recipeOutput, List<ItemLike> ingredients, RecipeCategory category, ItemLike result, float experience, int cookingTime, String group
) {
oreCooking(
recipeOutput,
RecipeSerializer.SMELTING_RECIPE,
SmeltingRecipe::new,
ingredients,
category,
result,
experience,
cookingTime,
group,
"_from_smelting"
);
}

protected static void oreBlasting(
RecipeOutput recipeOutput, List<ItemLike> ingredients, RecipeCategory category, ItemLike result, float experience, int cookingTime, String group
) {
oreCooking(
recipeOutput,
RecipeSerializer.BLASTING_RECIPE,
BlastingRecipe::new,
ingredients,
category,
result,
experience,
cookingTime,
group,
"_from_blasting"
);
}

protected static <T extends AbstractCookingRecipe> void oreCooking(
RecipeOutput recipeOutput,
RecipeSerializer<T> serializer,
AbstractCookingRecipe.Factory<T> recipeFactory,
List<ItemLike> ingredients,
RecipeCategory category,
ItemLike result,
float experience,
int cookingTime,
String group,
String suffix
) {
for (ItemLike itemlike : ingredients) {
SimpleCookingRecipeBuilder.generic(Ingredient.of(itemlike), category, result, experience, cookingTime, serializer, recipeFactory)
.group(group)
.unlockedBy(getHasName(itemlike), has(itemlike))
.save(recipeOutput, TutorialMod.MOD_ID + ":" + getItemName(result) + suffix + "_" + getItemName(itemlike));
}
}

这些是原版的方法,而我们要在oreCooking方法中加入我们的命名空间,也就是modid

除此之外,我们还要写List

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

这个是用来定义哪些东西可以熔炼为某一种东西

然后我们就可以写配方了

1
2
oreBlasting(recipeOutput, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER, 0.25f, 100, "ice_ether");
oreSmelting(recipeOutput, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER, 0.25f, 200, "ice_ether");

这里我们用oreBlasting方法来生成高炉配方,用oreSmelting方法来生成熔炉配方

第一个参数是配方文件生成器,第二个参数是熔炼的原料(列表),第三个参数是配方类型,第四个参数是熔炼的产物
第五个参数是熔炼的经验,第六个参数是熔炼的时间,第七个参数是配方组

上面创建一个List的好处就在于你能少些一些东西,
不然就是每种原料都得写一遍

合成

合成主要是有序合成和无序合成,可惜的是,和Forge一样,NeoForge也没有提供一个方块合成九个物品和它反过来的方法,
只能我们自己写

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
ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, ModBlocks.ICE_ETHER_BLOCK)
.pattern("###")
.pattern("###")
.pattern("###")
.define('#', ModItems.ICE_ETHER)
.unlockedBy(getHasName(ModItems.ICE_ETHER), has(ModItems.ICE_ETHER))
.save(recipeOutput);

ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ModItems.ICE_ETHER, 9)
.requires(ModBlocks.ICE_ETHER_BLOCK)
.unlockedBy(getHasName(ModBlocks.ICE_ETHER_BLOCK), has(ModBlocks.ICE_ETHER_BLOCK))
.save(recipeOutput);

ShapedRecipeBuilder.shaped(RecipeCategory.FOOD, Items.SUGAR, 3)
.pattern("###")
.define('#', Items.BEETROOT)
.unlockedBy(getHasName(Items.BEETROOT), has(Items.BEETROOT))
.save(recipeOutput, TutorialMod.MOD_ID + ":" + "sugar_from_beetroot");

ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ModBlocks.ICE_ETHER_ORE)
.requires(ModItems.RAW_ICE_ETHER)
.requires(Blocks.STONE)
.unlockedBy(getHasName(ModItems.RAW_ICE_ETHER), has(ModItems.RAW_ICE_ETHER))
.unlockedBy(getHasName(Blocks.STONE), has(Blocks.STONE))
.save(recipeOutput);

这里我们用ShapedRecipeBuilder来生成有序合成,用ShapelessRecipeBuilder来生成无序合成

pattern方法用来定义有序合成的合成表,define方法用来定义合成表中的符号,
requires方法用来定义无序合成合成表中的原料

unlockedBy方法用来定义解锁条件,save方法用来保存配方

数据生成主类

最后我们还要写一个数据生成主类,用于调用所有的数据生成器

我们创建一个ModDataGenerator

1
2
3
4
@EventBusSubscriber(modid = TutorialMod.MOD_ID)
public class ModDataGenerator {

}

这个类要加上@EventBusSubscriber注解,并且要指定modid

然后我们在里面创建一个方法

1
2
3
4
@SubscribeEvent
public static void gatherData(GatherDataEvent event) {

}

这个方法要加上@SubscribeEvent注解

然后我们就可以在里面调用所有的数据生成器了

1
2
3
4
DataGenerator generator = event.getGenerator();
PackOutput packOutput = generator.getPackOutput();
ExistingFileHelper existingFileHelper = event.getExistingFileHelper();
CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();

首先创建四个变量,DataGeneratorPackOutputExistingFileHelperCompletableFuture<HolderLookup.Provider>

这些东西其实也就是各个不同的数据生成类的构造函数中有的那些参数

然后我们就可以调用数据生成器了

1
2
3
4
5
6
7
8
9
10
11
12
generator.addProvider(event.includeServer(), new LootTableProvider(packOutput, Collections.emptySet(),
List.of(new LootTableProvider.SubProviderEntry(ModLootTablesProvider::new, LootContextParamSets.BLOCK)), lookupProvider));
generator.addProvider(event.includeServer(), new ModRecipesProvider(packOutput, lookupProvider));

BlockTagsProvider blockTagsProvider = new ModBlockTagsProvider(packOutput, lookupProvider, existingFileHelper);
generator.addProvider(event.includeServer(), blockTagsProvider);
generator.addProvider(event.includeServer(), new ModItemTagsProvider(packOutput, lookupProvider, blockTagsProvider.contentsGetter(), existingFileHelper));

generator.addProvider(event.includeClient(), new ModItemModelsProvider(packOutput, existingFileHelper));
generator.addProvider(event.includeClient(), new ModBlockStatesProvider(packOutput, existingFileHelper));
generator.addProvider(event.includeClient(), new ModEnUsLangProvider(packOutput));
generator.addProvider(event.includeClient(), new ModZhCnLangProvider(packOutput));

这里我们用generator.addProvider方法来添加数据生成器,
event.includeServer()event.includeClient()用来判断是否是服务器端或者客户端

一些数据文件是要在服务端上的,一些则是客户端

比如配方标签战利品列表用在服务端上,剩下的用在客户端上

你自己要判断的话其实也很简单,拿一个服务器来说,哪些东西用户能够更改,那些数据就是在客户端的,
比如语言,不同地区用的不一样;模型呢,有些材质包会改物品模型,而材质包本身也是在客户端跑的

调用生成

最后,就是直接运行配置里的Data,然后就能生成数据文件了

另外,原本你已经写好的数据文件得删掉,不然会因为重复文件而崩溃

不过,我更推荐是放到另一个地方,万一数据生成出问题了你还能补救一下

之后就是进入游戏测试