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:
@@ -29,6 +29,7 @@ import com.mojang.serialization.MapCodec;
|
|||||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||||
import net.neoforged.neoforge.registries.NeoForgeRegistries;
|
import net.neoforged.neoforge.registries.NeoForgeRegistries;
|
||||||
import net.mcreator.customoregen.loot.CustomOreLootModifier;
|
import net.mcreator.customoregen.loot.CustomOreLootModifier;
|
||||||
|
import net.mcreator.customoregen.worldgen.WorldGenRegistration;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -58,6 +59,7 @@ public class CustomOreGenMod {
|
|||||||
LOOT_MODIFIERS.register(modEventBus);
|
LOOT_MODIFIERS.register(modEventBus);
|
||||||
|
|
||||||
// Start of user code block mod init
|
// Start of user code block mod init
|
||||||
|
WorldGenRegistration.BIOME_SOURCES.register(modEventBus);
|
||||||
// End of user code block mod init
|
// 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 ≈ 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.sharddiamondleggings": "Shard Diamond Leggings",
|
||||||
"item.custom_ore_gen.sharddiamondboots": "Shard Diamond Boots",
|
"item.custom_ore_gen.sharddiamondboots": "Shard Diamond Boots",
|
||||||
"item.custom_ore_gen.sharddiamondpaxel": "Shard Diamond Paxel",
|
"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.sharddiamondleggings": "Jambières en éclat de diamant",
|
||||||
"item.custom_ore_gen.sharddiamondboots": "Bottes en éclat de diamant",
|
"item.custom_ore_gen.sharddiamondboots": "Bottes en éclat de diamant",
|
||||||
"item.custom_ore_gen.sharddiamondpaxel": "Paxel 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+99
@@ -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
+36
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user