Implement tiled biome distribution
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-22 17:04:34 -04:00
1 parent 6c1c90b171
commit 3ad518232a
5 files changed
+64 -22

No files matched your search

+6
View File
@@ -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
+1 -1
View File
@@ -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.
@@ -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");
@@ -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<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomeParameters
) {
if (age.traits().biomeDistribution() == AgeTraits.BiomeDistribution.SINGLE) {
List<net.minecraft.core.Holder.Reference<Biome>> 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<net.minecraft.core.Holder.Reference<Biome>> 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<net.minecraft.core.Holder.Reference<Biome>> 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));
@@ -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");