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

+5
View File
@@ -340,6 +340,11 @@ Age-seeded biomes in Legacy's diagonal checkerboard layout. Each cell is one
16-by-16-block chunk, and the built-in serializable biome source reconstructs
the same layout whenever the Age reloads.
Version 0.2.24.0 applies Tiny Biome Distribution using three deterministic
Age-seeded biomes and softly bounded seeded regions. Tiny begins at Legacy's
smallest 16-block nominal scale; the serializable biome source retains its
seed, selected biomes, and scale across Age 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.23.0
mod_version=0.2.24.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.
@@ -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;
}
}