本篇教程由作者设定使用 CC BY-NC-SA 协议。

零、写在前面

协议与声明

注意,本文内容使用CC: BY-NC-SA协议,本文代码大部分截取自绿宝石工艺,使用MIT协议;其他作特殊说明的代码以Forge反编译的Minecraft源代码为主,所有函数、类和成员变量的命名使用了官方的反混淆表,函数参数和局域变量根据笔者的个人理解命名,仅作学习交流使用。

由于此mod的拓展较多,但笔者并没有发现其他国人的作品,且官方并没有对API做出很好的文档或wiki说明。因此笔者撰写本篇中文教程以供开发者交流学习和节省阅读代码的时间,希望国内因为洞穴与山崖更新而停滞的优秀群系mod能够重新焕发活力。

此教程以默认读者具备Java代码能力和mod开发的基础了解。

配置build.gradle

首先你要在这里找到合适的TerraBlender版本,如1.19.2的TerraBlender v2.0.1.128

然后,你要在build.gradle中加入对应的maven下载url,除了minecraftforge外还需要加入spongepowered mixin:

buildscript {
    repositories {
        maven { url = 'https://maven.minecraftforge.net' }
        jcenter()
        mavenCentral()
        maven { name="sponge"; url 'https://repo.spongepowered.org/repository/maven-public/' }
    }
    dependencies {
        classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+', changing: true
        classpath 'org.spongepowered:mixingradle:0.7.32'
    }
}

apply plugin: 'org.spongepowered.mixin'

然后,在dependencies中加入spongepowered mixin和terrablender:

dependencies {
    minecraft 'net.minecraftforge:forge:1.19.2-43.2.0'
    
    //compileOnly fg.deobf("mezz.jei:jei-1.19.2-forge:11.5.0.297")
    
    annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
    
    implementation fg.deobf("com.github.glitchfiend:TerraBlender-forge:1.19.2-2.0.1.128")   //TerraBlender
}

执行构建,gradle会帮你下载好terrablender供你调用其API。

配置mods.toml

mods.toml中同样需要加入前置模组信息,方便没有装前置的玩家运行Forge加载模组报错时更加轻松地看出问题。当然,即使如此也总会有截个图就跑来不上传错误报告乱提交issue的人存在,甚至还会骂你两句。模组开发者就该有大心脏,遇到什么人都要处变不惊。

前置有两种,一种是必要前置,如TerraBlender是BYG和BOP的必要前置。另一种是可选前置,如TerraBlender是Quark的可选前置。区别是,必要前置没有安装时,拓展mod无法工作;而可选前置没有安装并不会影响拓展mod的工作,唯一可能影响其工作的情况则是可选前置mod的版本不正确。

前置的声明要写在[[dependencies.modid]]中,如绿宝石工艺的mods.toml中加入了TerraBlender这个必要前置:

[[dependencies.emeraldcraft]]
    modId="terrablender"
    mandatory=true
    versionRange="[2.0.1.128,)"
    ordering="NONE"
    side="BOTH"

可选前置只需要把mandatory改成false即可,不过实际项目代码中也要加以判断,如果前置mod没有安装,一定不要加载引用前置mod相关API的类!

一、1.18+与pre1.17.1群系注册和生成的区别

1. pre1.17的群系注册和生成

熟悉低版本mod开发的朋友们都知道,在1.17.1及之前,Forge就已经提供了群系注册和生成的各种接口,最具代表性的包括群系词典:

BiomeDictionary$addTypes(RegistryKey<Biome> biomeKey, BiomeDictionary.Type... types)

例如我们可以实现这样的群系:

private static Biome NetherGarden() {
    //群系内的生物生成情况
   MobSpawnInfo mobspawninfo = new MobSpawnInfo.Builder()
         .addSpawn(EntityClassification.MONSTER, new MobSpawnInfo.Spawners(EntityType.ZOMBIFIED_PIGLIN, 2, 2, 4))
         .addSpawn(EntityClassification.MONSTER, new MobSpawnInfo.Spawners(EntityType.HOGLIN, 1, 3, 4))
         .addSpawn(EntityClassification.MONSTER, new MobSpawnInfo.Spawners(EntityType.PIGLIN, 5, 3, 4))
         .addSpawn(EntityClassification.MONSTER, new MobSpawnInfo.Spawners(EntityType.ENDERMAN, 2, 1, 2))
         .addSpawn(EntityClassification.CREATURE, new MobSpawnInfo.Spawners(EntityType.STRIDER, 10, 1, 2)).build();
    //群系使用的Surface Builder及结构生成
   BiomeGenerationSettings.Builder biomegenerationsettings$builder = new BiomeGenerationSettings.Builder()
         .surfaceBuilder(ECConfiguredSurfaceBuilders.NETHER_GARDEN)
         .addStructureStart(StructureFeatures.RUINED_PORTAL_NETHER)
         .addCarver(GenerationStage.Carving.AIR, ConfiguredCarvers.NETHER_CAVE)
         .addStructureStart(StructureFeatures.NETHER_BRIDGE)
         .addStructureStart(StructureFeatures.BASTION_REMNANT)
         //.addStructureStart(StructureFeatures.STRONGHOLD)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.SPRING_LAVA);
    //群系地物
   DefaultBiomeFeatures.addDefaultMushrooms(biomegenerationsettings$builder);
   biomegenerationsettings$builder.addFeature(GenerationStage.Decoration.UNDERGROUND_DECORATION, Features.SPRING_OPEN)
         .addFeature(GenerationStage.Decoration.UNDERGROUND_DECORATION, Features.PATCH_FIRE)
         .addFeature(GenerationStage.Decoration.UNDERGROUND_DECORATION, Features.GLOWSTONE_EXTRA)
         .addFeature(GenerationStage.Decoration.UNDERGROUND_DECORATION, Features.GLOWSTONE)
         .addFeature(GenerationStage.Decoration.UNDERGROUND_DECORATION, Features.ORE_MAGMA)
         .addFeature(GenerationStage.Decoration.UNDERGROUND_DECORATION, Features.SPRING_CLOSED)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.WEEPING_VINES)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.TWISTING_VINES)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.CRIMSON_FUNGI)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.CRIMSON_FOREST_VEGETATION)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.WARPED_FUNGI)
         .addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, Features.WARPED_FOREST_VEGETATION);
   DefaultBiomeFeatures.addNetherDefaultOres(biomegenerationsettings$builder);
   //群系的其他说明
   return new Biome.Builder().precipitation(Biome.RainType.NONE).biomeCategory(Biome.Category.NETHER)
         .depth(0.1F).scale(0.2F).temperature(2.0F).downfall(0.0F)
         .specialEffects(new BiomeAmbience.Builder()
               .waterColor(4159204).waterFogColor(329011).fogColor(12169636).skyColor(calculateSkyColor(2.0F))
               .ambientParticle(new ParticleEffectAmbience(ParticleTypes.CRIMSON_SPORE, 0.025F))
               .ambientLoopSound(SoundEvents.AMBIENT_CRIMSON_FOREST_LOOP)
               .ambientMoodSound(new MoodSoundAmbience(SoundEvents.AMBIENT_CRIMSON_FOREST_MOOD, 6000, 8, 2.0D))
               .ambientAdditionsSound(new SoundAdditionsAmbience(SoundEvents.AMBIENT_CRIMSON_FOREST_ADDITIONS, 0.0111D))
               .backgroundMusic(BackgroundMusicTracks.createGameMusic(SoundEvents.MUSIC_BIOME_CRIMSON_FOREST)).build())
         .mobSpawnSettings(mobspawninfo).generationSettings(biomegenerationsettings$builder.build()).build();
}

简要说明一下Biome.Builder中的各种参数:

  • precipitation即降水,代表雨天群系是下雪(积雪针叶林、雪原等)还是下雨(平原、深海等)还是干旱(沙漠、热带草原),由于例子里是下界群系,所以选择了NONE。

  • biomeCategory代表群系类型,所有下界群系共用NETHER类型,末地群系共用END类型,只有主世界群系有着诸多差异,如沙滩和石岸是BEACH、白桦森林和繁华森林等是FOREST、各种海洋是OCEAN等。

  • depth表示群系相对海平面的高度,高山群系的depth值较大,平原则较接近0,而海洋则是负值。当然,在1.18+中这个属性被移除了。

  • scale则表示群系的起伏程度,注意和高度区分,举个例子,恶地高原虽然depth值较大,但scale值接近0,而风袭恶地的depth值并不大,但scale值较大,因而恶地高原高而平坦,风袭恶地则如丘陵一般。而和depth一样,这个值后来也被移除了。

  • waterColor、waterFogColor、fogColor和skyColor则分别代表群系内水的颜色、水中迷雾的颜色、远处迷雾的颜色和天空的颜色,这是一个RGB值,转换成十六进制是0xrrggbb的形式——暖海的水偏绿、冷水海洋的水偏蓝则是前两个值的作用,而灵魂沙峡谷背景是蓝色、绯红森林背景是红色则是fogColor的作用。

  • ambientParticle、ambientLoopSound、ambientMoodSound、ambientAdditionsSound分别代表群系环境粒子、群系环境音效、群系氛围音效、群系随机音效。而backgroundMusic则是群系中会随机播放的背景音乐。

  • mobSpawnSettings则是前文注册的生物生成,generationSettings是前文注册的世界生成,包括群系表面、结构和地物等。

调用这一工厂函数即可生成一个下界花园群系,通过订阅RegistryEvent.Register<Biome>事件或使用DeferredRegister<Biome>即可实现群系注册。

注册后,使用如下接口可以将群系注册到主世界中:

BiomeManager$addBiome(BiomeType type, BiomeEntry entry)

而注册到下界中则需要mixin注入来修改NetherBiomeProvider$parameters成员变量。

另外最好将你的群系也注册一下前文提到的Forge的群系词典,以使其他模组添加的内容能够在你的群系中正常工作。

额外说明一下Surface Builder,这个东西字如其名,是实现群系表面构造的,比如平原表层是草方块、下面是泥土,而沙漠表层是沙子、下面是砂岩,而恶地甚至有条带状的陶瓦山,这些都是Surface Builder的功劳。上述群系的Surface Builder源码如下:

public class NetherGardenSurfaceBuilder extends NetherForestsSurfaceBuilder {
  public NetherGardenSurfaceBuilder(Codec<SurfaceBuilderConfig> codec) {
     super(codec);
  }

  @Override
  public void apply(@Nonnull Random random, @Nonnull IChunk chunk, @Nonnull Biome biome, int x, int z, int startHeight, double noise,
                @Nonnull BlockState defaultBlock, @Nonnull BlockState defaultFluid, int surfaceLevel, long seed, @Nonnull SurfaceBuilderConfig config) {
     double noise2 = Biome.BIOME_INFO_NOISE.getValue((double)x * 0.03125D, (double)z * 0.03125D, false);
     if(noise2 > 0.0D) {
        super.apply(random, chunk, biome, x, z, startHeight, noise, defaultBlock, defaultFluid, surfaceLevel, seed, SurfaceBuilder.CONFIG_CRIMSON_FOREST);
     } else {
        super.apply(random, chunk, biome, x, z, startHeight, noise, defaultBlock, defaultFluid, surfaceLevel, seed, SurfaceBuilder.CONFIG_WARPED_FOREST);
     }
  }
}

很简单,用一个噪声来决定当前位置的地表,一半是绯红菌岩,另一半是诡异菌岩。

pre1.17.1的群系生成底层逻辑很简单,通过柏林噪声粗生成群系,然后经过一系列放大、rolling和相邻作用等操作使得温度、湿度、海陆等特征平滑可控,最后根据Surface Builder来生成高度图,再添加地物和结构——记住这个顺序,粗生成-放大-填充群系-相互作用-(循环若干次)-生成高度图。感兴趣的朋友可以在b站上搜索更详细的介绍。

2. 1.18+的群系生成机制

1.18+的生成机制和pre1.17.1正好相反——世界生成有温度、湿度、陆地性、侵蚀性、奇异度五个柏林噪声,分别对应了单人世界中F3显示debug信息里其中一行的T、V、C、E、W五个值;接着根据陆地性和侵蚀性生成世界的基础高度图,再将群系贴到对应的区域中——和前文方法不同,这是个噪声生成-生成高度图-粗生成-放大-填充群系的过程。

陆地性其实就是之前的depth的演变,低于-1.05是蘑菇岛,在-1.05到-0.455是深海,-0.455到-0.19是海洋,-0.19到-0.11是海滩(沙滩、石岸等),-0.11到0.3是各种内陆群系,而高于0.3是山地。侵蚀性则是scale的演变,这个值越大显然地形越崎岖。此时区分不同群系边界、高原或平原则毫无意义,于是原版删掉了数十个不再使用的群系。

OverworldBiomeBuilder中给出了群系是如何被“贴”进世界的,举OverworldBiomeBuilder$MIDDLE_BIOMES的例子:

private final ResourceKey<Biome>[][] MIDDLE_BIOMES = new ResourceKey[][]{
    { Biomes.SNOWY_PLAINS,  Biomes.SNOWY_PLAINS, Biomes.SNOWY_PLAINS, Biomes.SNOWY_TAIGA,  Biomes.TAIGA                   },
    { Biomes.PLAINS,        Biomes.PLAINS,       Biomes.FOREST,       Biomes.TAIGA,        Biomes.OLD_GROWTH_SPRUCE_TAIGA },
    { Biomes.FLOWER_FOREST, Biomes.PLAINS,       Biomes.FOREST,       Biomes.BIRCH_FOREST, Biomes.DARK_FOREST             },
    { Biomes.SAVANNA,       Biomes.SAVANNA,      Biomes.FOREST,       Biomes.JUNGLE,       Biomes.JUNGLE                  },
    { Biomes.DESERT,        Biomes.DESERT,       Biomes.DESERT,       Biomes.DESERT,       Biomes.DESERT                  }
};

在pickMiddleBiome函数中,给出了这个二维数组的含义:

private ResourceKey<Biome> pickMiddleBiome(int temperature, int humidity, Climate.Parameter weirdness) {
   if (weirdness.max() < 0L) {
      return this.MIDDLE_BIOMES[temperature][humidity];
   }
   ResourceKey<Biome> resourcekey = this.MIDDLE_BIOMES_VARIANT[temperature][humidity];
   return resourcekey == null ? this.MIDDLE_BIOMES[temperature][humidity] : resourcekey;
}

很明显,数组的每一行代表一个温度值,从上到下是由寒冷到炎热;而每一列代表一个湿度值,从左到右是由干燥到潮湿。而当奇异值大于0时,群系将倾向于变成其变种,如竹林、稀疏丛林、向日葵平原等。当然除了这三个值会影响群系生成外,陆地性会影响海陆群系池的选择,而河流等则与侵蚀性和奇异值有关系。

而OverworldBiomeBuilder并没有给出其他mod生成的API,于是TerraBlender应运而生,通过各种代码注入,为开发者们提供了实现相关操作的接口。

那么第一个问题解决了,但第二个问题又出现了,群系的地表千差万别,如何实现这一区别?

我们知道低版本可以通过Surface Builder实现群系表面构造,而高版本则变成了Surface Rule,其中主世界的Surface Rule在SurfaceRuleData$overworldLike函数中,是一段长数百行的屎山代码,充满了各种if-else,本着奇文共赏的态度我整理并分享了一下:原版主世界Surface Rule整理版

突然理解了范大将军怒斥国足的激昂情绪,通过多文件继承实现的Surface Builder做得蛮好的为啥把它换下去?

于是如果我们想生成自己的群系,除了要解决群系贴进世界生成过程外,还要解决Surface Rule的注入问题——于是TerraBlender又提供了新的API,来帮助你将你实现和修改的Surface Rule添加进世界生成阶段。

接下来就来介绍这两个API的使用方法。

二、Region(BiomeProvider)

1.18.2+这一接口是terrablender.api.Region,而1.18.1则是terrablender.api.BiomeProvider。二者略有不同,BiomeProvider需要同时注册主世界和下界群系,而Region则可以在构造函数中选择RegionType.OVERWORLD和RegionType.NETHER。下文全部使用Region进行解说,BiomeProvider与Region相差不大,因此1.18.1模组作者们可以自行分析。

1. 构造函数

Region的构造函数是

Region(ResourceLocation name, RegionType type, int weight)

name是方便TerraBlender管理的注册名,建议用modid:name形式实现;type则是代表主世界还是下界;weight是Region的权重,TerraBlender的config可以调节原版主世界和下界的权重,默认为40。

2. 重载函数

你几乎一定要重载的成员函数原型如下:

public void addBiomes(Registry<Biome> registry, Consumer<Pair<Climate.ParameterPoint, ResourceKey<Biome>>> mapper)

TerraBlender会调用这个函数,而你要做的就是将群系生成的各个参数(温度、湿度、陆地性、侵蚀性、奇异度和深度)对应群系通过mapper参数注册。

而1.18.1的BiomeProvider写法特殊,需要你通过BiomeProvider$getUniquenessParameter()获得该区域的独立数字id,并将它也一并作为参数注册。这一步显然是封装不够完善,而后续代码修正了这一点。

你可以自行实现你的OverworldBiomeBuilder,或是和BYG一样通过继承OverworldBiomeBuilder实现自己的主世界群系生成器——毕竟写法与原版相似,这种做法鲁棒性高一些,而且需要mixin,不过与其他群系mod一起生成世界的效果可能会变差,请自行权衡。

3. Region的继承示例

绿宝石工艺的源代码对于主世界和下界采取了两种不同的做法实现Region的继承,主世界的方法沿用了原版的OverworldBiomeBuilder的写法,而下界则仅仅在原版五个群系的基础上增加了三个。

主世界:

public class ECOverworldBiomeRegion extends Region {
   public static final ResourceLocation LOCATION = new ResourceLocation(MODID, "overworld_biome_provider");

   public ECOverworldBiomeRegion(int weight) {
      super(LOCATION, RegionType.OVERWORLD, weight);
   }

   @Override
   public void addBiomes(Registry<Biome> registry, Consumer<Pair<Climate.ParameterPoint, ResourceKey<Biome>>> mapper) {
      (new ECOverworldBiomeBuilder()).addBiomes(registry, mapper);
   }
}

下界:

public class ECNetherBiomeRegion extends Region {
   public static final ResourceLocation LOCATION = new ResourceLocation(MODID, "nether_biome_provider");

   public ECNetherBiomeRegion(int weight) {
      super(LOCATION, RegionType.NETHER, weight);
   }

   @Override
   public void addBiomes(Registry<Biome> registry, Consumer<Pair<Climate.ParameterPoint, ResourceKey<Biome>>> mapper) {
      this.addBiome(mapper, Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.0F, Biomes.NETHER_WASTES);
      this.addBiome(mapper, Climate.Parameter.point(0.0F), Climate.Parameter.point(-0.5F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.0F, Biomes.SOUL_SAND_VALLEY);
      this.addBiome(mapper, Climate.Parameter.point(0.4F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.0F, Biomes.CRIMSON_FOREST);
      this.addBiome(mapper, Climate.Parameter.point(0.0F), Climate.Parameter.point(0.5F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.375F, Biomes.WARPED_FOREST);
      this.addBiome(mapper, Climate.Parameter.point(-0.5F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.175F, Biomes.BASALT_DELTAS);
      if (BiomeUtil.isKeyRegistered(registry, ECBiomeKeys.EMERY_DESERT)) {
         this.addBiome(mapper, Climate.Parameter.point(-0.8F), Climate.Parameter.point(-0.8F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.0F, ECBiomeKeys.EMERY_DESERT.key());
      }
      if (BiomeUtil.isKeyRegistered(registry, ECBiomeKeys.PURPURACEUS_SWAMP)) {
         this.addBiome(mapper, Climate.Parameter.point(0.7F), Climate.Parameter.point(0.7F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.125F, ECBiomeKeys.PURPURACEUS_SWAMP.key());
      }
      if (BiomeUtil.isKeyRegistered(registry, ECBiomeKeys.QUARTZ_DESERT)) {
         this.addBiome(mapper, Climate.Parameter.point(0.75F), Climate.Parameter.point(-0.7F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), Climate.Parameter.point(0.0F), 0.0F, ECBiomeKeys.QUARTZ_DESERT.key());
      }
   }
}

深究下来,整体上区别不大,只不过前者封装到另一个对象中,后者直接在函数体内完成了群系添加。

4. 注册到Regions中

你需要在订阅FMLCommonSetupEvent事件的函数中(即pre-init)将你的Region注册到Regions里,需要用event.enqueueWork()包装,以防止多线程冲突。示例写法为:

Regions.register(new ECOverworldBiomeRegion(ECCommonConfig.EMERALD_CRAFT_OVERWORLD_BIOMES_WEIGHT.get()));
Regions.register(new ECNetherBiomeRegion(ECCommonConfig.EMERALD_CRAFT_NETHER_BIOMES_WEIGHT.get()));

这里使用了Config控制区域生成的权重,当然你也可以将这个值写死,但可配置的值显然更容易被用户接受。

这样,群系就可以被添加到世界生成中了。1.18.1则只需要把Regions改成BiomeProviders即可。

5. 群系的注册与构建

前文粗略地介绍了一下原版的OverworldBiomeBuilder,这里有必要进行详细的介绍模组开发者如何编写自己的OverworldBiomeBuilder。

addBiomes函数的第二个参数即是用于添加群系的消费函数,添加方法为:

mapper.accept(Pair.of(parameters, biome));

parameters是一个Climate.ParameterPoint类型,在Climate类中有两个最常用的工厂函数:

public static Climate.ParameterPoint parameters(float temperature, float humidity, float continentalness,
                                                float erosion, float depth, float weirdness, float offset) {
    return new Climate.ParameterPoint(
        Climate.Parameter.point(temperature),
        Climate.Parameter.point(humidity),
        Climate.Parameter.point(continentalness),
        Climate.Parameter.point(erosion),
        Climate.Parameter.point(depth),
        Climate.Parameter.point(weirdness),
        quantizeCoord(offset)
    );
}

public static Climate.ParameterPoint parameters(Climate.Parameter temperature, Climate.Parameter humidity,
                                                Climate.Parameter continentalness, Climate.Parameter erosion,
                                                Climate.Parameter depth, Climate.Parameter weirdness, float offset) {
    return new Climate.ParameterPoint(
        temperature, humidity, continentalness, erosion, depth, weirdness, quantizeCoord(offset)
    );
}

推荐使用下面的这个,而传入的参数可以从terrablender.api.ParameterUtils中选择,如:

mapper.accept(Pair.of(Climate.parameters(
    ParameterUtils.Temperature.FULL_RANGE.parameter(),
    ParameterUtils.Humidity.HUMID.parameter(),
    ParameterUtils.Continentalness.FAR_INLAND.parameter(),
    ParameterUtils.Erosion.span(ParameterUtils.Erosion.EROSION_5, ParameterUtils.Erosion.EROSION_6),
    ParameterUtils.Depth.UNDERGROUND.parameter(),
    ParameterUtils.Weirdness.FULL_RANGE.parameter(),
    0.5F
), CustomBiomeKeys.MOSSY_CAVES.key()));

上述做法可以将一个名为“覆苔洞穴”的群系加入到世界生成中,在任意温度和奇异度下,侵蚀度较高和非常湿润的内陆地区的地下生成。另外offset的值是0.5,这个值默认是0,越大会使得群系越稀有——这也是下界诡异森林相对罕见的原因之一。

当然,非常建议开发者效仿原版的OverworldBiomeBuilder的逻辑添加群系。

三、SurfaceRule的注册和实现

1. SurfaceRule的注册

TerraBlender提供的SurfaceRule的注册很简单,1.18.1需要使用GenerationSettings的如下两个成员函数注册主世界和下界群系的SurfaceRule:

public static void addBeforeBedrockOverworldSurfaceRules(SurfaceRules.RuleSource rules)
public static void addBeforeBedrockNetherSurfaceRules(int priority, SurfaceRules.RuleSource rules)

而1.18.2+则更改了函数名,使用SurfaceRuleManager$addSurfaceRules函数注册,如:

SurfaceRuleManager.addSurfaceRules(SurfaceRuleManager.RuleCategory.OVERWORLD, MODID, ECSurfaceRules.overworld());
SurfaceRuleManager.addSurfaceRules(SurfaceRuleManager.RuleCategory.NETHER, MODID, ECSurfaceRules.nether());

这一过程也需要写在订阅FMLCommonSetupEvent事件的函数中,同样为了防止线程冲突,要在事件提供的任务队列中进行。推荐直接写在注册到Regions的两段代码前面。

2. SurfaceRule的实现

唯一让人难受的则是SurfaceRules的实现,这里说一个思路,首先复制原版的SurfaceRuleData中的工厂函数,然后将你所想实现的群系和原版群系一一比较,选择地表的实现最为相近的一个,使用ctrl+F标记该群系,然后直接在它后面进行添加修改即可——这一点有些考验开发者对高版本的原版群系的了解水平。

举个最简单例子,绿宝石工艺添加了银杏树林群系,其表面以草方块为主,有蕨类和银杏树的生成,同时一部分表面被灰化土覆盖。对于这个需求,我们要先去区分什么是表面,什么是地物。很显然,蕨类和银杏树都是地物,但草方块和灰化土是表面。

更进一步的,什么其它的群系添加了灰化土?原版的知识告诉我们,巨型针叶林/原始松木针叶林群系、巨型云杉针叶林/原始云杉针叶林群系和竹林群系添加了灰化土。不过稍微有的反直觉的是,竹林的灰化土事实上是地物而不是表面(毕竟原版的新群系系统是一座屎山),但另外两个群系的灰化土确确实实是表面,因而我们可以搜索Biomes.OLD_GROWTH_PINE_TAIGA和Biomes.OLD_GROWTH_SPRUCE_TAIGA:

【开发向】如何在1.18+使用TerraBlender开发你的群系模组(以Forge为例)-第1张图片表面有灰化土的群系很好,直接加到后面就行了:

//......
SurfaceRules.ifTrue(
      SurfaceRules.isBiome(Biomes.WINDSWEPT_GRAVELLY_HILLS),
      SurfaceRules.sequence(
            SurfaceRules.ifTrue(surfaceNoiseAbove(2.0D), stoneLinedGravel),
            SurfaceRules.ifTrue(surfaceNoiseAbove(1.0D), STONE),
            SurfaceRules.ifTrue(surfaceNoiseAbove(-1.0D), grassSurface),
            stoneLinedGravel
      )
),
SurfaceRules.ifTrue(
      SurfaceRules.isBiome(Biomes.OLD_GROWTH_PINE_TAIGA, Biomes.OLD_GROWTH_SPRUCE_TAIGA),
      SurfaceRules.sequence(
            SurfaceRules.ifTrue(surfaceNoiseAbove(1.75D), COARSE_DIRT),
            SurfaceRules.ifTrue(surfaceNoiseAbove(-0.95D), PODZOL)
      )
),
SurfaceRules.ifTrue(SurfaceRules.isBiome(Biomes.ICE_SPIKES), SurfaceRules.ifTrue(isAboveWaterLevel, SNOW_BLOCK)),
SurfaceRules.ifTrue(SurfaceRules.isBiome(Biomes.MUSHROOM_FIELDS), MYCELIUM),
SurfaceRules.ifTrue(
      SurfaceRules.isBiome(ECBiomeKeys.GINKGO_FOREST.key()),
      SurfaceRules.ifTrue(SurfaceRules.noiseCondition(Noises.SURFACE, 0.6D), PODZOL)
),
grassSurface
//......

SurfaceRule实际上就是尝试匹配到不停fall back的过程,仅截取部分而言(前方省略了风袭热带草原、各种山地群系),上下文信息是这里是生成群系最表面那一层方块的代码,含义为:如果当前群系是风袭沙砾丘陵,则在SURFACE噪声大于2.0时按stoneLinedGravel预设生成,反之如果大于1.0则生成石头,反之如果大于-1.0则按grassSurface预设生成,否则按stoneLinedGravel预设生成;如果当前群系不是风袭沙砾丘陵而是两种原始针叶林群系,依旧是根据选择SURFACE噪声选择表面,大于1.75生成砂土,否则大于-0.95生成灰化土,以上均不成立则暂时搁置(最后会提到如何处理);如果当前群系是冰刺平原,且海拔高于海平面,那么表面生成雪块,否则搁置;如果当前群系是蘑菇岛,那么表面生成菌丝;如果当前群系是银杏树林,且SURFACE噪声大于0.6,那么表面生成灰化土,否则搁置;最后以上未提及的群系(平原、森林、热带草原、雪原等)以及所有搁置的情况,按grassSurface预设生成——因此大部分群系的表面都是草方块(沙漠、恶地等群系不走这个预设,是另作考虑的)。

这里使用了SURFACE柏林噪声生成器,实际上原版有若干个辅助地表生成实现的柏林噪声,位于net.minecraft.world.level.levelgen.Noises,当然你也可以实现独立于它们的柏林噪声。这些噪声可以用于条带状生成(如裸岩山峰的方解石条带)、点状生成(如冻洋的浮冰和冰块)、块状生成(如风袭丘陵的大片的石头)等。其区别是,条带状生成通常是仅接受比较大的噪声范围,点状生成则是仅接受极端值或较小的噪声范围,而块状则是接受大于或小于某个值的全部噪声取值。

四、总结

以上则是笔者关于TerraBlender模组用法和教程的拙见,如有不完美或不详细之处,敬请指出。如有其他需要更新的内容,笔者也会尽快修改!