From a287e092f5e54ffbc767c10af2bcbf3bab008863 Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Thu, 8 Oct 2026 02:29:59 -0400 Subject: [PATCH] Use Nether materials for Nether cave biomes (#59) --- gradle.properties | 2 +- .../rivencraft/book/AgeDimensionService.java | 5 ++- .../book/FeatureWritingGameTests.java | 27 ++++++++++++++ .../world/AgeFeatureChunkGenerator.java | 37 ++++++++++++++++++- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 371f7f2..fdce9eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,7 +36,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.4.19.2 +mod_version=0.4.20.0 immersive_portals_version=6.0.7 terrablender_version=4.1.0.8 serene_seasons_version_id=B6nTjh4b diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index 9403a52..d624fa9 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -15,6 +15,7 @@ import java.util.Random; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders; import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility; +import net.dnorberg.rivencraft.compat.TerraBlenderBiomes; import net.dnorberg.rivencraft.writing.OrderedWriting; import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; @@ -337,9 +338,11 @@ public final class AgeDimensionService { Registries.NOISE_SETTINGS, profile.noiseSettings()); NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value(); Holder settings = Holder.direct(withBiomeMaterialRules(baseSettings)); + boolean caveTerrain = AgeBiomeSelection.isCave(profile); return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION ? new NoiseBasedChunkGenerator(biomeSource, settings) - : new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed()); + : new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed(), + caveTerrain, caveTerrain ? TerraBlenderBiomes.nether(server.registryAccess()) : List.of()); } private static NoiseGeneratorSettings withBiomeMaterialRules(NoiseGeneratorSettings base) { diff --git a/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java index e1b51ff..ac3f5c0 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java @@ -14,6 +14,7 @@ import net.dnorberg.rivencraft.writing.SymbolCatalog; import net.dnorberg.rivencraft.writing.SymbolReference; import net.dnorberg.rivencraft.writing.WritingEntry; import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Blocks; import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.resources.ResourceKey; @@ -460,6 +461,32 @@ public final class FeatureWritingGameTests { }); } + @GameTest(template = "empty", timeoutTicks = 600) + public static void netherCaveBiomeUsesNetherRock(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + OrderedWriting writing = new OrderedWriting(List.of(panel(), page("terrain/cave"), + page("biome/nether_wastes"), page("biome/single"))); + var target = AgeDimensionService.resolve(player, + new AgeReference(UUID.randomUUID(), "Nether Cave Materials"), writing).orElseThrow(); + helper.runAfterDelay(1, () -> { + var level = helper.getLevel().getServer().getLevel(target.location().dimension()); + helper.assertTrue(level != null, "The Nether cave Age must load"); + var chunk = level.getChunk(0, 0); + int[] rock = new int[2]; + chunk.findBlocks(state -> state.is(Blocks.NETHERRACK) || state.is(Blocks.STONE) + || state.is(Blocks.DEEPSLATE) || state.is(Blocks.DIRT) + || state.is(Blocks.GRASS_BLOCK), + (pos, state) -> { + if (state.is(Blocks.NETHERRACK)) rock[0]++; + else rock[1]++; + }); + helper.assertTrue(rock[0] > 0, "Nether cave terrain must contain netherrack"); + helper.assertValueEqual(rock[1], 0, + "Nether cave terrain must not retain stone, deepslate, dirt, or grass"); + helper.succeed(); + }); + } + private static void rejects(GameTestHelper helper, OrderedWriting writing, String expected) { try { AgeGenerationPlan.compile(writing, 87L, AgeGenerationProfiles.STANDARD, diff --git a/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java b/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java index 1e38d41..de8f165 100644 --- a/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java +++ b/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java @@ -6,6 +6,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import net.dnorberg.rivencraft.book.AgeFeatureSelection; import net.dnorberg.rivencraft.book.AgeStructureSelection; @@ -16,6 +17,7 @@ import net.minecraft.core.Holder; import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.QuartPos; +import net.minecraft.tags.BiomeTags; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.RandomSource; @@ -55,23 +57,54 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator { instance.group(BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.getBiomeSource()), NoiseGeneratorSettings.CODEC.fieldOf("settings").forGetter(generator -> generator.settings), AgeFeatureSelection.Choice.CODEC.listOf().fieldOf("age_features").forGetter(generator -> generator.features), - Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)) + Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed), + Codec.BOOL.optionalFieldOf("nether_cave_materials", false).forGetter(generator -> generator.netherCaveMaterials), + ResourceLocation.CODEC.listOf().optionalFieldOf("nether_biomes", List.of()) + .forGetter(generator -> generator.netherBiomeIds)) .apply(instance, AgeFeatureChunkGenerator::new)); private final Holder settings; private final List features; private final long ageSeed; + private final boolean netherCaveMaterials; + private final List netherBiomeIds; public AgeFeatureChunkGenerator(BiomeSource biomes, Holder settings, - List features, long ageSeed) { + List features, long ageSeed, + boolean netherCaveMaterials, List netherBiomeIds) { super(biomes, settings); this.settings = settings; this.features = List.copyOf(features); this.ageSeed = ageSeed; + this.netherCaveMaterials = netherCaveMaterials; + this.netherBiomeIds = List.copyOf(netherBiomeIds); } public List ageFeatures() { return features; } + @Override + public void buildSurface(WorldGenRegion level, StructureManager structures, RandomState random, ChunkAccess chunk) { + super.buildSurface(level, structures, random, chunk); + if (!netherCaveMaterials) return; + Set netherIds = Set.copyOf(netherBiomeIds); + // The Cave noise router starts with stone even in Nether biomes. Surface rules + // only touch exposed layers, so convert the base rock (and any Overworld soil) + // before carvers, ores, and biome decorations run. Never change adjacent biomes. + chunk.findBlocks(AgeFeatureChunkGenerator::isOverworldCaveMaterial, (pos, state) -> { + Holder biome = chunk.getNoiseBiome( + QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())); + if (biome.is(BiomeTags.IS_NETHER) + || biome.unwrapKey().map(key -> netherIds.contains(key.location())).orElse(false)) { + chunk.setBlockState(pos, Blocks.NETHERRACK.defaultBlockState(), false); + } + }); + } + + private static boolean isOverworldCaveMaterial(net.minecraft.world.level.block.state.BlockState state) { + return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF) + || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK); + } + @Override protected MapCodec codec() { return CODEC; }