diff --git a/docs/PORTING.md b/docs/PORTING.md index 8886022..f7c1eab 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -350,6 +350,11 @@ Large, and Huge reuse Tiny's seeded, softly bounded regions at nominal widths of 32, 64, 128, and 256 blocks. Every size selects three deterministic biomes and serializes the selection, Age seed, and scale with the runtime dimension. +Version 0.2.25.1 applies biome-specific material rules without replacing an +Age's selected terrain profile. End biome columns generate from End stone; +Nether biome columns use Minecraft's Nether surface rules for netherrack, +soul sand, basalt, and nylium instead of inheriting Overworld blocks. + Version 0.2.14.0 restores the legacy Linking and Descriptive Book artwork for inventory items and gives each book its matching open-book texture when placed on a Book Stand. Return Linking Books created inside another Age also retain and diff --git a/gradle.properties b/gradle.properties index d76ee2a..41fa8fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,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.2.25.0 +mod_version=0.2.25.1 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index a902f45..d7e8f02 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -11,7 +11,9 @@ import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders; import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; +import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; +import net.minecraft.data.worldgen.SurfaceRuleData; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -37,6 +39,7 @@ import net.minecraft.world.level.levelgen.FlatLevelSource; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.SurfaceRules; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; import net.neoforged.fml.ModList; @@ -273,9 +276,47 @@ public final class AgeDimensionService { // Island/Skylands uses uniform floating-island density without the // End's central dragon island. Both use the Overworld environment. // random_vanilla is retained as a migration alias for existing Ages. - return new NoiseBasedChunkGenerator( - createBiomeSource(server, age, biomeParameters), - noiseSettings.getOrThrow(settingsKey) + BiomeSource biomeSource = createBiomeSource(server, age, biomeParameters); + NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value(); + return new NoiseBasedChunkGenerator(biomeSource, Holder.direct(withBiomeMaterialRules(baseSettings))); + } + + private static NoiseGeneratorSettings withBiomeMaterialRules(NoiseGeneratorSettings base) { + SurfaceRules.RuleSource biomeMaterials = SurfaceRules.sequence( + SurfaceRules.ifTrue( + SurfaceRules.isBiome( + Biomes.THE_END, + Biomes.END_HIGHLANDS, + Biomes.END_MIDLANDS, + Biomes.SMALL_END_ISLANDS, + Biomes.END_BARRENS + ), + SurfaceRuleData.end() + ), + SurfaceRules.ifTrue( + SurfaceRules.isBiome( + Biomes.NETHER_WASTES, + Biomes.SOUL_SAND_VALLEY, + Biomes.CRIMSON_FOREST, + Biomes.WARPED_FOREST, + Biomes.BASALT_DELTAS + ), + SurfaceRuleData.nether() + ), + base.surfaceRule() + ); + return new NoiseGeneratorSettings( + base.noiseSettings(), + base.defaultBlock(), + base.defaultFluid(), + base.noiseRouter(), + biomeMaterials, + base.spawnTarget(), + base.seaLevel(), + base.disableMobGeneration(), + base.isAquifersEnabled(), + base.oreVeinsEnabled(), + base.useLegacyRandomSource() ); } diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index 87d5df5..2ebb449 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -22,6 +22,7 @@ import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.phys.Vec3; import java.util.UUID; @@ -347,9 +348,77 @@ public final class LinkingGameTests { distribution + " Biome Distribution must use its Legacy scale" ); } + + assertSingleBiomeSurfaceMaterial(helper, Biomes.THE_END, Blocks.END_STONE); + assertSingleBiomeSurfaceMaterial(helper, Biomes.NETHER_WASTES, Blocks.NETHERRACK); helper.succeed(); } + private static void assertSingleBiomeSurfaceMaterial( + GameTestHelper helper, + net.minecraft.resources.ResourceKey biome, + net.minecraft.world.level.block.Block expectedBlock + ) { + var biomeRegistry = helper.getLevel().registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME); + var sortedBiomes = biomeRegistry.listElements() + .sorted(java.util.Comparator.comparing(holder -> holder.key().location().toString())) + .toList(); + int targetIndex = -1; + for (int index = 0; index < sortedBiomes.size(); index++) { + if (sortedBiomes.get(index).is(biome)) { + targetIndex = index; + break; + } + } + helper.assertTrue(targetIndex >= 0, "The requested material-test biome must be registered"); + + long seed = 0L; + while (Math.floorMod(Long.hashCode(seed ^ 0x53494E474C454249L), sortedBiomes.size()) != targetIndex) { + seed++; + } + UUID id = UUID.randomUUID(); + AgeDefinition definition = new AgeDefinition( + id, + "Material " + biome.location(), + seed, + AgeGenerationProfiles.STANDARD, + AgeDefinition.CURRENT_GENERATION_VERSION, + helper.getLevel().getGameTime(), + net.minecraft.resources.ResourceLocation.fromNamespaceAndPath( + Rivencraft.MOD_ID, "gametest/material_" + biome.location().getPath() + "_" + id + ), + null, + new AgeTraits( + AgeTraits.BiomeDistribution.SINGLE, + AgeTraits.Lighting.NORMAL, + AgeTraits.Weather.NORMAL, + 1.0D, + 0.0F, + 0.0F + ) + ); + var level = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), definition); + helper.assertTrue(level != null, biome.location() + " material test Age must load"); + if (level == null) { + return; + } + helper.assertTrue(level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream().allMatch(holder -> holder.is(biome)), + "The material test Age must select " + biome.location()); + // Sample away from the generated arrival platform around the Age spawn. + int sampleX = 64; + int sampleZ = 64; + level.getChunk(sampleX >> 4, sampleZ >> 4); + int surfaceY = level.getHeight( + net.minecraft.world.level.levelgen.Heightmap.Types.OCEAN_FLOOR_WG, + sampleX, + sampleZ + ) - 1; + var surfaceState = level.getBlockState(new BlockPos(sampleX, surfaceY, sampleZ)); + helper.assertTrue(surfaceState.is(expectedBlock), + biome.location() + " terrain must expose " + expectedBlock.getDescriptionId() + + " at the surface, found " + surfaceState); + } + @GameTest(template = "empty") public static void travelUsesLeafyDestinationWithoutFloor(GameTestHelper helper) { helper.setBlock(ORIGIN.below(), Blocks.AIR);