feat: latitude biome system with two world types + map validation command

Add a latitude-based world generation system: biomes are distributed by Z
coordinate (frozen north -> temperate equator -> hot south) with extremely
large biomes on a continental scale, plus full Biomes O' Plenty support.

World types (selectable in the world creation 'World Type' button):
- Ultra Wide Biome: latitude biomes + vanilla terrain (immune to Tectonic
  via a private noise_settings copy under custom_ore_gen).
- Tectonic Ultra Wide Biome: latitude biomes + minecraft:overworld terrain
  (uses Tectonic when present, vanilla otherwise).

Core implementation:
- LatitudeBiomeSource: custom BiomeSource distributing biomes by latitude.
  Temperature derived from Z with a boundary wobble, dual-octave selector
  noise for a flat biome distribution (no biome dominates), land/ocean mask,
  underground cave layer, moisture-driven rare swamp/mangrove pockets, and a
  guaranteed safe spawn zone (plains/forests) around the origin.
- BiomeBand: 5 climate bands (FROZEN/COLD/TEMPERATE/WARM/HOT) with vanilla
  surface pools + dedicated climate tags (latitude_*_surface) for optional
  BOP biomes via required:false, plus ocean and underground pools.
- WorldGenRegistration: DeferredRegister for the 'custom_ore_gen:latitude'
  BiomeSource codec.
- LatitudeSpawnHandler: pins spawn to a plains/forest biome on overworld load.

Validation:
- /latitude map [radius] [step]: samples the LatitudeBiomeSource on a large
  grid, renders a top-down PNG map (run/latitude/latitude_map.png) and writes
  a per-band distribution + invariant report (run/latitude/latitude_report.txt).

Constants tuned for a continental scale:
  TEMPERATURE_SCALE = 16000 (equator->pole)
  SURFACE_SELECTOR_SCALE = 0.00033 (biomes ~3000 blocks wide)

Swamp fix: removed from the common temperate surface tag and made rare
(~8% of temperate land via moisture noise), matching vanilla humidity biomes.
This commit is contained in:
feldenr
2026-06-14 22:59:09 +02:00
parent 74480d9d2c
commit 7729eeb65c
15 changed files with 3631 additions and 2 deletions
@@ -29,6 +29,7 @@ import com.mojang.serialization.MapCodec;
import net.neoforged.neoforge.registries.DeferredRegister;
import net.neoforged.neoforge.registries.NeoForgeRegistries;
import net.mcreator.customoregen.loot.CustomOreLootModifier;
import net.mcreator.customoregen.worldgen.WorldGenRegistration;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.List;
@@ -58,6 +59,7 @@ public class CustomOreGenMod {
LOOT_MODIFIERS.register(modEventBus);
// Start of user code block mod init
WorldGenRegistration.BIOME_SOURCES.register(modEventBus);
// End of user code block mod init
}
@@ -0,0 +1,147 @@
package net.mcreator.customoregen.worldgen;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderGetter;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import java.util.ArrayList;
import java.util.List;
/**
* Latitude climate bands used by {@link LatitudeBiomeSource}.
*
* <p>The world is split along the Z axis (latitude) into climate bands:
* the far north is frozen, the equator is temperate, and the far south is hot.
* Surface biomes are selected from dedicated climate tags (so optional mods such
* as Biomes O' Plenty are supported gracefully). Ocean and underground (cave) pools
* are vanilla-only and resolved eagerly because those biomes always exist.</p>
*/
public enum BiomeBand {
FROZEN(-1.0),
COLD(-0.5),
TEMPERATE(0.0),
WARM(0.5),
HOT(1.0);
/** Mid-temperature of the band, in [-1.0, 1.0]. */
public final double centerTemperature;
BiomeBand(double centerTemperature) {
this.centerTemperature = centerTemperature;
}
public static BiomeBand fromTemperature(double temperature) {
if (temperature <= -0.7) return FROZEN;
if (temperature <= -0.25) return COLD;
if (temperature < 0.25) return TEMPERATE;
if (temperature < 0.7) return WARM;
return HOT;
}
/** Surface biome tag for this band (vanilla + optional Biomes O' Plenty). */
public TagKey<Biome> surfaceTag() {
switch (this) {
case FROZEN:
case COLD:
return tag("latitude_cold_surface");
case TEMPERATE:
return tag("latitude_temperate_surface");
case WARM:
case HOT:
return tag("latitude_hot_surface");
default:
return tag("latitude_temperate_surface");
}
}
/**
* Vanilla surface biomes, used to declare {@code possibleBiomes} safely during worldgen
* data loading. The full (mod-aware) surface pool is resolved lazily from {@link #surfaceTag()}.
*/
public List<ResourceKey<Biome>> surfaceVanilla() {
switch (this) {
case FROZEN:
return List.of(
Biomes.SNOWY_PLAINS, Biomes.ICE_SPIKES, Biomes.SNOWY_TAIGA,
Biomes.GROVE, Biomes.SNOWY_SLOPES, Biomes.JAGGED_PEAKS, Biomes.FROZEN_PEAKS
);
case COLD:
return List.of(
Biomes.TAIGA, Biomes.OLD_GROWTH_PINE_TAIGA, Biomes.OLD_GROWTH_SPRUCE_TAIGA,
Biomes.WINDSWEPT_HILLS, Biomes.WINDSWEPT_FOREST, Biomes.WINDSWEPT_GRAVELLY_HILLS,
Biomes.GROVE, Biomes.SNOWY_SLOPES
);
case TEMPERATE:
return List.of(
Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS, Biomes.FOREST, Biomes.BIRCH_FOREST,
Biomes.OLD_GROWTH_BIRCH_FOREST, Biomes.DARK_FOREST, Biomes.FLOWER_FOREST,
Biomes.SWAMP, Biomes.MEADOW, Biomes.CHERRY_GROVE
);
case WARM:
return List.of(
Biomes.SAVANNA, Biomes.SAVANNA_PLATEAU, Biomes.WINDSWEPT_SAVANNA,
Biomes.BIRCH_FOREST, Biomes.FOREST, Biomes.PLAINS, Biomes.MEADOW
);
case HOT:
return List.of(
Biomes.DESERT, Biomes.BADLANDS, Biomes.WOODED_BADLANDS, Biomes.ERODED_BADLANDS,
Biomes.SAVANNA, Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE,
Biomes.MANGROVE_SWAMP
);
default:
return List.of(Biomes.PLAINS);
}
}
public List<ResourceKey<Biome>> ocean() {
switch (this) {
case FROZEN:
return List.of(Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.FROZEN_RIVER);
case COLD:
return List.of(Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN);
case TEMPERATE:
return List.of(Biomes.OCEAN, Biomes.DEEP_OCEAN, Biomes.RIVER);
case WARM:
return List.of(Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN);
case HOT:
return List.of(Biomes.WARM_OCEAN);
default:
return List.of(Biomes.OCEAN);
}
}
public List<ResourceKey<Biome>> underground() {
switch (this) {
case FROZEN:
return List.of(Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
case COLD:
return List.of(Biomes.DRIPSTONE_CAVES);
case TEMPERATE:
return List.of(Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES);
case WARM:
return List.of(Biomes.LUSH_CAVES);
case HOT:
return List.of(Biomes.LUSH_CAVES, Biomes.DEEP_DARK);
default:
return List.of(Biomes.LUSH_CAVES);
}
}
/** Resolve a list of (possibly absent) biome keys into actual holders, skipping missing ones. */
public static List<Holder<Biome>> resolve(HolderGetter<Biome> getter, List<ResourceKey<Biome>> keys) {
List<Holder<Biome>> holders = new ArrayList<>();
for (ResourceKey<Biome> key : keys) {
getter.get(key).ifPresent(holders::add);
}
return holders;
}
private static TagKey<Biome> tag(String name) {
return TagKey.create(Registries.BIOME, ResourceLocation.fromNamespaceAndPath("custom_ore_gen", name));
}
}
@@ -0,0 +1,263 @@
package net.mcreator.customoregen.worldgen;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.RegistryOps;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.synth.ImprovedNoise;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
/**
* Custom {@link BiomeSource} that distributes biomes by latitude (Z axis).
*
* <ul>
* <li>Far north (large negative Z) = frozen / cold biomes</li>
* <li>Equator (Z &asymp; 0) = temperate biomes</li>
* <li>Far south (large positive Z) = hot biomes</li>
* </ul>
*
* <p>Temperature is derived from the Z coordinate with a gentle boundary perturbation so the
* climate bands are not perfectly straight lines. Inside each band, a very low-frequency
* selector noise picks among the band's biomes, producing extremely large biomes that
* encourage exploration and long-distance travel (railways, roads, nether hubs).</p>
*
* <p>Surface biomes come from dedicated climate tags (see {@code data/custom_ore_gen/tags/
* worldgen/biome/latitude_*_surface.json}), which support optional Biomes O' Plenty biomes
* via {@code "required": false} without ever creating unbound holders. Ocean and underground
* (cave) pools are vanilla-only.</p>
*/
public class LatitudeBiomeSource extends BiomeSource {
public static final MapCodec<LatitudeBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.LONG.fieldOf("seed").forGetter(src -> src.seed),
RegistryOps.retrieveGetter(Registries.BIOME)
).apply(instance, instance.stable(LatitudeBiomeSource::new)));
// ------------------------------------------------------------------
// Tunable constants
// ------------------------------------------------------------------
/** Number of blocks for the temperature to go from 0 (equator) to +-1 (pole). */
private static final double TEMPERATURE_SCALE = 16000.0;
/** Noise frequency for the climate band boundary wobble. */
private static final double BOUNDARY_NOISE_SCALE = 0.00015;
/** Amplitude of the boundary wobble (in temperature units). */
private static final double BOUNDARY_NOISE_AMPLITUDE = 0.15;
/** Selector noise frequency for surface sub-biomes (very low = very large biomes). */
private static final double SURFACE_SELECTOR_SCALE = 0.00033;
/** Frequency of the land/ocean mask noise. */
private static final double OCEAN_NOISE_SCALE = 0.0011;
/** Above this ocean-noise value, the column is ocean. */
private static final double OCEAN_THRESHOLD = 0.30;
/** Frequency of the moisture noise that carves rare swamp/mangrove pockets. */
private static final double MOISTURE_SCALE = 0.0009;
/** Above this moisture value a wet biome (swamp/mangrove) overrides the surface. ~8% of land. */
private static final double MOISTURE_THRESHOLD = 0.55;
/** Below this block Y we resolve underground (cave) biomes. */
private static final int UNDERGROUND_Y = 30;
/** Half-size of the guaranteed safe spawn square around the origin (plains/forest). */
private static final int SPAWN_SAFE_RADIUS = 96;
/** Selector frequency inside the spawn safe zone (finer, for gentle variety). */
private static final double SPAWN_SELECTOR_SCALE = 0.02;
// ------------------------------------------------------------------
// Fields
// ------------------------------------------------------------------
private final long seed;
private final HolderGetter<Biome> biomeGetter;
private final ImprovedNoise boundaryNoise;
private final ImprovedNoise selectorNoise;
private final ImprovedNoise oceanNoise;
private final ImprovedNoise moistureNoise;
/** Vanilla-only pools resolved eagerly (safe: vanilla biomes always exist and are bound). */
private final EnumMap<BiomeBand, ResolvedBand> resolvedVanilla;
private final Set<Holder<Biome>> possibleBiomes;
private final Holder<Biome> fallback;
/** Safe spawn pool (plains / forests) resolved eagerly. Always near the origin. */
private final List<Holder<Biome>> spawnSafeBiomes;
/** Rare wet biomes (swamp = temperate, mangrove = warm/hot), placed by moisture noise. */
private final Holder<Biome> swampBiome;
private final Holder<Biome> mangroveBiome;
/** Mod-aware surface pools resolved lazily from climate tags (safe: tags are bound by then). */
private volatile EnumMap<BiomeBand, List<Holder<Biome>>> surfaceFromTag;
public LatitudeBiomeSource(long seed, HolderGetter<Biome> biomeGetter) {
this.seed = seed;
this.biomeGetter = biomeGetter;
RandomSource rng = RandomSource.create(seed);
this.boundaryNoise = new ImprovedNoise(rng);
this.selectorNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x4C415449L));
this.oceanNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x4F434541L));
this.moistureNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x57455421L));
this.resolvedVanilla = new EnumMap<>(BiomeBand.class);
Set<Holder<Biome>> all = new HashSet<>();
for (BiomeBand band : BiomeBand.values()) {
List<Holder<Biome>> surface = BiomeBand.resolve(biomeGetter, band.surfaceVanilla());
List<Holder<Biome>> ocean = BiomeBand.resolve(biomeGetter, band.ocean());
List<Holder<Biome>> underground = BiomeBand.resolve(biomeGetter, band.underground());
resolvedVanilla.put(band, new ResolvedBand(surface, ocean, underground));
all.addAll(surface);
all.addAll(ocean);
all.addAll(underground);
}
this.possibleBiomes = all;
List<Holder<Biome>> safe = new ArrayList<>();
safe.addAll(BiomeBand.resolve(biomeGetter, List.of(
Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS,
Biomes.FOREST, Biomes.BIRCH_FOREST, Biomes.FLOWER_FOREST,
Biomes.MEADOW
)));
this.spawnSafeBiomes = safe.isEmpty() ? null : safe;
this.swampBiome = biomeGetter.get(Biomes.SWAMP).orElse(null);
this.mangroveBiome = biomeGetter.get(Biomes.MANGROVE_SWAMP).orElse(null);
Holder<Biome> plains = biomeGetter.get(Biomes.PLAINS).orElse(null);
this.fallback = plains != null ? plains : (possibleBiomes.isEmpty() ? null : possibleBiomes.iterator().next());
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
return CODEC;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return possibleBiomes.stream();
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
int blockX = quartX * 4;
int blockZ = quartZ * 4;
int blockY = quartY * 4;
double latitudeTemp = blockZ / TEMPERATURE_SCALE;
double wobble = boundaryNoise.noise(blockX * BOUNDARY_NOISE_SCALE, 0.0, blockZ * BOUNDARY_NOISE_SCALE) * BOUNDARY_NOISE_AMPLITUDE;
double temperature = clamp(latitudeTemp + wobble, -1.0, 1.0);
BiomeBand band = BiomeBand.fromTemperature(temperature);
ResolvedBand pool = resolvedVanilla.get(band);
if (pool == null) {
pool = resolvedVanilla.get(BiomeBand.TEMPERATE);
}
// Guaranteed safe spawn zone (open, buildable biomes) around the origin.
if (spawnSafeBiomes != null && Math.abs(blockX) < SPAWN_SAFE_RADIUS && Math.abs(blockZ) < SPAWN_SAFE_RADIUS) {
return pickBiome(spawnSafeBiomes, blockX, blockZ, SPAWN_SELECTOR_SCALE);
}
// Underground (caves) layer.
if (blockY < UNDERGROUND_Y && !pool.underground().isEmpty()) {
return pickBiome(pool.underground(), blockX, blockZ, 0.004);
}
// Land vs ocean mask.
double oceanValue = oceanNoise.noise(blockX * OCEAN_NOISE_SCALE, 0.0, blockZ * OCEAN_NOISE_SCALE);
if (oceanValue > OCEAN_THRESHOLD && !pool.ocean().isEmpty()) {
return pickBiome(pool.ocean(), blockX, blockZ, 0.002);
}
// Rare wet pockets: high moisture carves a swamp (temperate) or mangrove swamp (warm/hot),
// matching vanilla where these are uncommon humid biomes rather than equal-weighted surface ones.
double moisture = moistureNoise.noise(blockX * MOISTURE_SCALE, 0.0, blockZ * MOISTURE_SCALE);
if (moisture > MOISTURE_THRESHOLD) {
if (band == BiomeBand.TEMPERATE && swampBiome != null) {
return swampBiome;
}
if ((band == BiomeBand.WARM || band == BiomeBand.HOT) && mangroveBiome != null) {
return mangroveBiome;
}
}
// Surface biome from the (mod-aware) climate tag, with a vanilla fallback.
List<Holder<Biome>> surface = resolveSurfaceTag(band);
if (surface == null || surface.isEmpty()) {
surface = pool.surface();
if (surface.isEmpty()) {
surface = resolvedVanilla.get(BiomeBand.TEMPERATE).surface();
}
}
return pickBiome(surface, blockX, blockZ, SURFACE_SELECTOR_SCALE);
}
/** Lazily resolve surface biomes from climate tags. Tags are bound after the registry freezes,
* so this is safe and never creates unbound holders. */
private List<Holder<Biome>> resolveSurfaceTag(BiomeBand band) {
EnumMap<BiomeBand, List<Holder<Biome>>> cache = surfaceFromTag;
if (cache != null) {
return cache.get(band);
}
synchronized (this) {
if (surfaceFromTag == null) {
EnumMap<BiomeBand, List<Holder<Biome>>> built = new EnumMap<>(BiomeBand.class);
for (BiomeBand b : BiomeBand.values()) {
List<Holder<Biome>> holders = new ArrayList<>();
biomeGetter.get(b.surfaceTag()).map(HolderSet::stream).ifPresent(stream -> stream.forEach(holders::add));
built.put(b, holders);
}
surfaceFromTag = built;
}
return surfaceFromTag.get(band);
}
}
private Holder<Biome> pickBiome(List<Holder<Biome>> biomes, int blockX, int blockZ, double scale) {
if (biomes == null || biomes.isEmpty()) {
return fallback;
}
// Dual-octave noise for a flatter, near-uniform index distribution: a single ImprovedNoise
// value is bell-curved around 0, which makes the middle of the biome list dominate. Adding a
// second octave at an offset flattens the curve so no biome is disproportionately common.
double n1 = selectorNoise.noise(blockX * scale, blockZ * scale, 1000.0);
double n2 = selectorNoise.noise(blockX * scale * 1.9 + 137.0, blockZ * scale * 1.9 - 211.0, 2000.0);
double combined = (n1 + n2 * 0.5) / 1.5;
double normalized = combined * 0.5 + 0.5;
int idx = (int) (normalized * biomes.size());
if (idx >= biomes.size()) idx = biomes.size() - 1;
if (idx < 0) idx = 0;
return biomes.get(idx);
}
private static double clamp(double value, double min, double max) {
return value < min ? min : (value > max ? max : value);
}
private record ResolvedBand(List<Holder<Biome>> surface, List<Holder<Biome>> ocean, List<Holder<Biome>> underground) {
}
}
@@ -0,0 +1,219 @@
package net.mcreator.customoregen.worldgen;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import net.mcreator.customoregen.CustomOreGenMod;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* {@code /latitude map [radius] [step]} — renders a top-down biome map of the latitude world.
*
* <p>Samples {@link LatitudeBiomeSource} on a large square grid (independent of the current
* world's biome source, so it works in any world) and writes:
* <ul>
* <li>{@code run/latitude/latitude_map.png} — one colour per biome category (the "map view"),</li>
* <li>{@code run/latitude/latitude_report.txt} — per-band distribution + validation invariants.</li>
* </ul>
*
* <p>Use this to validate the climate distribution without exploring by hand.</p>
*/
@EventBusSubscriber(modid = CustomOreGenMod.MODID, bus = EventBusSubscriber.Bus.GAME)
public class LatitudeMapCommand {
private static final int DEFAULT_RADIUS = 24000;
private static final int DEFAULT_STEP = 60;
// Biome key -> colour category (kept in sync with BiomeBand pools)
private static final Map<ResourceKey<Biome>, Integer> COLORS = new HashMap<>();
static {
col(0xB6E388, Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS, Biomes.FOREST, Biomes.BIRCH_FOREST,
Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE); // safe spawn / temperate
col(0xC8E6FF, Biomes.SNOWY_PLAINS, Biomes.ICE_SPIKES, Biomes.SNOWY_TAIGA, Biomes.GROVE,
Biomes.SNOWY_SLOPES, Biomes.JAGGED_PEAKS, Biomes.FROZEN_PEAKS); // frozen
col(0x8FB8D6, Biomes.TAIGA, Biomes.OLD_GROWTH_PINE_TAIGA, Biomes.OLD_GROWTH_SPRUCE_TAIGA,
Biomes.WINDSWEPT_HILLS, Biomes.WINDSWEPT_FOREST, Biomes.WINDSWEPT_GRAVELLY_HILLS); // cold
col(0x7CC576, Biomes.DARK_FOREST, Biomes.OLD_GROWTH_BIRCH_FOREST); // temperate
col(0xE0C068, Biomes.SAVANNA, Biomes.SAVANNA_PLATEAU, Biomes.WINDSWEPT_SAVANNA); // warm
col(0xE0884C, Biomes.DESERT, Biomes.BADLANDS, Biomes.WOODED_BADLANDS, Biomes.ERODED_BADLANDS,
Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE); // hot
col(0x2B5C8A, Biomes.OCEAN, Biomes.DEEP_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN,
Biomes.WARM_OCEAN, Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN,
Biomes.DEEP_COLD_OCEAN, Biomes.RIVER, Biomes.FROZEN_RIVER); // water
col(0x5B7A3A, Biomes.SWAMP); // swamp (should be rare pockets)
col(0x3A5A3A, Biomes.MANGROVE_SWAMP); // mangrove
col(0x6B4E3A, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK); // underground
}
private static void col(int rgb, ResourceKey<Biome>... keys) {
for (ResourceKey<Biome> k : keys) COLORS.put(k, rgb);
}
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
dispatcher.register(Commands.literal("latitude")
.requires(source -> source.hasPermission(2))
.then(Commands.literal("map")
.executes(ctx -> run(ctx, DEFAULT_RADIUS, DEFAULT_STEP))
.then(Commands.argument("radius", IntegerArgumentType.integer(1000, 100000))
.executes(ctx -> run(ctx, IntegerArgumentType.getInteger(ctx, "radius"), DEFAULT_STEP))
.then(Commands.argument("step", IntegerArgumentType.integer(4, 1000))
.executes(ctx -> run(ctx,
IntegerArgumentType.getInteger(ctx, "radius"),
IntegerArgumentType.getInteger(ctx, "step")))))));
}
private static int run(CommandContext<CommandSourceStack> ctx, int radius, int step) {
CommandSourceStack src = ctx.getSource();
if (!(src.getLevel() instanceof ServerLevel serverLevel)) {
src.sendFailure(Component.literal("Must be run on a server level."));
return 0;
}
long seed = serverLevel.getSeed();
HolderGetter<Biome> getter = serverLevel.registryAccess().lookupOrThrow(Registries.BIOME);
LatitudeBiomeSource source = new LatitudeBiomeSource(seed, getter);
int dim = (2 * radius) / step + 1;
src.sendSystemMessage(Component.literal(
"\u00A7b[Latitude]\u00A7r Sampling " + dim + "x" + dim + " grid (radius=" + radius + ", step=" + step + ")..."));
BufferedImage img = new BufferedImage(dim, dim, BufferedImage.TYPE_INT_RGB);
Map<ResourceKey<Biome>, Integer> global = new HashMap<>();
Map<BiomeBand, Map<ResourceKey<Biome>, Integer>> perBand = new EnumMap<>(BiomeBand.class);
for (BiomeBand b : BiomeBand.values()) perBand.put(b, new HashMap<>());
for (int zi = 0; zi < dim; zi++) {
for (int xi = 0; xi < dim; xi++) {
int blockX = -radius + xi * step;
int blockZ = -radius + zi * step;
Holder<Biome> holder = source.getNoiseBiome(blockX >> 2, 64 >> 2, blockZ >> 2, null);
ResourceKey<Biome> key = holder.unwrapKey().orElse(null);
global.merge(key, 1, Integer::sum);
perBand.get(BiomeBand.fromTemperature(blockZ / 16000.0)).merge(key, 1, Integer::sum);
int rgb = key == null ? 0x888888 : COLORS.getOrDefault(key, 0x888888);
img.setRGB(xi, dim - 1 - zi, rgb); // flip Z so north is at the top
}
}
// ---- Spawn biome ----
ResourceKey<Biome> spawnKey = source.getNoiseBiome(0, 64 >> 2, 0, null).unwrapKey().orElse(null);
// ---- Write PNG ----
try {
Files.createDirectories(Path.of("latitude"));
ImageIO.write(img, "png", Path.of("latitude", "latitude_map.png").toFile());
} catch (Exception e) {
CustomOreGenMod.LOGGER.error("Latitude map: failed to write PNG", e);
src.sendFailure(Component.literal("Failed to write PNG: " + e.getMessage()));
return 0;
}
// ---- Build report ----
StringBuilder r = new StringBuilder();
r.append("Latitude World Map Report\n");
r.append("seed=").append(seed).append(" radius=").append(radius).append(" step=").append(step)
.append(" grid=").append(dim).append("x").append(dim).append("\n\n");
r.append("Spawn biome @ (0,0): ").append(spawnKey).append("\n\n");
int total = global.values().stream().mapToInt(Integer::intValue).sum();
r.append("Global biome distribution (top 15):\n");
global.entrySet().stream()
.sorted(Map.Entry.<ResourceKey<Biome>, Integer>comparingByValue().reversed())
.limit(15)
.forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total)));
List<String> warnings = new ArrayList<>();
for (BiomeBand band : BiomeBand.values()) {
Map<ResourceKey<Biome>, Integer> m = perBand.get(band);
int bt = m.values().stream().mapToInt(Integer::intValue).sum();
if (bt == 0) continue;
int swamp = m.getOrDefault(Biomes.SWAMP, 0) + m.getOrDefault(Biomes.MANGROVE_SWAMP, 0);
double swampPct = 100.0 * swamp / bt;
r.append("\nBand ").append(band).append(" (").append(bt).append(" samples, swamp+mangrove ")
.append(String.format("%.2f%%", swampPct)).append("):\n");
m.entrySet().stream()
.sorted(Map.Entry.<ResourceKey<Biome>, Integer>comparingByValue().reversed())
.limit(6)
.forEach(e -> r.append(String.format(" %-43s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / bt)));
if (swampPct > 15.0) {
warnings.add(band + " has " + String.format("%.1f%%", swampPct) + " swamp (expected <15%)");
}
}
// Invariant checks
r.append("\n=== Validation ===\n");
boolean spawnOk = spawnKey != null && isSafeSpawn(spawnKey);
r.append("Spawn on safe biome: ").append(spawnOk ? "PASS" : "FAIL (" + spawnKey + ")").append("\n");
double frozenCold = bandRatio(perBand.get(BiomeBand.FROZEN), Set.of(
Biomes.SNOWY_PLAINS, Biomes.ICE_SPIKES, Biomes.SNOWY_TAIGA, Biomes.GROVE, Biomes.SNOWY_SLOPES,
Biomes.JAGGED_PEAKS, Biomes.FROZEN_PEAKS, Biomes.TAIGA, Biomes.OLD_GROWTH_PINE_TAIGA,
Biomes.OLD_GROWTH_SPRUCE_TAIGA, Biomes.WINDSWEPT_HILLS, Biomes.WINDSWEPT_FOREST,
Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN,
Biomes.FROZEN_RIVER));
r.append(String.format("North (FROZEN) cold/frozen ratio: %.1f%% (want >60%%)%n", frozenCold));
double hotRatio = bandRatio(perBand.get(BiomeBand.HOT), Set.of(
Biomes.DESERT, Biomes.BADLANDS, Biomes.WOODED_BADLANDS, Biomes.ERODED_BADLANDS,
Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE, Biomes.SAVANNA,
Biomes.SAVANNA_PLATEAU, Biomes.WARM_OCEAN));
r.append(String.format("South (HOT) warm/hot ratio: %.1f%% (want >60%%)%n", hotRatio));
r.append("Warnings: ").append(warnings.isEmpty() ? "none" : warnings).append("\n");
try {
Files.writeString(Path.of("latitude", "latitude_report.txt"), r.toString());
} catch (Exception e) {
CustomOreGenMod.LOGGER.error("Latitude map: failed to write report", e);
}
// ---- Summary to chat ----
src.sendSystemMessage(Component.literal("\u00A7a[Latitude]\u00A7r Map + report saved to /latitude/"));
src.sendSystemMessage(Component.literal(" Spawn: " + spawnKey + (spawnOk ? " \u00A7aOK\u00A7r" : " \u00A7cBAD\u00A7r")));
src.sendSystemMessage(Component.literal(String.format(
" North cold/frozen: %.0f%% | South hot: %.0f%% | Swamp warnings: %d",
frozenCold, hotRatio, warnings.size())));
return 1;
}
private static double bandRatio(Map<ResourceKey<Biome>, Integer> m, Set<ResourceKey<Biome>> expected) {
int total = m.values().stream().mapToInt(Integer::intValue).sum();
if (total == 0) return 0;
int match = 0;
for (ResourceKey<Biome> k : expected) match += m.getOrDefault(k, 0);
return 100.0 * match / total;
}
private static boolean isSafeSpawn(ResourceKey<Biome> key) {
Set<ResourceKey<Biome>> safe = new HashSet<>(List.of(
Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS, Biomes.FOREST, Biomes.BIRCH_FOREST,
Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE));
return safe.contains(key);
}
}
@@ -0,0 +1,108 @@
package net.mcreator.customoregen.worldgen;
import net.mcreator.customoregen.CustomOreGenMod;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.storage.ServerLevelData;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.level.LevelEvent;
import java.util.Set;
/**
* Forces the world spawn point to a plains-like biome near the origin.
*
* <p>Because {@link LatitudeBiomeSource} places temperate biomes around Z=0, a plains
* or forest biome is always available close to spawn. This handler runs once per server
* lifetime when the overworld loads, scans a growing square around the origin and pins
* the default spawn point to the first matching biome. The player therefore always
* starts the game on open, buildable terrain instead of a swamp, mountain or ocean.</p>
*/
@EventBusSubscriber(modid = CustomOreGenMod.MODID)
public class LatitudeSpawnHandler {
/** Biomes considered a good starting point (open, buildable temperate terrain). */
private static final Set<ResourceKey<Biome>> SPAWN_BIOMES = Set.of(
Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS,
Biomes.FOREST, Biomes.BIRCH_FOREST, Biomes.FLOWER_FOREST,
Biomes.MEADOW, Biomes.CHERRY_GROVE
);
/** Maximum search radius (in blocks) around the origin. */
private static final int MAX_RADIUS = 512;
/** Step between sampled positions. */
private static final int STEP = 8;
private static volatile boolean initialized = false;
@SubscribeEvent
public static void onLevelLoad(LevelEvent.Load event) {
if (initialized) {
return;
}
if (!(event.getLevel() instanceof ServerLevel level)) {
return;
}
if (level.dimension() != Level.OVERWORLD) {
return;
}
initialized = true;
if (!(level.getLevelData() instanceof ServerLevelData levelData)) {
return;
}
BlockPos current = levelData.getSpawnPos();
if (isSpawnBiome(level, current)) {
return; // Already on a good biome, leave it alone.
}
BlockPos found = findSpawnBiome(level);
if (found == null) {
CustomOreGenMod.LOGGER.warn("Latitude: no suitable spawn biome found within {} blocks, keeping {}", MAX_RADIUS, current);
return;
}
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, found.getX(), found.getZ());
BlockPos spawn = new BlockPos(found.getX(), y, found.getZ());
level.setDefaultSpawnPos(spawn, 0.0f);
CustomOreGenMod.LOGGER.info("Latitude: spawn point set to {} ({})", spawn,
level.getBiome(spawn).unwrapKey().map(Object::toString).orElse("?"));
}
private static boolean isSpawnBiome(ServerLevel level, BlockPos pos) {
Holder<Biome> biome = level.getBiome(pos);
return biome.unwrapKey().map(SPAWN_BIOMES::contains).orElse(false);
}
private static BlockPos findSpawnBiome(ServerLevel level) {
// Concentric square rings starting from the origin.
for (int radius = 0; radius <= MAX_RADIUS; radius += STEP) {
for (int x = -radius; x <= radius; x += STEP) {
for (int z = -radius; z <= radius; z += STEP) {
// Only scan the outer ring of each pass to avoid re-checking inner cells.
if (Math.abs(x) != radius && Math.abs(z) != radius && radius != 0) {
continue;
}
BlockPos pos = new BlockPos(x, 0, z);
if (isSpawnBiome(level, pos)) {
return pos;
}
}
}
}
return null;
}
}
@@ -0,0 +1,22 @@
package net.mcreator.customoregen.worldgen;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.biome.BiomeSource;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
/**
* Registration of the custom latitude {@link BiomeSource} codec.
*
* <p>The biome source is referenced from world presets as
* {@code "custom_ore_gen:latitude"}.</p>
*/
public class WorldGenRegistration {
public static final DeferredRegister<MapCodec<? extends BiomeSource>> BIOME_SOURCES =
DeferredRegister.create(Registries.BIOME_SOURCE, "custom_ore_gen");
public static final DeferredHolder<MapCodec<? extends BiomeSource>, MapCodec<? extends BiomeSource>> LATITUDE =
BIOME_SOURCES.register("latitude", () -> LatitudeBiomeSource.CODEC);
}
@@ -27,5 +27,9 @@
"item.custom_ore_gen.sharddiamondleggings": "Shard Diamond Leggings",
"item.custom_ore_gen.sharddiamondboots": "Shard Diamond Boots",
"item.custom_ore_gen.sharddiamondpaxel": "Shard Diamond Paxel",
"itemGroup.custom_ore_gen": "Custom Ore Gen"
"itemGroup.custom_ore_gen": "Custom Ore Gen",
"generator.custom_ore_gen.ultra_wide_biome": "Ultra Wide Biome",
"generator.custom_ore_gen.ultra_wide_biome.info": "Climate bands by latitude: frozen north, temperate equator, hot south. Extremely large biomes.",
"generator.custom_ore_gen.tectonic_ultra_wide_biome": "Tectonic Ultra Wide Biome",
"generator.custom_ore_gen.tectonic_ultra_wide_biome.info": "Latitude climate bands with Tectonic terrain (requires Tectonic)."
}
@@ -27,5 +27,9 @@
"item.custom_ore_gen.sharddiamondleggings": "Jambières en éclat de diamant",
"item.custom_ore_gen.sharddiamondboots": "Bottes en éclat de diamant",
"item.custom_ore_gen.sharddiamondpaxel": "Paxel en éclat de diamant",
"itemGroup.custom_ore_gen": "Custom Ore Gen"
"itemGroup.custom_ore_gen": "Custom Ore Gen",
"generator.custom_ore_gen.ultra_wide_biome": "Biomes Ultra-Larges",
"generator.custom_ore_gen.ultra_wide_biome.info": "Bandes climatiques par latitude : nord gelé, équateur tempéré, sud chaud. Biomes immenses.",
"generator.custom_ore_gen.tectonic_ultra_wide_biome": "Biomes Ultra-Larges (Tectonic)",
"generator.custom_ore_gen.tectonic_ultra_wide_biome.info": "Bandes climatiques par latitude avec terrain Tectonic (nécessite Tectonic)."
}
@@ -0,0 +1,80 @@
{
"replace": false,
"values": [
"minecraft:snowy_plains",
"minecraft:ice_spikes",
"minecraft:snowy_taiga",
"minecraft:taiga",
"minecraft:old_growth_pine_taiga",
"minecraft:old_growth_spruce_taiga",
"minecraft:windswept_hills",
"minecraft:windswept_forest",
"minecraft:windswept_gravelly_hills",
"minecraft:grove",
"minecraft:snowy_slopes",
{
"id": "biomesoplenty:snowblossom_grove",
"required": false
},
{
"id": "biomesoplenty:snowy_coniferous_forest",
"required": false
},
{
"id": "biomesoplenty:snowy_maple_woods",
"required": false
},
{
"id": "biomesoplenty:snowy_fir_clearing",
"required": false
},
{
"id": "biomesoplenty:auroral_garden",
"required": false
},
{
"id": "biomesoplenty:cold_desert",
"required": false
},
{
"id": "biomesoplenty:tundra",
"required": false
},
{
"id": "biomesoplenty:muskeg",
"required": false
},
{
"id": "biomesoplenty:maple_woods",
"required": false
},
{
"id": "biomesoplenty:dead_forest",
"required": false
},
{
"id": "biomesoplenty:old_growth_dead_forest",
"required": false
},
{
"id": "biomesoplenty:bog",
"required": false
},
{
"id": "biomesoplenty:coniferous_forest",
"required": false
},
{
"id": "biomesoplenty:fir_clearing",
"required": false
},
{
"id": "biomesoplenty:seasonal_forest",
"required": false
},
{
"id": "biomesoplenty:seasonal_orchard",
"required": false
}
]
}
@@ -0,0 +1,60 @@
{
"replace": false,
"values": [
"minecraft:desert",
"minecraft:badlands",
"minecraft:wooded_badlands",
"minecraft:eroded_badlands",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:windswept_savanna",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:birch_forest",
{
"id": "biomesoplenty:dryland",
"required": false
},
{
"id": "biomesoplenty:dune_beach",
"required": false
},
{
"id": "biomesoplenty:bayou",
"required": false
},
{
"id": "biomesoplenty:fungal_jungle",
"required": false
},
{
"id": "biomesoplenty:rainforest",
"required": false
},
{
"id": "biomesoplenty:rocky_rainforest",
"required": false
},
{
"id": "biomesoplenty:floodplain",
"required": false
},
{
"id": "biomesoplenty:tropics",
"required": false
},
{
"id": "biomesoplenty:scrubland",
"required": false
},
{
"id": "biomesoplenty:rocky_shrubland",
"required": false
},
{
"id": "biomesoplenty:jade_cliffs",
"required": false
}
]
}
@@ -0,0 +1,99 @@
{
"replace": false,
"values": [
"minecraft:plains",
"minecraft:sunflower_plains",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:old_growth_birch_forest",
"minecraft:dark_forest",
"minecraft:flower_forest",
"minecraft:meadow",
"minecraft:cherry_grove",
"minecraft:windswept_forest",
{
"id": "biomesoplenty:field",
"required": false
},
{
"id": "biomesoplenty:forested_field",
"required": false
},
{
"id": "biomesoplenty:pumpkin_patch",
"required": false
},
{
"id": "biomesoplenty:grassland",
"required": false
},
{
"id": "biomesoplenty:highland",
"required": false
},
{
"id": "biomesoplenty:moor",
"required": false
},
{
"id": "biomesoplenty:shrubland",
"required": false
},
{
"id": "biomesoplenty:wetland",
"required": false
},
{
"id": "biomesoplenty:clover_patch",
"required": false
},
{
"id": "biomesoplenty:redwood_forest",
"required": false
},
{
"id": "biomesoplenty:woodland",
"required": false
},
{
"id": "biomesoplenty:old_growth_woodland",
"required": false
},
{
"id": "biomesoplenty:marsh",
"required": false
},
{
"id": "biomesoplenty:meadow",
"required": false
},
{
"id": "biomesoplenty:orchard",
"required": false
},
{
"id": "biomesoplenty:mediterranean_forest",
"required": false
},
{
"id": "biomesoplenty:lavender_field",
"required": false
},
{
"id": "biomesoplenty:lavender_forest",
"required": false
},
{
"id": "biomesoplenty:mystic_grove",
"required": false
},
{
"id": "biomesoplenty:cherry_blossom_grove",
"required": false
},
{
"id": "biomesoplenty:origin_valley",
"required": false
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "minecraft:overworld",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "custom_ore_gen:latitude",
"seed": 0
},
"settings": "minecraft:overworld"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
}
}
}
@@ -0,0 +1,36 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "minecraft:overworld",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "custom_ore_gen:latitude",
"seed": 0
},
"settings": "custom_ore_gen:overworld"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
}
}
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"custom_ore_gen:ultra_wide_biome",
"custom_ore_gen:tectonic_ultra_wide_biome"
]
}