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

This commit is contained in:
dnorberg committed 2026-09-22 21:53:14 -04:00
1 parent 4886f8bb7d
commit a7986001f1
8 files changed
+167 -2

No files matched your search

@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModSounds;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModBiomeSources;
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.config.RivencraftConfig;
@@ -31,6 +32,7 @@ public final class Rivencraft {
modContainer.registerConfig(ModConfig.Type.CLIENT, RivencraftClientConfig.SPEC);
modEventBus.addListener(ModNetworking::register);
ModDataComponents.register(modEventBus);
ModBiomeSources.register(modEventBus);
ModBlocks.register(modEventBus);
ModBlockEntities.register(modEventBus);
ModItems.register(modEventBus);
@@ -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 = 11;
public static final int CURRENT_GENERATION_VERSION = 12;
public AgeDefinition {
Objects.requireNonNull(id, "id");
@@ -29,6 +29,7 @@ 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;
import net.dnorberg.rivencraft.world.ScaledBiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.dimension.DimensionType;
@@ -307,6 +308,15 @@ public final class AgeDimensionService {
: TILED_BIOME_SCALE
);
}
if (distribution == AgeTraits.BiomeDistribution.TINY && biomes.size() >= 3) {
List<net.minecraft.core.Holder.Reference<Biome>> selected = new java.util.ArrayList<>(biomes);
Collections.shuffle(selected, new java.util.Random(age.seed() ^ 0x54494E5942494F4DL));
return new ScaledBiomeSource(
HolderSet.direct(List.of(selected.get(0), selected.get(1), selected.get(2))),
age.seed(),
0
);
}
return MultiNoiseBiomeSource.createFromPreset(
biomeParameters.getOrThrow(MultiNoiseBiomeSourceParameterLists.OVERWORLD));
}
@@ -183,6 +183,15 @@ public final class LinkingGameTests {
selectedTraits.celestialAngle(),
selectedTraits.celestialPhase()
)
: profile.equals(AgeGenerationProfiles.CAVE)
? new AgeTraits(
AgeTraits.BiomeDistribution.TINY,
selectedTraits.lighting(),
selectedTraits.weather(),
selectedTraits.celestialPeriod(),
selectedTraits.celestialAngle(),
selectedTraits.celestialPhase()
)
: selectedTraits;
AgeDefinition definition = new AgeDefinition(
id,
@@ -245,6 +254,30 @@ public final class LinkingGameTests {
helper.assertTrue(origin.equals(diagonalChunk),
"Diagonal Grid chunks must repeat the biome");
}
if (profile.equals(AgeGenerationProfiles.CAVE)) {
var biomeSource = level.getChunkSource().getGenerator().getBiomeSource();
helper.assertTrue(biomeSource instanceof net.dnorberg.rivencraft.world.ScaledBiomeSource,
"Tiny Biome Distribution must use the serializable scaled biome source");
helper.assertValueEqual(
biomeSource.possibleBiomes().size(),
3,
"Tiny Biome Distribution must expose exactly three seeded biomes"
);
java.util.Set<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>> sampled =
new java.util.HashSet<>();
for (int x = -32; x <= 32; x += 4) {
for (int z = -32; z <= 32; z += 4) {
sampled.add(biomeSource.getNoiseBiome(x, 0, z, null));
}
}
helper.assertTrue(sampled.size() >= 2,
"Tiny Biome Distribution must form multiple seeded regions near spawn");
helper.assertValueEqual(
biomeSource.getNoiseBiome(7, 0, -5, null),
biomeSource.getNoiseBiome(7, 0, -5, null),
"Tiny biome sampling must be deterministic"
);
}
if (definition.traits().lighting() == AgeTraits.Lighting.DARK) {
helper.assertFalse(level.dimensionType().hasSkyLight(),
"Dark Lighting Ages must suppress dimension skylight");
@@ -0,0 +1,26 @@
package net.dnorberg.rivencraft.registry;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.world.ScaledBiomeSource;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.biome.BiomeSource;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredRegister;
/** Serializable biome-source types used by generated Ages. */
public final class ModBiomeSources {
private static final DeferredRegister<MapCodec<? extends BiomeSource>> BIOME_SOURCES =
DeferredRegister.create(Registries.BIOME_SOURCE, Rivencraft.MOD_ID);
static {
BIOME_SOURCES.register("scaled", () -> ScaledBiomeSource.CODEC);
}
private ModBiomeSources() {
}
public static void register(IEventBus modEventBus) {
BIOME_SOURCES.register(modEventBus);
}
}
@@ -0,0 +1,89 @@
package net.dnorberg.rivencraft.world;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.stream.Stream;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
/** Seeded, softly bounded biome regions at the five Mystcraft distribution scales. */
public final class ScaledBiomeSource extends BiomeSource {
public static final MapCodec<ScaledBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Biome.LIST_CODEC.fieldOf("biomes").forGetter(source -> source.biomes),
Codec.LONG.fieldOf("seed").forGetter(source -> source.seed),
Codec.intRange(0, 4).fieldOf("scale").forGetter(source -> source.scale)
).apply(instance, ScaledBiomeSource::new));
private final HolderSet<Biome> biomes;
private final long seed;
private final int scale;
private final int cellWidth;
public ScaledBiomeSource(HolderSet<Biome> biomes, long seed, int scale) {
if (biomes.size() < 3) {
throw new IllegalArgumentException("Scaled biome distributions require at least three biomes");
}
this.biomes = biomes;
this.seed = seed;
this.scale = scale;
// BiomeSource coordinates are quart positions. Legacy applies two zooms
// before the selected scale, yielding 16-block Tiny regions that double
// with every subsequent distribution size.
this.cellWidth = 4 << scale;
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
return CODEC;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return biomes.stream();
}
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
int baseX = Math.floorDiv(x, cellWidth);
int baseZ = Math.floorDiv(z, cellWidth);
int closestX = baseX;
int closestZ = baseZ;
double closestDistance = Double.POSITIVE_INFINITY;
for (int cellX = baseX - 1; cellX <= baseX + 1; cellX++) {
for (int cellZ = baseZ - 1; cellZ <= baseZ + 1; cellZ++) {
long cellSeed = mix(seed, cellX, cellZ);
double centerX = (cellX + 0.5D + jitter(cellSeed) * 0.8D) * cellWidth;
double centerZ = (cellZ + 0.5D + jitter(mix64(cellSeed)) * 0.8D) * cellWidth;
double deltaX = x - centerX;
double deltaZ = z - centerZ;
double distance = deltaX * deltaX + deltaZ * deltaZ;
if (distance < closestDistance) {
closestDistance = distance;
closestX = cellX;
closestZ = cellZ;
}
}
}
return biomes.get(Math.floorMod((int)mix(seed ^ 0x42494F4D45534CL, closestX, closestZ), biomes.size()));
}
private static double jitter(long value) {
return ((value >>> 11) * 0x1.0p-53) - 0.5D;
}
private static long mix(long seed, int x, int z) {
return mix64(seed ^ (long)x * 0x9E3779B97F4A7C15L ^ (long)z * 0xC2B2AE3D27D4EB4FL);
}
private static long mix64(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}