diff --git a/docs/PORTING.md b/docs/PORTING.md index 945a146..ca32a2b 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -323,6 +323,12 @@ Overworld-like surfaces together with the persisted Native or Single biome distribution. This prevents Nether terrain generation from leaking into newly generated Cave Age chunks. +Version 0.2.22.0 applies Tiled Biome Distribution using two deterministic +Age-seeded biomes in the legacy diagonal-checkerboard layout. Each square is +16 chunks (256 blocks) wide, matching Mystcraft Legacy's coordinate shift, and +uses Minecraft's serializable checkerboard biome source so the layout remains +stable across server reloads. + 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 a9dc751..9b52c20 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.21.1 +mod_version=0.2.22.0 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/AgeDefinition.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java index ea6b507..0208825 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java @@ -21,7 +21,7 @@ public record AgeDefinition( public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath( Rivencraft.MOD_ID, "random_vanilla" ); - public static final int CURRENT_GENERATION_VERSION = 9; + public static final int CURRENT_GENERATION_VERSION = 10; public AgeDefinition { Objects.requireNonNull(id, "id"); diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index ff900f4..cde5c89 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -6,10 +6,12 @@ import java.util.Optional; import java.util.OptionalLong; import java.util.List; import java.util.Comparator; +import java.util.Collections; 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.HolderSet; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -23,6 +25,7 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource; import net.minecraft.world.level.biome.FixedBiomeSource; import net.minecraft.world.level.biome.MultiNoiseBiomeSource; import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterLists; @@ -42,6 +45,7 @@ public final class AgeDimensionService { private static final int SURFACE_SEARCH_RADIUS = 16; private static final int FALLBACK_PLATFORM_Y = 63; private static final int CAVE_SPAWN_SEARCH_Y = 64; + private static final int TILED_BIOME_SCALE = 4; private AgeDimensionService() { } @@ -278,16 +282,22 @@ public final class AgeDimensionService { AgeDefinition age, net.minecraft.core.HolderGetter biomeParameters ) { - if (age.traits().biomeDistribution() == AgeTraits.BiomeDistribution.SINGLE) { - List> biomes = server.registryAccess() - .lookupOrThrow(Registries.BIOME) - .listElements() - .sorted(Comparator.comparing(holder -> holder.key().location().toString())) - .toList(); - if (!biomes.isEmpty()) { - int index = Math.floorMod(Long.hashCode(age.seed() ^ 0x53494E474C454249L), biomes.size()); - return new FixedBiomeSource(biomes.get(index)); - } + List> biomes = server.registryAccess() + .lookupOrThrow(Registries.BIOME) + .listElements() + .sorted(Comparator.comparing(holder -> holder.key().location().toString())) + .toList(); + if (age.traits().biomeDistribution() == AgeTraits.BiomeDistribution.SINGLE && !biomes.isEmpty()) { + int index = Math.floorMod(Long.hashCode(age.seed() ^ 0x53494E474C454249L), biomes.size()); + return new FixedBiomeSource(biomes.get(index)); + } + if (age.traits().biomeDistribution() == AgeTraits.BiomeDistribution.TILED && biomes.size() >= 2) { + List> selected = new java.util.ArrayList<>(biomes); + Collections.shuffle(selected, new java.util.Random(age.seed() ^ 0x54494C454442494FL)); + return new CheckerboardColumnBiomeSource( + HolderSet.direct(List.of(selected.get(0), selected.get(1))), + TILED_BIOME_SCALE + ); } return MultiNoiseBiomeSource.createFromPreset( biomeParameters.getOrThrow(MultiNoiseBiomeSourceParameterLists.OVERWORLD)); diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index 4e693b5..9af0b8b 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -155,6 +155,26 @@ public final class LinkingGameTests { public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) { for (var profile : AgeGenerationProfiles.all()) { UUID id = UUID.randomUUID(); + AgeTraits selectedTraits = AgeTraits.select(id.getMostSignificantBits()); + AgeTraits traits = profile.equals(AgeGenerationProfiles.STANDARD) + ? new AgeTraits( + AgeTraits.BiomeDistribution.SINGLE, + AgeTraits.Lighting.NORMAL, + AgeTraits.Weather.NORMAL, + 1.0D, + 0.0F, + 0.0F + ) + : profile.equals(AgeGenerationProfiles.AMPLIFIED) + ? new AgeTraits( + AgeTraits.BiomeDistribution.TILED, + selectedTraits.lighting(), + selectedTraits.weather(), + selectedTraits.celestialPeriod(), + selectedTraits.celestialAngle(), + selectedTraits.celestialPhase() + ) + : selectedTraits; AgeDefinition definition = new AgeDefinition( id, "Profile " + profile.getPath(), @@ -166,16 +186,7 @@ public final class LinkingGameTests { Rivencraft.MOD_ID, "gametest/" + profile.getPath() + "_" + id ), null, - profile.equals(AgeGenerationProfiles.STANDARD) - ? new AgeTraits( - AgeTraits.BiomeDistribution.SINGLE, - AgeTraits.Lighting.NORMAL, - AgeTraits.Weather.NORMAL, - 1.0D, - 0.0F, - 0.0F - ) - : AgeTraits.select(id.getMostSignificantBits()) + traits ); var level = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), definition); helper.assertTrue(level != null, "The " + profile + " profile must load a runtime dimension"); @@ -192,6 +203,21 @@ public final class LinkingGameTests { "Single Biome Distribution must expose exactly one biome" ); } + if (profile.equals(AgeGenerationProfiles.AMPLIFIED)) { + var biomeSource = level.getChunkSource().getGenerator().getBiomeSource(); + helper.assertValueEqual( + biomeSource.possibleBiomes().size(), + 2, + "Tiled Biome Distribution must expose exactly two seeded biomes" + ); + var origin = biomeSource.getNoiseBiome(0, 0, 0, null); + var eastTile = biomeSource.getNoiseBiome(64, 0, 0, null); + var diagonalTile = biomeSource.getNoiseBiome(64, 0, 64, null); + helper.assertFalse(origin.equals(eastTile), + "Adjacent 256-block Tiled squares must alternate biomes"); + helper.assertTrue(origin.equals(diagonalTile), + "Diagonal 256-block Tiled squares must repeat the biome"); + } if (definition.traits().lighting() == AgeTraits.Lighting.DARK) { helper.assertFalse(level.dimensionType().hasSkyLight(), "Dark Lighting Ages must suppress dimension skylight");