feat(worldgen): v4.1 - latitude zones via density function + Lithosphere

- Replace custom LatitudeBiomeSource/BiomeBand system with
  LatitudeSignalDensityFunction + LatitudeZonePlacement (mixin-based)
- Add mandatory Lithosphere (mr_lithosphere) dependency + mixin config
- Override overworld/large_biomes noise_settings for ultra-wide terrain
- Rename remove_vanilla_ores -> z_remove_vanilla_ores (load order)
- Merge cold/hot/tempered biome tags into latitude_*_surface tags
- Rework OresCommand, OreBiomeFinderItem, ModConfigs for new system
- Remove obsolete latitude classes, tests & world preset overrides
- Bump mod_version 3.2 -> 4.1
This commit is contained in:
feldenr
2026-07-20 23:11:16 +02:00
parent e667083853
commit e179baaf25
71 changed files with 6061 additions and 2832 deletions
+85
View File
@@ -0,0 +1,85 @@
# Custom Ore Gen — 4.0 Changelog
**Custom Ore Gen 4.0** is a full rewrite, rebuilt from the ground up for **NeoForge 1.21.1**. It drops KubeJS entirely, introduces a brand-new **latitude-based ore system**, ships custom **ultra-wide terrain** via the Lithosphere companion, and turns the **Deep Dark into a legendary northern feature**.
This is the biggest update in the mod's history. Existing worlds from 2.x are **not** compatible — create a new world.
---
## 🆕 New Features
* **Latitude-based ore generation** — Ores are now distributed by your **Z coordinate** instead of biome categories. The world is split into three bands:
* ❄️ **Cold** (Z < 8000, north): Lapis, Deepslate Diamond
* 🔥 **Hot** (Z > 8000, south): Pure Gold, Redstone, Copper
* 🌳 **Temperate** (middle): Iron, Concentrated Coal
* Works with **any biome mod** out of the box — no per-biome classification needed.
* **Legendary Deep Dark** — The Deep Dark now only generates in cold northern latitudes, making it a true endgame destination rather than a random underground encounter. (Implemented via a safe, targeted mixin.)
* **Custom ultra-wide terrain** — Ships a handcrafted world generation through the new **Lithosphere** companion mod (now a required dependency).
* **Config-gated feature toggles** — A custom `ConfigGatedFeaturesModifier` makes the config feature toggles (`concentratedOres`, `impureOres`, `vanillaOreVariants`, etc.) actually gate world generation at runtime.
* **Global Loot Modifier for drops** — Replaced fragile event-based drops with a proper data-driven loot modifier (also fixed a crash with the Create drill).
* **Color-coded `/ores` command & Ore Biome Finder** — Output is now color-coded per zone (cold = aqua, hot = gold, temperate = green, shard ores = light purple) and clearly explains that the **Z coordinate** determines your zone.
* **Configurable zone thresholds** — The cold/hot thresholds (default Z = ±8000) are now adjustable in `custom-ore-gen-common.toml`.
* **Create & Mekanism compat** — Standalone-safe recipes for crushing, milling, mixing and enriching shard diamonds.
---
## ⚙️ Changes
* **Migrated from Forge 1.20.1 → NeoForge 1.21.1.**
* **Removed KubeJS dependency entirely.** Vanilla ores are now removed via NeoForge **biome modifiers** (`z_remove_vanilla_ores.json`) — no startup scripts, nothing to run.
* **New required dependency: Lithosphere 1.7+** — provides the custom terrain the mod is balanced for.
* Tool & armor stats are now **linked to config** — durability, speed, damage and armor values can all be tuned.
* Emeralds remain **biome-based** (mountain and rare-biome tags), since they're tied to terrain, not latitude.
* Rebalanced ore drop counts and XP to align with vanilla 1.21.
* Iron and Gold ores now **ignore Fortune** like vanilla.
* Paxel now affects every block mineable by pickaxe/shovel/axe tags.
* Rebalanced Diamond Shard tier as a clear bridge between iron and diamond.
---
## 🐛 Bug Fixes
* **Vanilla diamond leaked everywhere** — All **4** vanilla diamond placed features are now removed (`ore_diamond`, `ore_diamond_buried`, `ore_diamond_large`, `ore_diamond_medium`). Previously only 2 were removed, so buried/medium diamonds spawned in every biome.
* **Copper showed the wrong texture in deepslate** — Custom copper now uses the vanilla two-target approach (stone → `copper_ore`, deepslate → `deepslate_copper_ore`) instead of forcing `copper_ore` everywhere.
* **`/ores` always reported temperate** — `zoneName()` was reading static fields instead of the config getters; it now reads the live thresholds.
* **Garbage `6a9` prefix in ore listings** — Removed a broken color-code remnant that appeared in front of every ore name.
* **Diamond rate was way too high in cold zones** — Reduced the custom Deepslate Diamond `count` from 6 → 3 (combined with the vanilla leak fix, diamonds are now properly rare).
* **Server hung on startup** — Removed the `SpawnRelocator` and `OreAuditHandler` entirely. They forced synchronous chunk generation on the main server thread (a known cause of deadlocks and world corruption) and the audit handler shipped a `System.exit(0)` hard-kill. Both were dev-only diagnostics that never belonged in a release.
* **Badlands gold leaked into non-hot zones** — Added `ore_gold_extra` to the vanilla removal list.
* **Cross-climate tag leaks** — Removed copper/gold appearing in temperate spawns due to overlapping surface tags.
* **Caves piercing the surface** — Reverted the stretched terrain that caused cave openings to break the surface.
* **Create processing recipes** — Repaired and made them standalone-safe (no leftover references to removed ores).
* Various startup crashes, build errors and config-load race conditions resolved.
---
## 🗑️ Removals
* **KubeJS** — no longer used or required.
* Auto-generated `kubejs/startup_scripts/custom_ore_gen_remove_vanilla_ores.js` — replaced by biome modifiers.
* Dead config categories, unused `EnchantabilityFix` class, and redundant `tectonic`/custom noise settings.
* Legacy biome-tag surface files (`latitude_cold_surface`, `latitude_hot_surface`, `latitude_temperate_surface`, `cold_biomes`, `hot_biomes`, `tempered_biomes`, etc.) — superseded by the Z-based system.
---
## 🧪 Technical / Internal
* Mixed-in `MultiNoiseBiomeSource` to intercept Deep Dark biome resolution safely (Mojmap, no refmap needed on production).
* Custom `LatitudeZonePlacement` placement modifier filters ore placement by Z at the placed-feature level.
* Comprehensive test suite added: `OresCommandTest`, `ModConfigsTest`, `ConfigurableOreDropsProcedureTest`, `OreDropMathTest`, plus an automated GameTest server for latitude validation.
* Cleaned up block/item classes and migrated the entire test suite to NeoForge.
---
## 📦 Requirements
* Minecraft 1.21.1
* NeoForge 21.1.x
* **Lithosphere 1.7+** (required)
* Biomes O' Plenty, Create, Mekanism (optional, recommended)
---
> ⚠️ **Create a NEW world after updating.** Chunks generated before 4.0 keep their old ore distribution.
**Happy mining!** 🌍⛏️✨
+30
View File
@@ -0,0 +1,30 @@
# Custom Ore Gen — 4.1
A hotfix release that removes developer-only diagnostics that could destabilise dedicated servers. **No gameplay changes** — all ore generation, the Diamond Shard tier, the Deep Dark lock and mod compatibility are identical to 4.0.
---
## 🐛 Bug Fixes
* **Removed world/server-corrupting code** — Three developer-only diagnostic classes were accidentally shipped in 4.0. They have been removed entirely:
* `OreAuditHandler` — contained a `System.exit(0)` hard-kill that would terminate the server process, and forced chunk generation from a separate thread (a classic cause of chunk corruption).
* `SpawnRelocator` — forced synchronous chunk generation on the main server thread during startup, causing deadlocks and hang.
* `GenerationStabilityTest` — a GameTest utility that had no place in a release build.
* None of these classes were referenced by any gameplay code; removing them has **zero impact** on how the mod plays.
---
## 🔄 Changes
* Spawn-point behaviour reverts to vanilla (the removed `SpawnRelocator` no longer moves the spawn). Since the temperate band is 16,000 blocks wide, the vast majority of spawns still land in a balanced ore zone.
---
## ✅ Verification
* No `System.exit`, `Runtime.halt`, forced chunk loading (`getChunkAt`/`getChunk`), manual threads or `setBlock` calls remain anywhere in the codebase.
* Build and unit tests pass.
---
**Full build:** Minecraft 1.21.1 · NeoForge 21.1.x · Lithosphere 1.7+ (required)
+176
View File
@@ -0,0 +1,176 @@
# 🔧 Custom Ore Gen
**Custom Ore Gen** completely overhauls Minecraft's ore generation by replacing vanilla distribution with a **latitude-based system** driven by the world's Z coordinate, and adds a complete intermediate equipment tier between iron and diamond: **Diamond Shards**.
After months of reworking, **version 4.0** is a full rewrite — rebuilt from the ground up for **NeoForge 1.21.1**, dropping KubeJS entirely in favor of fully data-driven biome modifiers, and introducing a brand-new world of latitude exploration.
***
## 🎯 Overview
A mod that:
* Replaces **all** vanilla overworld ore generation with a **latitude system** (north = cold, south = hot, middle = temperate)
* Adds a new intermediate equipment tier: **Diamond Shards**
* Encourages **north/south exploration** to gather specific resources
* Locks the **Deep Dark** to cold northern latitudes as a legendary feature
* The temperate band is **16,000 blocks wide** (Z = 8000 to +8000), so most spawns already land in a balanced area
* Ships custom **ultra-wide terrain** through its **Lithosphere** companion mod
* Automatically integrates with **Biomes O' Plenty**, **Create** and **Mekanism**
***
## 🌍 Latitude-Based Ore Generation
The world is split into three latitude bands running along the **Z axis**. Travel north or south to change which ores you can find — *your Z coordinate, not your biome, decides the ores* (Emeralds being the biome-tagged exception).
| Zone | Direction | Ores |
| --- | --- | --- |
| ❄️ **Cold** | North (Z < -8000) | Lapis, Deepslate Diamond |
| 🔥 **Hot** | South (Z > 8000) | Pure Gold, Redstone, Copper |
| 🌳 **Temperate** | Middle (-8000 → 8000) | Iron, Concentrated Coal |
| ⛰️ **Mountain biomes** | Anywhere (by biome tag) | High Emerald |
| ✨ **Rare biomes** | Anywhere (by biome tag) | Lower Emerald |
| 🔷 **All zones** | Everywhere | Deepslate Shard Diamond, Shard Diamond Block |
> ⚠️ **Note**: _Deepslate Shard Diamond Ore_ is the **only progression ore that spawns in ALL zones**, ensuring accessible early-game progression wherever you start. The temperate band is huge (16,000 blocks), so you almost always start in a balanced ore zone.
🧭 Use the **Ore Biome Finder** item or the **`/ores`** command to see exactly which ores are available at your current Z coordinate — the output is **color-coded per zone** (cold = aqua, hot = gold, temperate = green, shard ores = light purple).
***
## 💎 Diamond Shard Tier — New Progression
A complete set of tools and armor that bridges the gap between iron and diamond.
### ⚙️ **Available Equipment**
| Type | Components | Durability |
| --- | --- | --- |
| **Tools** (Pickaxe, Axe, Shovel) | Diamond Shards | 450 |
| **Paxel** (3-in-1 tool) | Diamond Shards | 800 |
| **Full Armor Set** | Diamond Shards + 2 Diamonds | 17 protection, Toughness 1.0 |
> ✅ **The Paxel** combines the functions of **pickaxe, axe, and shovel** into a single tool — affecting every block mineable by those tags.
> 🛠️ All stats above are the **defaults** — every durability, speed and damage value, plus the cold/hot zone thresholds and ore drops, is configurable in `custom-ore-gen-common.toml`.
### 🧪 **Shard → Diamond Conversion**
9 Diamond Shards → 1 Diamond (3×3 crafting grid).
### 🛡️ **Armor Recipes**
* **Helmet**: 5 Diamond Shards
* **Chestplate**: 8 Diamond Shards + 1 Diamond
* **Leggings**: 7 Diamond Shards + 1 Diamond
* **Boots**: 4 Diamond Shards
> ⚙️ **Repair** any Diamond Shard equipment with Diamond Shards on an anvil.
***
## 🔍 Ore Biome Finder
A handy tool to instantly know which ores are available at your current latitude.
🎮 **Alternatives**:
* Craft the **Ore Biome Finder** item (check JEI for the recipe).
* Use the **`/ores`** (or `/ore`) command — it shows your X/Z position, the zone determined by Z, the cold/hot thresholds, and a **color-coded** list of findable ores.
***
## 📦 Installation & Dependencies
### ⚠️ **Mandatory Requirements**
* **Minecraft**: 1.21.1
* **NeoForge**: 21.1.x
* **Lithosphere**: 1.7+ _(required — provides the custom ultra-wide terrain the mod is balanced for)_
### 🌐 **Optional but Recommended**
* **Biomes O' Plenty** — ore generation automatically extends to all its biomes.
* **Create** — ore-processing compat (crushing/milling shard diamond ore, mixing shards → diamond).
* **Mekanism** — enriching compat for shard diamonds.
> 🚫 The mod does **not** use KubeJS anymore. Vanilla ores are fully removed via NeoForge **biome modifiers** — no scripts, nothing to run.
***
## 🎮 Gameplay & Progression
1. **Initial Stage**
Start in the temperate band and mine **Deepslate Shard Diamond Ore** (available everywhere) to obtain **Diamond Shards**.
2. **First Equipment**
Craft Diamond Shard tools (or the versatile Paxel) for better efficiency than iron.
3. **Targeted Exploration**
Travel north or south along the Z axis to reach different ore zones:
* **North (Cold)**: Lapis and Deepslate Diamonds
* **South (Hot)**: Pure Gold, Redstone, Copper
* **Mountain biomes**: High Emeralds
* **Rare biomes**: Lower Emeralds
4. **Final Progression**
Combine 9 Diamond Shards to create a regular Diamond and access vanilla diamond equipment.
***
## 📊 Equipment Statistics Comparison
| Material | Protection | Chestplate Durability |
| --- | --- | --- |
| Iron | 15 | 240 |
| **Diamond Shard** | **17** | **300** |
| Diamond | 20 | 528 |
***
## 🌟 Key Features
* 🗺️ **Latitude Exploration**: the Z coordinate — not the biome — decides your ores
* 🌌 **Legendary Deep Dark**: locked to cold northern latitudes only (via a safe mixin)
* 🚀 **Custom Ultra-Wide Terrain**: ships with Lithosphere for a fresher, wider world
* ⚔️ **Balanced Progression**: Diamond Shard tier bridges the irondiamond gap
* 🚫 **No Vanilla Ores**: fully removed via data-driven biome modifiers (no scripts)
* 🌍 **Modpack Friendly**: automatic integration with any biome mod through the Z system
* 🎨 **Dedicated Creative Tab**: all mod items in the "Custom Ore Gen" tab
* 🛠️ **Versatile Tools**: the Paxel saves inventory space
* 📊 **In-game Info**: Finder item + `/ores` command with color-coded zones
* ⚙️ **Fully Configurable**: zone thresholds, tool stats, armor values and ore drops
***
## 🆕 What's New in 4.0
Version 4.0 is a ground-up rebuild. Highlights:
* **Full NeoForge 1.21.1 port** — left Forge 1.20.1 and KubeJS behind; everything is now data-driven biome modifiers.
* **Latitude ore system** — ores are now distributed by **Z coordinate** instead of biome categories, so it works with *any* biome mod out of the box.
* **Ultra-wide custom terrain** — bundled through the **Lithosphere** companion mod.
* **Legendary Deep Dark** — the Deep Dark now only generates in cold northern latitudes, making it a true endgame destination.
* **Create & Mekanism compat** — recipes for crushing, milling, mixing and enriching shard diamonds.
* **Color-coded `/ores` & Ore Finder** — clear, readable output that explains the Z mechanic.
* **Polished ore behaviour** — all 4 vanilla diamond features are properly removed, and copper now uses the correct deepslate texture underground.
> 💡 Existing worlds from older versions are **not** compatible — create a **new world** after installing 4.0.
***
## 📝 Important Notes
* ⚠️ **Create a NEW world** after installing. Chunks generated before installation keep their old ore distribution.
* The cold/hot zone thresholds default to **Z = ±8000** but are fully adjustable in the config.
* Emeralds remain **biome-based** (mountain and rare-biome tags), not latitude-based.
* All Diamond Shard equipment can be repaired with Diamond Shards.
***
### 🎯 **Why Choose Custom Ore Gen?**
Because it brings **logic to exploration** (travel the world to find specific ores), **smoother progression** (a real iron-to-diamond bridge), and **seamless integration** with many modded biomes — all without tedious configuration.
> **Happy mining!** 🌍⛏️✨
+1 -1
View File
@@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx4G
org.gradle.daemon=true org.gradle.daemon=true
# Mod Properties # Mod Properties
mod_version=3.2 mod_version=4.1
mod_id=custom_ore_gen mod_id=custom_ore_gen
mod_name=Custom Ore Gen mod_name=Custom Ore Gen
mod_group_id=com.aulyrius.custom_ore_gen mod_group_id=com.aulyrius.custom_ore_gen
@@ -59,8 +59,9 @@ 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);
WorldGenRegistration.BIOME_MODIFIER_SERIALIZERS.register(modEventBus); WorldGenRegistration.BIOME_MODIFIER_SERIALIZERS.register(modEventBus);
WorldGenRegistration.DENSITY_FUNCTION_TYPES.register(modEventBus);
WorldGenRegistration.PLACEMENT_MODIFIERS.register(modEventBus);
// End of user code block mod init // End of user code block mod init
} }
@@ -2,153 +2,104 @@ package net.mcreator.customoregen;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.biome.Biome;
import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.EventBusSubscriber;
import java.util.*; import java.util.*;
import net.mcreator.customoregen.worldgen.LatitudeConfig;
@EventBusSubscriber(modid = CustomOreGenMod.MODID, bus = EventBusSubscriber.Bus.GAME) @EventBusSubscriber(modid = CustomOreGenMod.MODID, bus = EventBusSubscriber.Bus.GAME)
public class OresCommand { public class OresCommand {
// Tags personnalisés du mod
private static final TagKey<Biome> COLD_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "cold_biomes"));
private static final TagKey<Biome> HOT_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "hot_biomes"));
private static final TagKey<Biome> MOUNTAIN_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "mountain_biomes"));
private static final TagKey<Biome> TEMPERED_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "tempered_biomes"));
private static final TagKey<Biome> RARE_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "rare_biomes"));
// Minerais par catégorie (mappé avec les biome modifiers)
private static final List<String> COLD_ORES = Arrays.asList( private static final List<String> COLD_ORES = Arrays.asList(
"Lapis (stone) [Y: 0 à 32]", "Lapis (stone) [Y: 0 a 32]",
"Lapis (deepslate) [Y: -64 à 0]", "Lapis (deepslate) [Y: -64 a 0]",
"Diamant (deepslate) [Y: -64 à 0]"); "Diamant (deepslate) [Y: -64 a 0]");
private static final List<String> HOT_ORES = Arrays.asList( private static final List<String> HOT_ORES = Arrays.asList(
"Or Pur (stone) [Y: 0 à 256]", "Or (stone) [Y: 0 a 320]",
"Or Pur (deepslate) [Y: -64 à 0]", "Or (deepslate) [Y: -64 a 0]",
"Redstone (stone) [Y: -10 à 20]", "Cuivre (haut) [Y: 15 a 320]",
"Redstone (deepslate) [Y: -80 à -30]", "Cuivre (bas) [Y: -64 a 0]",
"Cuivre (haut) [Y: 15 à 256]", "Redstone (stone) [Y: -10 a 20]",
"Cuivre (bas) [Y: -64 à 0]"); "Redstone (deepslate) [Y: -80 a -30]");
private static final List<String> MOUNTAIN_ORES = Arrays.asList( private static final List<String> TEMPERATE_ORES = Arrays.asList(
"Emeraude (haute altitude) [Y: 0 à 64]"); "Fer (stone) [Y: 0 a 100]",
"Fer (deepslate) [Y: -64 a 0]",
private static final List<String> TEMPERED_ORES = Arrays.asList( "Charbon concentre [Y: 0 a 70]");
"Charbon Concentre [Y: 0 à 70]",
"Fer (stone) [Y: 0 à 100]",
"Fer (deepslate) [Y: -64 à 0]");
private static final List<String> RARE_ORES = Arrays.asList(
"Emeraude (basse altitude) [Y: -64 à 0]");
private static final List<String> EVERYWHERE_ORES = Arrays.asList( private static final List<String> EVERYWHERE_ORES = Arrays.asList(
"Diamant Shard (deepslate) [Y: -64 à -40]", "Diamant Shard (deepslate) [Y: -64 a -40]",
"Bloc Diamant Shard [Y: 0 à 15]"); "Bloc Diamant Shard [Y: 0 a 15]");
@SubscribeEvent @SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) { public static void onRegisterCommands(RegisterCommandsEvent event) {
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher(); event.getDispatcher().register(Commands.literal("ores")
// Niveau de permission 2 = OP (Admin level)
dispatcher.register(Commands.literal("ores")
.requires(source -> source.hasPermission(2)) .requires(source -> source.hasPermission(2))
.executes(OresCommand::executeOres)); .executes(OresCommand::executeOres));
event.getDispatcher().register(Commands.literal("ore")
dispatcher.register(Commands.literal("ore")
.requires(source -> source.hasPermission(2)) .requires(source -> source.hasPermission(2))
.executes(OresCommand::executeOres)); .executes(OresCommand::executeOres));
} }
private static int executeOres(CommandContext<CommandSourceStack> context) { private static int executeOres(CommandContext<CommandSourceStack> context) {
if (!(context.getSource().getEntity() instanceof net.minecraft.world.entity.player.Player)) { if (!(context.getSource().getEntity() instanceof net.minecraft.world.entity.player.Player player)) {
context.getSource() context.getSource().sendFailure(Component.literal("Cette commande ne peut etre utilisee que par un joueur"));
.sendFailure(Component.literal("Cette commande ne peut etre utilisee que par un joueur"));
return 0; return 0;
} }
net.minecraft.world.entity.player.Player player = (net.minecraft.world.entity.player.Player) context.getSource() int x = player.blockPosition().getX();
.getEntity(); int z = player.blockPosition().getZ();
net.minecraft.world.level.Level level = player.level();
net.minecraft.core.BlockPos pos = player.blockPosition();
var biomeHolder = level.getBiome(pos); String zone = LatitudeConfig.zoneName(z);
ResourceKey<Biome> biomeKey = biomeHolder.unwrapKey() int coldZ = LatitudeConfig.getColdZoneZ();
.orElse(ResourceKey.create(Registries.BIOME, ResourceLocation.fromNamespaceAndPath("minecraft", "plains"))); int hotZ = LatitudeConfig.getHotZoneZ();
ResourceLocation biomeId = biomeKey.location(); ChatFormatting zoneColor = zoneColor(zone);
String biomeName = biomeId.getPath();
context.getSource().sendSuccess(() -> Component.literal("=== Minerais dans: " + biomeName + " ==="), true); context.getSource().sendSuccess(() -> Component.literal(
context.getSource().sendSuccess(() -> Component.literal("Biome ID: " + biomeId), true); "=== Position: X=" + x + " Z=" + z + " ===").withStyle(ChatFormatting.GRAY), true);
context.getSource().sendSuccess(() -> Component.literal(
"Zone determinee par la coordonnee Z : " + zone).withStyle(zoneColor), true);
context.getSource().sendSuccess(() -> Component.literal(
"Z < " + coldZ + " = froid | " + coldZ + " a " + hotZ + " = tempere | Z > " + hotZ + " = chaud")
.withStyle(ChatFormatting.GRAY), true);
// Vérifier les tags personnalisés du mod List<String> zoneOres = new ArrayList<>();
Set<String> foundTags = new HashSet<>(); switch (zone) {
List<String> foundOres = new ArrayList<>(); case "COLD" -> zoneOres.addAll(COLD_ORES);
case "HOT" -> zoneOres.addAll(HOT_ORES);
// Vérifier chaque tag de biome et ajouter les minerais correspondants case "TEMPERATE" -> zoneOres.addAll(TEMPERATE_ORES);
if (isBiomeInTag(level, pos, COLD_BIOMES_TAG)) {
foundTags.add("cold_biomes");
foundOres.addAll(COLD_ORES);
}
if (isBiomeInTag(level, pos, HOT_BIOMES_TAG)) {
foundTags.add("hot_biomes");
foundOres.addAll(HOT_ORES);
}
if (isBiomeInTag(level, pos, MOUNTAIN_BIOMES_TAG)) {
foundTags.add("mountain_biomes");
foundOres.addAll(MOUNTAIN_ORES);
}
if (isBiomeInTag(level, pos, TEMPERED_BIOMES_TAG)) {
foundTags.add("tempered_biomes");
foundOres.addAll(TEMPERED_ORES);
}
if (isBiomeInTag(level, pos, RARE_BIOMES_TAG)) {
foundTags.add("rare_biomes");
foundOres.addAll(RARE_ORES);
} }
// Ajouter les minerais présents partout Set<String> allOres = new LinkedHashSet<>(zoneOres);
foundOres.addAll(EVERYWHERE_ORES); allOres.addAll(EVERYWHERE_ORES);
// Afficher les tags trouvés context.getSource().sendSuccess(() -> Component.literal(
if (!foundTags.isEmpty()) { "Minerais trouvables (" + allOres.size() + ") :").withStyle(ChatFormatting.WHITE), true);
context.getSource().sendSuccess(() -> Component.literal("Tags du mod: " + String.join(", ", foundTags)), for (String ore : zoneOres) {
true); context.getSource().sendSuccess(() -> Component.literal(" * " + ore).withStyle(zoneColor), true);
} else {
context.getSource().sendSuccess(() -> Component.literal("Tags du mod: aucun"), true);
} }
for (String ore : EVERYWHERE_ORES) {
// Afficher les minerais (sans doublons) context.getSource().sendSuccess(() -> Component.literal(" * " + ore)
if (!foundOres.isEmpty()) { .withStyle(ChatFormatting.LIGHT_PURPLE), true);
Set<String> uniqueOres = new LinkedHashSet<>(foundOres);
context.getSource().sendSuccess(() -> Component.literal("Minerais trouvables:"), true);
for (String ore : uniqueOres) {
context.getSource().sendSuccess(() -> Component.literal(" * " + ore), true);
}
} else {
context.getSource().sendSuccess(() -> Component.literal("Aucun minerai specifique"), true);
} }
return 1; return 1;
} }
private static boolean isBiomeInTag(net.minecraft.world.level.Level level, net.minecraft.core.BlockPos pos, private static ChatFormatting zoneColor(String zone) {
TagKey<Biome> tag) { return switch (zone) {
return level.getBiome(pos).is(tag); case "COLD" -> ChatFormatting.AQUA;
case "HOT" -> ChatFormatting.GOLD;
case "TEMPERATE" -> ChatFormatting.GREEN;
default -> ChatFormatting.WHITE;
};
} }
} }
@@ -16,6 +16,7 @@ public class ModConfigs {
TOOL_STATS = new ToolStatsConfig(BUILDER); TOOL_STATS = new ToolStatsConfig(BUILDER);
DROPS = new DropsConfig(BUILDER); DROPS = new DropsConfig(BUILDER);
FEATURES = new FeatureToggleConfig(BUILDER); FEATURES = new FeatureToggleConfig(BUILDER);
LATITUDE_ORE = new LatitudeOreConfig(BUILDER);
BUILDER.pop(); BUILDER.pop();
SPEC = BUILDER.build(); SPEC = BUILDER.build();
@@ -318,4 +319,30 @@ public class ModConfigs {
builder.pop(); builder.pop();
} }
} }
// Configuration for the latitude ore distribution system
public static class LatitudeOreConfig {
public final ModConfigSpec.ConfigValue<Integer> coldZoneZ;
public final ModConfigSpec.ConfigValue<Integer> hotZoneZ;
public LatitudeOreConfig(ModConfigSpec.Builder builder) {
builder.push("latitude_ore_zones");
coldZoneZ = builder
.comment("Z coordinate below which is the COLD ore zone (north). Default: -8000",
"Example: -8000 means Z < -8000 gets cold ores (lapis, diamond)",
"Changing this also shifts the temperature latitude scale.")
.defineInRange("coldZoneZ", -8000, -32000, 0);
hotZoneZ = builder
.comment("Z coordinate above which is the HOT ore zone (south). Default: 8000",
"Example: 8000 means Z > 8000 gets hot ores (copper, redstone, gold)",
"Changing this also shifts the temperature latitude scale.")
.defineInRange("hotZoneZ", 8000, 0, 32000);
builder.pop();
}
}
public static final LatitudeOreConfig LATITUDE_ORE;
} }
@@ -1,63 +1,41 @@
package net.mcreator.customoregen.item; package net.mcreator.customoregen.item;
import net.minecraft.core.registries.Registries; import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.biome.Biome;
import java.util.*; import java.util.*;
import net.mcreator.customoregen.worldgen.LatitudeConfig;
public class OreBiomeFinderItem extends Item { public class OreBiomeFinderItem extends Item {
// Tags personnalisés du mod
private static final TagKey<Biome> COLD_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "cold_biomes"));
private static final TagKey<Biome> HOT_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "hot_biomes"));
private static final TagKey<Biome> MOUNTAIN_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "mountain_biomes"));
private static final TagKey<Biome> TEMPERED_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "tempered_biomes"));
private static final TagKey<Biome> RARE_BIOMES_TAG = TagKey.create(Registries.BIOME,
ResourceLocation.fromNamespaceAndPath("custom_ore_gen", "rare_biomes"));
// Minerais par catégorie
private static final List<String> COLD_ORES = Arrays.asList( private static final List<String> COLD_ORES = Arrays.asList(
"Lapis (stone) [Y: 0 à 32]", "Lapis (stone) [Y: 0 a 32]",
"Lapis (deepslate) [Y: -64 à 0]", "Lapis (deepslate) [Y: -64 a 0]",
"Diamant (deepslate) [Y: -64 à 0]"); "Diamant (deepslate) [Y: -64 a 0]");
private static final List<String> HOT_ORES = Arrays.asList( private static final List<String> HOT_ORES = Arrays.asList(
"Or Pur (stone) [Y: 0 à 256]", "Or (stone) [Y: 0 a 320]",
"Or Pur (deepslate) [Y: -64 à 0]", "Or (deepslate) [Y: -64 a 0]",
"Redstone (stone) [Y: -10 à 20]", "Cuivre (haut) [Y: 15 a 320]",
"Redstone (deepslate) [Y: -80 à -30]", "Cuivre (bas) [Y: -64 a 0]",
"Cuivre (haut) [Y: 15 à 256]", "Redstone (stone) [Y: -10 a 20]",
"Cuivre (bas) [Y: -64 à 0]"); "Redstone (deepslate) [Y: -80 a -30]");
private static final List<String> MOUNTAIN_ORES = Arrays.asList( private static final List<String> TEMPERATE_ORES = Arrays.asList(
"Emeraude (haute altitude) [Y: 0 à 64]"); "Fer (stone) [Y: 0 a 100]",
"Fer (deepslate) [Y: -64 a 0]",
private static final List<String> TEMPERED_ORES = Arrays.asList( "Charbon concentre [Y: 0 a 70]");
"Charbon Concentre [Y: 0 à 70]",
"Fer (stone) [Y: 0 à 100]",
"Fer (deepslate) [Y: -64 à 0]");
private static final List<String> RARE_ORES = Arrays.asList(
"Emeraude (basse altitude) [Y: -64 à 0]");
private static final List<String> EVERYWHERE_ORES = Arrays.asList( private static final List<String> EVERYWHERE_ORES = Arrays.asList(
"Diamant Shard (deepslate) [Y: -64 à -40]", "Diamant Shard (deepslate) [Y: -64 a -40]",
"Bloc Diamant Shard [Y: 0 à 15]"); "Bloc Diamant Shard [Y: 0 a 15]");
public OreBiomeFinderItem() { public OreBiomeFinderItem() {
super(new Item.Properties().stacksTo(1)); super(new Item.Properties().stacksTo(1));
@@ -68,68 +46,52 @@ public class OreBiomeFinderItem extends Item {
ItemStack stack = player.getItemInHand(hand); ItemStack stack = player.getItemInHand(hand);
if (!level.isClientSide) { if (!level.isClientSide) {
BlockPos pos = player.blockPosition(); int x = player.blockPosition().getX();
var biomeHolder = level.getBiome(pos); int z = player.blockPosition().getZ();
ResourceKey<Biome> biomeKey = biomeHolder.unwrapKey()
.orElse(ResourceKey.create(Registries.BIOME, ResourceLocation.fromNamespaceAndPath("minecraft", "plains")));
ResourceLocation biomeId = biomeKey.location();
String biomeName = biomeId.getPath();
player.displayClientMessage(Component.literal("=== Minerais dans: " + biomeName + " ==="), false); String zone = LatitudeConfig.zoneName(z);
player.displayClientMessage(Component.literal("Biome ID: " + biomeId), false); int coldZ = LatitudeConfig.getColdZoneZ();
int hotZ = LatitudeConfig.getHotZoneZ();
ChatFormatting zoneColor = zoneColor(zone);
// Vérifier les tags personnalisés du mod player.displayClientMessage(Component.literal(
Set<String> foundTags = new HashSet<>(); "=== Position: X=" + x + " Z=" + z + " ===").withStyle(ChatFormatting.GRAY), false);
List<String> foundOres = new ArrayList<>(); player.displayClientMessage(Component.literal(
"Zone determinee par la coordonnee Z : " + zone).withStyle(zoneColor), false);
player.displayClientMessage(Component.literal(
"Z < " + coldZ + " = froid | " + coldZ + " a " + hotZ + " = tempere | Z > " + hotZ + " = chaud")
.withStyle(ChatFormatting.GRAY), false);
// Vérifier chaque tag de biome et ajouter les minerais correspondants List<String> zoneOres = new ArrayList<>();
if (isBiomeInTag(level, pos, COLD_BIOMES_TAG)) { switch (zone) {
foundTags.add("cold_biomes"); case "COLD" -> zoneOres.addAll(COLD_ORES);
foundOres.addAll(COLD_ORES); case "HOT" -> zoneOres.addAll(HOT_ORES);
} case "TEMPERATE" -> zoneOres.addAll(TEMPERATE_ORES);
if (isBiomeInTag(level, pos, HOT_BIOMES_TAG)) {
foundTags.add("hot_biomes");
foundOres.addAll(HOT_ORES);
}
if (isBiomeInTag(level, pos, MOUNTAIN_BIOMES_TAG)) {
foundTags.add("mountain_biomes");
foundOres.addAll(MOUNTAIN_ORES);
}
if (isBiomeInTag(level, pos, TEMPERED_BIOMES_TAG)) {
foundTags.add("tempered_biomes");
foundOres.addAll(TEMPERED_ORES);
}
if (isBiomeInTag(level, pos, RARE_BIOMES_TAG)) {
foundTags.add("rare_biomes");
foundOres.addAll(RARE_ORES);
} }
// Ajouter les minerais présents partout Set<String> allOres = new LinkedHashSet<>(zoneOres);
foundOres.addAll(EVERYWHERE_ORES); allOres.addAll(EVERYWHERE_ORES);
// Afficher les tags trouvés player.displayClientMessage(Component.literal(
if (!foundTags.isEmpty()) { "Minerais trouvables (" + allOres.size() + ") :").withStyle(ChatFormatting.WHITE), false);
player.displayClientMessage(Component.literal("Tags du mod: " + String.join(", ", foundTags)), false); for (String ore : zoneOres) {
} else { player.displayClientMessage(Component.literal(" * " + ore).withStyle(zoneColor), false);
player.displayClientMessage(Component.literal("Tags du mod: aucun"), false);
} }
for (String ore : EVERYWHERE_ORES) {
// Afficher les minerais (sans doublons) player.displayClientMessage(Component.literal(" * " + ore)
if (!foundOres.isEmpty()) { .withStyle(ChatFormatting.LIGHT_PURPLE), false);
Set<String> uniqueOres = new LinkedHashSet<>(foundOres);
player.displayClientMessage(Component.literal("Minerais trouvables:"), false);
for (String ore : uniqueOres) {
player.displayClientMessage(Component.literal(" * " + ore), false);
}
} else {
player.displayClientMessage(Component.literal("Aucun minerai specifique"), false);
} }
} }
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide); return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
} }
private static boolean isBiomeInTag(Level level, BlockPos pos, TagKey<Biome> tag) { private static ChatFormatting zoneColor(String zone) {
return level.getBiome(pos).is(tag); return switch (zone) {
case "COLD" -> ChatFormatting.AQUA;
case "HOT" -> ChatFormatting.GOLD;
case "TEMPERATE" -> ChatFormatting.GREEN;
default -> ChatFormatting.WHITE;
};
} }
} }
@@ -0,0 +1,91 @@
package net.mcreator.customoregen.mixin;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.Holder;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Optional;
/**
* Makes the Deep Dark a legendary northern-only feature.
*
* <p>Vanilla places Deep Dark based on depth alone, appearing everywhere underground. This mixin
* intercepts biome resolution: when vanilla selects Deep Dark outside cold latitudes
* (temperature > -0.3), it brute-force searches the parameter list for the best non-Deep-Dark
* biome using the same fitness function as vanilla (sum of squared parameter distances).</p>
*/
@Mixin(MultiNoiseBiomeSource.class)
public abstract class MultiNoiseBiomeSourceMixin {
@Shadow
@Final
private com.mojang.datafixers.util.Either<Climate.ParameterList<Holder<Biome>>, Holder<MultiNoiseBiomeSourceParameterList>> parameters;
@Inject(method = "getNoiseBiome(Lnet/minecraft/world/level/biome/Climate$TargetPoint;)Lnet/minecraft/core/Holder;",
at = @At("RETURN"), cancellable = true)
private void customoregen$latitudeDeepDarkLock(Climate.TargetPoint target,
CallbackInfoReturnable<Holder<Biome>> cir) {
Holder<Biome> original = cir.getReturnValue();
if (original == null) return;
if (original.unwrapKey().orElse(null) != Biomes.DEEP_DARK) return;
float temperature = Climate.unquantizeCoord(target.temperature());
if (temperature <= -0.3f) return; // Cold zone — keep Deep Dark
// Warm zone: brute-force search for best non-Deep-Dark biome
Climate.ParameterList<Holder<Biome>> paramList = resolveParamList();
if (paramList == null) return;
Holder<Biome> best = null;
long bestFitness = Long.MAX_VALUE;
for (Pair<Climate.ParameterPoint, Holder<Biome>> entry : paramList.values()) {
if (entry.getSecond().unwrapKey().orElse(null) == Biomes.DEEP_DARK) continue;
long fitness = computeFitness(entry.getFirst(), target);
if (fitness < bestFitness) {
bestFitness = fitness;
best = entry.getSecond();
}
}
if (best != null) {
cir.setReturnValue(best);
}
}
/**
* Compute fitness using the same formula as Climate.ParameterPoint.fitness():
* fitness = offset + sum of squared parameter distances for temperature, humidity,
* continentalness, erosion, depth, weirdness.
*/
private static long computeFitness(Climate.ParameterPoint params, Climate.TargetPoint target) {
long tempDist = params.temperature().distance(target.temperature());
long humidDist = params.humidity().distance(target.humidity());
long contDist = params.continentalness().distance(target.continentalness());
long erosDist = params.erosion().distance(target.erosion());
long depthDist = params.depth().distance(target.depth());
long weirdDist = params.weirdness().distance(target.weirdness());
return params.offset()
+ tempDist * tempDist + humidDist * humidDist + contDist * contDist
+ erosDist * erosDist + depthDist * depthDist + weirdDist * weirdDist;
}
private Climate.ParameterList<Holder<Biome>> resolveParamList() {
Optional<Climate.ParameterList<Holder<Biome>>> left = parameters.left();
if (left.isPresent()) return left.get();
Optional<Holder<MultiNoiseBiomeSourceParameterList>> right = parameters.right();
if (right.isPresent() && right.get().isBound()) {
return right.get().value().parameters();
}
return null;
}
}
@@ -1,130 +0,0 @@
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);
}
}
/** 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));
}
}
@@ -1,356 +0,0 @@
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><b>Underground philosophy:</b> the surface latitude biome extends downward by default, so
* mining under a desert feels like being in the desert. Cave biomes (lush / dripstone) only
* appear as rare climatic pockets in the mid underground, and the Deep Dark is a legendary
* (~1%) feature of the deep zone. This keeps the latitude logic coherent at every depth
* instead of overriding it with a uniform cave-biome slab.</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 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)));
// ------------------------------------------------------------------
// Surface / climate tunables
// ------------------------------------------------------------------
/** Number of blocks for the temperature to go from 0 (equator) to +-1 (pole). */
private static final double TEMPERATURE_SCALE = LatitudeMath.TEMPERATURE_SCALE;
/** Noise frequency for the climate band boundary wobble. */
private static final double BOUNDARY_NOISE_SCALE = LatitudeMath.BOUNDARY_NOISE_SCALE;
/** Amplitude of the boundary wobble (in temperature units). */
private static final double BOUNDARY_NOISE_AMPLITUDE = LatitudeMath.BOUNDARY_NOISE_AMPLITUDE;
/** Selector noise frequency for surface sub-biomes (very low = very large biomes). */
private static final double SURFACE_SELECTOR_SCALE = LatitudeMath.SURFACE_SELECTOR_SCALE;
/** Frequency of the land/ocean mask noise. */
private static final double OCEAN_NOISE_SCALE = LatitudeMath.OCEAN_NOISE_SCALE;
/** Above this ocean-noise value, the column is ocean. */
private static final double OCEAN_THRESHOLD = LatitudeMath.OCEAN_THRESHOLD;
/** Frequency of the moisture noise that carves rare swamp/mangrove pockets. */
private static final double MOISTURE_SCALE = LatitudeMath.MOISTURE_SCALE;
/** Above this moisture value a wet biome (swamp/mangrove) overrides the surface. ~8% of land. */
private static final double MOISTURE_THRESHOLD = LatitudeMath.MOISTURE_THRESHOLD;
// ------------------------------------------------------------------
// Underground tunables (3-zone model)
// ------------------------------------------------------------------
/** Top of the deep zone (Y &lt; this = deep underground). Deep Dark can live here. */
private static final int DEEP_ZONE_TOP = LatitudeMath.DEEP_ZONE_TOP;
/** Top of the mid-cave zone (DEEP_ZONE_TOP &le; Y &lt; this = mid caves, lush/dripstone pockets). */
private static final int MID_CAVE_TOP = LatitudeMath.MID_CAVE_TOP;
/** Frequency of the mid-cave pocket noise (lush/dripstone). */
private static final double CAVE_SCALE = LatitudeMath.CAVE_SCALE;
/** Above this value a lush/dripstone pocket overrides the surface biome. ~5-8% of the mid zone. */
private static final double CAVE_THRESHOLD = LatitudeMath.CAVE_THRESHOLD;
/** Frequency of the Deep Dark noise (very low = large, rare regions). */
private static final double DEEP_DARK_SCALE = LatitudeMath.DEEP_DARK_SCALE;
/** Vertical frequency of the Deep Dark noise (keeps it coherent in tall sections). */
private static final double DEEP_DARK_SCALE_Y = LatitudeMath.DEEP_DARK_SCALE_Y;
/** Above this value the Deep Dark overrides. Tuned for ~1-2% of the deep zone (legendary). */
private static final double DEEP_DARK_THRESHOLD = LatitudeMath.DEEP_DARK_THRESHOLD;
// ------------------------------------------------------------------
// Spawn safe zone
// ------------------------------------------------------------------
/** Half-size of the guaranteed safe spawn square around the origin (plains/forest). */
private static final int SPAWN_SAFE_RADIUS = LatitudeMath.SPAWN_SAFE_RADIUS;
/** Selector frequency inside the spawn safe zone (finer, for gentle variety). */
private static final double SPAWN_SELECTOR_SCALE = LatitudeMath.SPAWN_SELECTOR_SCALE;
// ------------------------------------------------------------------
// 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;
private final ImprovedNoise caveNoise;
private final ImprovedNoise deepDarkNoise;
/** 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;
/** Cave biomes resolved eagerly (vanilla, always present). Lush = humid bands, Dripstone = dry bands. */
private final Holder<Biome> lushCavesBiome;
private final Holder<Biome> dripstoneCavesBiome;
private final Holder<Biome> deepDarkBiome;
/** 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.caveNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x43415645L));
this.deepDarkNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x44454550L));
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());
resolvedVanilla.put(band, new ResolvedBand(surface, ocean));
all.addAll(surface);
all.addAll(ocean);
}
// Cave biomes (vanilla) declared so the source advertises them as possible.
this.lushCavesBiome = resolveAndAdd(biomeGetter, Biomes.LUSH_CAVES, all);
this.dripstoneCavesBiome = resolveAndAdd(biomeGetter, Biomes.DRIPSTONE_CAVES, all);
this.deepDarkBiome = resolveAndAdd(biomeGetter, Biomes.DEEP_DARK, all);
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());
}
private static Holder<Biome> resolveAndAdd(HolderGetter<Biome> getter, net.minecraft.resources.ResourceKey<Biome> key, Set<Holder<Biome>> into) {
Holder<Biome> holder = getter.get(key).orElse(null);
if (holder != null) {
into.add(holder);
}
return holder;
}
@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 && LatitudeMath.isInSpawnSafeZone(blockX, blockZ)) {
return pickBiome(spawnSafeBiomes, blockX, blockZ, SPAWN_SELECTOR_SCALE);
}
// Determine the column's base biome: ocean mask, rare wet pocket, or latitude surface biome.
Holder<Biome> base = baseSurfaceBiome(band, pool, blockX, blockZ);
// Underground cave overrides (3-zone model) extend latitude rather than replace it.
return applyCaveOverrides(base, band, blockX, blockY, blockZ);
}
/** Resolve the surface biome for a column: ocean, swamp/mangrove pocket, or the band's surface tag. */
private Holder<Biome> baseSurfaceBiome(BiomeBand band, ResolvedBand pool, int blockX, int blockZ) {
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);
}
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;
}
}
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);
}
/**
* Underground overrides keep the latitude biome by default and only carve rare cave features:
* <ul>
* <li>Deep zone (Y &lt; {@value #DEEP_ZONE_TOP}): legendary Deep Dark pockets (~1%), else surface biome.</li>
* <li>Mid caves (DEEP_ZONE_TOP &le; Y &lt; {@value #MID_CAVE_TOP}): lush/dripstone pockets (~8%) matching the climate.</li>
* <li>Near surface: no override, latitude biome as-is.</li>
* </ul>
*/
private Holder<Biome> applyCaveOverrides(Holder<Biome> surfaceBiome, BiomeBand band, int blockX, int blockY, int blockZ) {
LatitudeMath.Zone zone = LatitudeMath.zoneForY(blockY);
// Deep zone: legendary Deep Dark.
if (zone == LatitudeMath.Zone.DEEP) {
if (deepDarkBiome != null) {
double dd = deepDarkNoise.noise(blockX * DEEP_DARK_SCALE, blockY * DEEP_DARK_SCALE_Y, blockZ * DEEP_DARK_SCALE);
if (LatitudeMath.isDeepDarkPocket(dd)) {
return deepDarkBiome;
}
}
return surfaceBiome;
}
// Mid caves: rare lush/dripstone pockets by climate.
if (zone == LatitudeMath.Zone.MID_CAVE) {
Holder<Biome> caveBiome = caveBiomeFor(band);
if (caveBiome != null) {
double cave = caveNoise.noise(blockX * CAVE_SCALE, 0.0, blockZ * CAVE_SCALE);
if (LatitudeMath.isMidCavePocket(cave)) {
return caveBiome;
}
}
}
return surfaceBiome;
}
/** Lush caves for humid/warm bands, dripstone for dry/cold bands. */
private Holder<Biome> caveBiomeFor(BiomeBand band) {
if (band == BiomeBand.FROZEN || band == BiomeBand.COLD) {
return dripstoneCavesBiome;
}
return lushCavesBiome;
}
/** 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 normalized = LatitudeMath.normalisedSelector(n1, n2);
int idx = LatitudeMath.selectorIndex(normalized, biomes.size());
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) {
}
}
@@ -0,0 +1,57 @@
package net.mcreator.customoregen.worldgen;
import net.mcreator.customoregen.config.ModConfigs;
/**
* Runtime accessor for the latitude zone thresholds.
*
* <p>These values come from the mod's config file and control both the ore distribution (which Z
* coordinates get which ores) and the temperature latitude scale. Changing them at runtime (via
* the config file) shifts both the ore zones and the climate bands simultaneously.</p>
*
* <p>Defaults:
* <ul>
* <li>{@code coldZoneZ = -8000}: north of Z=-8000 is the cold zone</li>
* <li>{@code hotZoneZ = 8000}: south of Z=8000 is the hot zone</li>
* <li>Between -8000 and 8000 is the temperate zone (16,000 blocks wide)</li>
* </ul>
* </p>
*/
public final class LatitudeConfig {
private LatitudeConfig() {}
private static int coldZoneZ = -8000;
private static int hotZoneZ = 8000;
/** Z threshold below which is the cold zone. Negative = north. */
public static int getColdZoneZ() {
try {
return net.mcreator.customoregen.config.ModConfigs.LATITUDE_ORE.coldZoneZ.get();
} catch (Exception e) {
return coldZoneZ;
}
}
/** Z threshold above which is the hot zone. Positive = south. */
public static int getHotZoneZ() {
try {
return net.mcreator.customoregen.config.ModConfigs.LATITUDE_ORE.hotZoneZ.get();
} catch (Exception e) {
return hotZoneZ;
}
}
/** Called from the config load to update thresholds. */
public static void update(int coldZ, int hotZ) {
coldZoneZ = coldZ;
hotZoneZ = hotZ;
}
/** Returns a human-readable description of the zone for a given Z. */
public static String zoneName(int z) {
if (z < getColdZoneZ()) return "COLD";
if (z > getHotZoneZ()) return "HOT";
return "TEMPERATE";
}
}
@@ -1,518 +0,0 @@
package net.mcreator.customoregen.worldgen;
import net.mcreator.customoregen.CustomOreGenMod;
import net.mcreator.customoregen.config.ConfigHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
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.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Automated world-map validation for the latitude biome system, run as a NeoForge
* {@link GameTest} so it executes inside a real game server with the full biome registry.
*
* <p>Launch with {@code ./gradlew runGameTestServer}. The test samples a large grid through a
* freshly built {@link LatitudeBiomeSource} at several depths and renders one PNG map per depth
* (a "geological slice" view), writes a distribution report and asserts the climate + cave
* invariants. No manual in-game testing required.</p>
*/
@GameTestHolder(CustomOreGenMod.MODID)
@PrefixGameTestTemplate(false)
public class LatitudeGameTest {
private static final int RADIUS = 16000; // covers equator -> pole
private static final int STEP = 80; // ~400x400 samples
private static final long SEED = 0L;
private static final Path OUT_DIR = Path.of("gametest-results", "latitude");
// Sampling depths (one PNG per depth = a top-down "slice").
private static final int SURFACE_Y = 64;
private static final int MID_CAVE_Y = -15; // -30 <= Y < 0 (lush/dripstone pockets)
private static final int DEEP_Y = -50; // Y < -30 (legendary Deep Dark)
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);
col(0xC8E6FF, Biomes.SNOWY_PLAINS, Biomes.ICE_SPIKES, Biomes.SNOWY_TAIGA, Biomes.GROVE,
Biomes.SNOWY_SLOPES, Biomes.JAGGED_PEAKS, Biomes.FROZEN_PEAKS, Biomes.FROZEN_OCEAN,
Biomes.DEEP_FROZEN_OCEAN, Biomes.FROZEN_RIVER);
col(0x8FB8D6, Biomes.TAIGA, Biomes.OLD_GROWTH_PINE_TAIGA, Biomes.OLD_GROWTH_SPRUCE_TAIGA,
Biomes.WINDSWEPT_HILLS, Biomes.WINDSWEPT_FOREST, Biomes.WINDSWEPT_GRAVELLY_HILLS,
Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN);
col(0x7CC576, Biomes.DARK_FOREST, Biomes.OLD_GROWTH_BIRCH_FOREST, Biomes.RIVER,
Biomes.OCEAN, Biomes.DEEP_OCEAN);
col(0xE0C068, Biomes.SAVANNA, Biomes.SAVANNA_PLATEAU, Biomes.WINDSWEPT_SAVANNA,
Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN);
col(0xE0884C, Biomes.DESERT, Biomes.BADLANDS, Biomes.WOODED_BADLANDS, Biomes.ERODED_BADLANDS,
Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE, Biomes.WARM_OCEAN);
col(0x5B7A3A, Biomes.SWAMP);
col(0x3A5A3A, Biomes.MANGROVE_SWAMP);
// Cave biomes - distinct colors so the slices are readable.
col(0x4DB05A, Biomes.LUSH_CAVES); // lush green
col(0xC8782C, Biomes.DRIPSTONE_CAVES); // stalactite orange-brown
col(0x0A4D54, Biomes.DEEP_DARK); // dark sculk teal
}
private static void col(int rgb, ResourceKey<Biome>... keys) {
for (ResourceKey<Biome> k : keys) COLORS.put(k, rgb);
}
@GameTest(template = "empty_1x1", timeoutTicks = 600)
public static void latitudeMap(GameTestHelper helper) {
try {
runLatitudeValidation(helper);
helper.succeed();
} catch (AssertionError | Exception e) {
CustomOreGenMod.LOGGER.error("Latitude game test failed", e);
helper.fail(e.getMessage());
}
}
/**
* Proves the config-gated biome modifiers actually drive ore placement: queries the
* chunk generator's effective biome generation settings (which reflect applied biome
* modifiers, terrain-independent so it works on the void GameTest level) for the Shard
* Diamond placed feature. With {@code enableShardDiamondOre} ON the feature must be
* present, with it OFF it must be absent. Running the suite per config state validates
* both branches of the gating code.
*/
/**
* Compares ore feature presence across vanilla and Biomes O' Plenty temperate biomes by
* querying each biome holder's {@code modifiableBiomeInfo()} (the generation settings after
* NeoForge applies every biome modifier, including the config-gated ore modifiers).
*
* <p>In-game, vanilla plains shows custom iron/coal while BOP biomes (highland, shrubland)
* show none. This test reproduces the comparison in the registry context to pinpoint
* whether the config-gated modifiers actually inject ores into BOP biomes' settings.</p>
*/
@GameTest(template = "empty_1x1", timeoutTicks = 200)
public static void oreFeaturesAcrossBiomes(GameTestHelper helper) {
try {
ServerLevel level = helper.getLevel();
var biomeRegistry = level.registryAccess().lookupOrThrow(Registries.BIOME);
String[] temperateBiomes = {
"minecraft:plains", "minecraft:birch_forest", "minecraft:forest",
"biomesoplenty:shrubland", "biomesoplenty:field", "biomesoplenty:moor",
"biomesoplenty:grassland", "biomesoplenty:woodland", "biomesoplenty:mediterranean_forest"
};
StringBuilder report = new StringBuilder("\n=== ORE FEATURE PRESENCE PER BIOME (after biome modifiers) ===\n");
boolean modifiersApplied = false;
for (String id : temperateBiomes) {
var key = net.minecraft.resources.ResourceKey.create(Registries.BIOME,
net.minecraft.resources.ResourceLocation.parse(id));
Holder<Biome> holder = biomeRegistry.get(key).orElse(null);
if (holder == null) {
report.append(String.format("%-36s NOT FOUND%n", id));
continue;
}
var genSettings = holder.value().modifiableBiomeInfo().get().generationSettings();
int totalFeatures = 0;
java.util.List<String> customOres = new java.util.ArrayList<>();
for (net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.placement.PlacedFeature> stepSet : genSettings.features()) {
for (net.minecraft.core.Holder<net.minecraft.world.level.levelgen.placement.PlacedFeature> featHolder : stepSet) {
totalFeatures++;
var fk = featHolder.unwrapKey();
if (fk.isPresent() && fk.get().location().getNamespace().equals("custom_ore_gen")) {
customOres.add(fk.get().location().getPath());
}
}
}
if (totalFeatures > 0) modifiersApplied = true;
report.append(String.format("%-36s total=%-4d customOres=[%s]%n",
id, totalFeatures, String.join(",", customOres)));
}
CustomOreGenMod.LOGGER.info(report.toString());
if (!modifiersApplied) {
CustomOreGenMod.LOGGER.info("oreFeaturesAcrossBiomes: biome modifiers not observable in this context "
+ "(void test level). Run against a real latitude world for the decisive comparison.");
}
helper.succeed();
} catch (AssertionError | Exception e) {
CustomOreGenMod.LOGGER.error("oreFeaturesAcrossBiomes test failed", e);
helper.fail(e.getMessage());
}
}
/**
* Determinism: two {@link LatitudeBiomeSource} instances built with the same seed must
* return biome holders that resolve to the <b>same biome key</b> at every sampled point.
* Worldgen is shared work across threads, so equality of biome-by-coord is a hard contract.
* Also asserts a coarse cross-depth determinism: re-querying the same (x,z) at different Y in
* the surface zone returns the same biome (latitude surface band does not depend on Y in the
* zone with no override).
*/
@GameTest(template = "empty_1x1", timeoutTicks = 400)
public static void latitudeDeterminism(GameTestHelper helper) {
try {
ServerLevel level = helper.getLevel();
HolderGetter<Biome> getter = level.registryAccess().lookupOrThrow(Registries.BIOME);
LatitudeBiomeSource a = new LatitudeBiomeSource(SEED, getter);
LatitudeBiomeSource b = new LatitudeBiomeSource(SEED, getter);
int mismatches = 0;
int checked = 0;
// Sample a coarse grid across the climate range, several Y of the surface zone.
int[] ys = {SURFACE_Y, SURFACE_Y + 1, 100, 200, 320};
for (int blockZ = -RADIUS; blockZ <= RADIUS; blockZ += 1000) {
for (int blockX = -4000; blockX <= 4000; blockX += 1000) {
for (int y : ys) {
ResourceKey<Biome> ka = a.getNoiseBiome(blockX >> 2, y >> 2, blockZ >> 2, null).unwrapKey().orElse(null);
ResourceKey<Biome> kb = b.getNoiseBiome(blockX >> 2, y >> 2, blockZ >> 2, null).unwrapKey().orElse(null);
checked++;
if (!java.util.Objects.equals(ka, kb)) mismatches++;
}
}
}
assertTrue(checked > 100, "determinism test must check many points, checked=" + checked);
assertEquals(0, mismatches,
"same-seed sources must be identical everywhere, had " + mismatches + " / " + checked + " mismatches");
CustomOreGenMod.LOGGER.info("[latitude-determinism] OK: {} points all identical for seed={}", checked, SEED);
helper.succeed();
} catch (AssertionError | Exception e) {
CustomOreGenMod.LOGGER.error("latitudeDeterminism test failed", e);
helper.fail(e.getMessage());
}
}
/**
* Cave biomes (LUSH_CAVES, DRIPSTONE_CAVES, DEEP_DARK) must never surface: the 3-zone model
* only injects them underground (Y &lt; 0 for lush/dripstone, Y &lt; -30 for Deep Dark).
* Sampling the whole surface grid and asserting zero cave biomes protects against a regression
* that would leak a cave biome to the surface (e.g. a swapped zone boundary).
*/
@GameTest(template = "empty_1x1", timeoutTicks = 400)
public static void noCaveBiomeAtSurface(GameTestHelper helper) {
try {
ServerLevel level = helper.getLevel();
HolderGetter<Biome> getter = level.registryAccess().lookupOrThrow(Registries.BIOME);
LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter);
Set<ResourceKey<Biome>> caveBiomes = Set.of(
Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
int surfaceCaveLeaks = 0;
int checked = 0;
for (int blockZ = -RADIUS; blockZ <= RADIUS; blockZ += STEP) {
for (int blockX = -RADIUS; blockX <= RADIUS; blockX += STEP) {
ResourceKey<Biome> k = source.getNoiseBiome(blockX >> 2, SURFACE_Y >> 2, blockZ >> 2, null)
.unwrapKey().orElse(null);
checked++;
if (k != null && caveBiomes.contains(k)) surfaceCaveLeaks++;
}
}
assertEquals(0, surfaceCaveLeaks,
"no cave biome must leak to the surface, found " + surfaceCaveLeaks + " / " + checked);
CustomOreGenMod.LOGGER.info("[no-cave-at-surface] OK: {} surface samples, zero cave leaks", checked);
helper.succeed();
} catch (AssertionError | Exception e) {
CustomOreGenMod.LOGGER.error("noCaveBiomeAtSurface test failed", e);
helper.fail(e.getMessage());
}
}
/**
* Spatial continuity / large-biome property: at the surface, the selector noise has a very
* low frequency (SURFACE_SELECTOR_SCALE = 0.00033), so neighbouring sample points (80 blocks
* apart) almost always fall in the same biome. This guards against a regression that would
* turn the world into per-block noise. We accept that band boundaries and ocean/rare pockets
* create some transitions, but they must remain rare ((< 25 %).</p>
*/
@GameTest(template = "empty_1x1", timeoutTicks = 400)
public static void surfaceBiomeContinuity(GameTestHelper helper) {
try {
ServerLevel level = helper.getLevel();
HolderGetter<Biome> getter = level.registryAccess().lookupOrThrow(Registries.BIOME);
LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter);
// Sample a single horizontal line across one temperate row (Z near 0, well inside one band)
// and count how often adjacent samples change biome.
int transitions = 0;
ResourceKey<Biome> prev = null;
int sampleCount = 200;
int z = 160; // just south of equator, well inside TEMPERATE band, outside spawn-safe square
for (int i = 0; i < sampleCount; i++) {
int x = -8000 + i * 80;
ResourceKey<Biome> k = source.getNoiseBiome(x >> 2, SURFACE_Y >> 2, z >> 2, null)
.unwrapKey().orElse(null);
if (prev != null && !java.util.Objects.equals(prev, k)) transitions++;
prev = k;
}
// 80-block spacing on a 0.00033-frequency noise: the noise barely moves, so transitions
// are driven mostly by ocean/rare-pocket edges, not selector turnover.
double transitionRatio = 100.0 * transitions / (sampleCount - 1);
assertTrue(transitionRatio < 25.0,
"surface should have large continuous biomes; transition ratio was "
+ String.format("%.1f%%", transitionRatio) + " across a temperate row");
CustomOreGenMod.LOGGER.info("[surface-continuity] OK: {} transitions / {} samples = {}%",
transitions, sampleCount - 1, String.format("%.1f", transitionRatio));
helper.succeed();
} catch (AssertionError | Exception e) {
CustomOreGenMod.LOGGER.error("surfaceBiomeContinuity test failed", e);
helper.fail(e.getMessage());
}
}
/**
* Progressive climate transition: the surface biome must be predominantly cold at the far
* north, predominantly temperate at the equator, and predominantly hot at the far south.
* This is a finer-grained version of the band-ratio assertions in {@link #latitudeMap},
* checking the gradient direction in three Z windows rather than just the two poles.
*/
@GameTest(template = "empty_1x1", timeoutTicks = 400)
public static void climateGradient(GameTestHelper helper) {
try {
ServerLevel level = helper.getLevel();
HolderGetter<Biome> getter = level.registryAccess().lookupOrThrow(Registries.BIOME);
LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter);
Set<ResourceKey<Biome>> cold = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag());
Set<ResourceKey<Biome>> coldOceans = new HashSet<>(List.of(
Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN, Biomes.FROZEN_RIVER));
cold.addAll(coldOceans);
Set<ResourceKey<Biome>> hot = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag());
Set<ResourceKey<Biome>> hotOceans = new HashSet<>(List.of(
Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN, Biomes.MANGROVE_SWAMP));
hot.addAll(hotOceans);
// Three Z windows, 3200 blocks wide, 800 apart (RADIUS=16000 covers them all).
assertEquals(5, BiomeBand.values().length, "exactly five climate bands expected");
double north = windowBiomeRatio(source, -15000, -11800, cold);
double equator = windowBiomeRatio(source, -1600, 1600, cold); // equator is NOT cold
double south = windowBiomeRatio(source, 11800, 15000, hot);
assertTrue(north > 50.0, "far north must be >50% cold, was " + String.format("%.1f%%", north));
double equatorCold = windowBiomeRatio(source, -1600, 1600, cold);
assertTrue(equatorCold < 25.0,
"equator must NOT be predominantly cold (gradient broken), was " + String.format("%.1f%%", equatorCold));
assertTrue(south > 50.0, "far south must be >50% hot, was " + String.format("%.1f%%", south));
CustomOreGenMod.LOGGER.info("[climate-gradient] OK: north(cold)={}%, south(hot)={}%, equator(cold)={}%",
String.format("%.0f", north), String.format("%.0f", south), String.format("%.0f", equatorCold));
helper.succeed();
} catch (AssertionError | Exception e) {
CustomOreGenMod.LOGGER.error("climateGradient test failed", e);
helper.fail(e.getMessage());
}
}
/** Sample a Z window of the surface and return the % of biomes in the expected set. */
private static double windowBiomeRatio(LatitudeBiomeSource source, int zMin, int zMax,
Set<ResourceKey<Biome>> expected) {
int match = 0;
int total = 0;
for (int z = zMin; z <= zMax; z += 160) {
for (int x = -4000; x <= 4000; x += 800) {
ResourceKey<Biome> k = source.getNoiseBiome(x >> 2, SURFACE_Y >> 2, z >> 2, null)
.unwrapKey().orElse(null);
total++;
if (k != null && expected.contains(k)) match++;
}
}
return total == 0 ? 0 : 100.0 * match / total;
}
private static void runLatitudeValidation(GameTestHelper helper) throws Exception {
ServerLevel level = helper.getLevel();
HolderGetter<Biome> getter = level.registryAccess().lookupOrThrow(Registries.BIOME);
LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter);
Files.createDirectories(OUT_DIR);
// ---- Sample one slice per depth. Each slice produces a PNG + a biome-count map. ----
Slice surface = sampleSlice(source, SURFACE_Y, "latitude_map_surface.png");
Slice midCave = sampleSlice(source, MID_CAVE_Y, "latitude_map_mid_cave.png");
Slice deep = sampleSlice(source, DEEP_Y, "latitude_map_deep.png");
// ---- Surface climate assertions ----
ResourceKey<Biome> spawnKey = source.getNoiseBiome(0, SURFACE_Y >> 2, 0, null).unwrapKey().orElse(null);
Set<ResourceKey<Biome>> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag());
Set<ResourceKey<Biome>> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag());
Set<ResourceKey<Biome>> coldOceans = new HashSet<>(List.of(
Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN,
Biomes.FROZEN_RIVER));
Set<ResourceKey<Biome>> hotOceans = new HashSet<>(List.of(
Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN));
assertNotNull(spawnKey, "spawn biome resolved");
assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey);
Set<ResourceKey<Biome>> northExpected = new HashSet<>();
northExpected.addAll(coldTag);
northExpected.addAll(coldOceans);
double north = sliceBandRatio(surface, BiomeBand.FROZEN, northExpected);
assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen (by tag), was %.1f%%".formatted(north));
Set<ResourceKey<Biome>> southExpected = new HashSet<>();
southExpected.addAll(hotTag);
southExpected.addAll(hotOceans);
southExpected.add(Biomes.MANGROVE_SWAMP);
double south = sliceBandRatio(surface, BiomeBand.HOT, southExpected);
assertTrue(south > 60.0, "HOT band should be >60%% warm/hot (by tag), was %.1f%%".formatted(south));
// Swamp stays rare at the surface in the temperate band.
double swampRatio = biomeRatioInBand(surface, BiomeBand.TEMPERATE, Biomes.SWAMP);
assertTrue(swampRatio < 15.0, "swamp must be <15%% of temperate band, was %.2f%%".formatted(swampRatio));
// ---- Cave assertions ----
// Deep Dark is legendary: a small fraction of the deep slice only.
double deepDarkRatio = biomeRatio(deep, Biomes.DEEP_DARK);
assertTrue(deepDarkRatio < 8.0,
"Deep Dark must be rare in the deep zone (<8%%), was %.2f%%".formatted(deepDarkRatio));
// Cave biomes (lush/dripstone) should be a minority even in the mid-cave slice - the latitude
// biome dominates underground by design.
double caveBiomeRatio = biomeRatio(midCave, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES);
assertTrue(caveBiomeRatio < 25.0,
"cave biomes must stay a minority in mid-cave (<25%%), was %.2f%%".formatted(caveBiomeRatio));
// ---- Report ----
StringBuilder r = new StringBuilder();
r.append("Latitude GameTest Report (seed=").append(SEED).append(")\n\n");
r.append("Spawn @ (0,0): ").append(spawnKey).append("\n\n");
r.append("=== Depth slices (PNG) ===\n");
r.append(String.format(" Surface Y=%3d : latitude_map_surface.png%n", SURFACE_Y));
r.append(String.format(" Mid cave Y=%3d: latitude_map_mid_cave.png%n", MID_CAVE_Y));
r.append(String.format(" Deep Y=%3d : latitude_map_deep.png%n%n", DEEP_Y));
r.append("=== Surface top biomes (top 12) ===\n");
appendTop(r, surface.global, 12);
r.append("\n=== Mid cave (Y=").append(MID_CAVE_Y).append(") top biomes (top 10) ===\n");
appendTop(r, midCave.global, 10);
r.append("\n=== Deep (Y=").append(DEEP_Y).append(") top biomes (top 10) ===\n");
appendTop(r, deep.global, 10);
Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString());
// ---- One-line summary ----
CustomOreGenMod.LOGGER.info(String.format(
"Latitude game test OK: spawn=%s | north=%.0f%% | south=%.0f%% | swamp=%.2f%% | "
+ "deepDark=%.2f%% | caveBiomes=%.1f%%",
spawnKey, north, south, swampRatio, deepDarkRatio, caveBiomeRatio));
}
/** Sample the whole grid at a single Y and write a PNG; returns the slice's biome counts. */
private static Slice sampleSlice(LatitudeBiomeSource source, int blockY, String pngName) throws Exception {
int dim = (2 * RADIUS) / STEP + 1;
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, blockY >> 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
}
}
ImageIO.write(img, "png", OUT_DIR.resolve(pngName).toFile());
return new Slice(global, perBand);
}
private static void appendTop(StringBuilder r, Map<ResourceKey<Biome>, Integer> counts, int limit) {
int total = counts.values().stream().mapToInt(Integer::intValue).sum();
counts.entrySet().stream()
.sorted(Map.Entry.<ResourceKey<Biome>, Integer>comparingByValue().reversed())
.limit(limit)
.forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total)));
}
/** Ratio of a set of biomes within a single band of a slice. */
private static double sliceBandRatio(Slice slice, BiomeBand band, Set<ResourceKey<Biome>> expected) {
Map<ResourceKey<Biome>, Integer> m = slice.perBand.get(band);
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;
}
/** Ratio of one or more biomes across a whole slice. */
private static double biomeRatio(Slice slice, ResourceKey<Biome>... keys) {
int total = slice.global.values().stream().mapToInt(Integer::intValue).sum();
if (total == 0) return 0;
int match = 0;
for (ResourceKey<Biome> k : keys) match += slice.global.getOrDefault(k, 0);
return 100.0 * match / total;
}
/** Ratio of a biome within a single band of a slice. */
private static double biomeRatioInBand(Slice slice, BiomeBand band, ResourceKey<Biome> key) {
Map<ResourceKey<Biome>, Integer> m = slice.perBand.get(band);
int total = m.values().stream().mapToInt(Integer::intValue).sum();
if (total == 0) return 0;
return 100.0 * m.getOrDefault(key, 0) / total;
}
/** Resolve all biome keys that belong to a tag, via the real registry. */
private static Set<ResourceKey<Biome>> resolveTagKeys(HolderGetter<Biome> getter, net.minecraft.tags.TagKey<Biome> tag) {
Set<ResourceKey<Biome>> keys = new HashSet<>();
getter.get(tag).ifPresent(set -> set.forEach(h -> h.unwrapKey().ifPresent(keys::add)));
return keys;
}
private static boolean isSafeSpawn(ResourceKey<Biome> key) {
return Set.of(Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS, Biomes.FOREST, Biomes.BIRCH_FOREST,
Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE).contains(key);
}
/** A sampled depth slice: global counts + per-band counts. */
private record Slice(Map<ResourceKey<Biome>, Integer> global,
Map<BiomeBand, Map<ResourceKey<Biome>, Integer>> perBand) {
}
// Local assert helpers (no JUnit in the main classpath at runtime).
private static void assertTrue(boolean condition, String msg) {
if (!condition) throw new AssertionError(msg);
}
private static void assertNotNull(Object o, String msg) {
if (o == null) throw new AssertionError(msg);
}
private static void assertEquals(Object expected, Object actual, String msg) {
if (!java.util.Objects.equals(expected, actual)) {
throw new AssertionError(msg + " (expected=" + expected + ", actual=" + actual + ")");
}
}
}
@@ -1,219 +0,0 @@
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);
}
}
@@ -1,133 +0,0 @@
package net.mcreator.customoregen.worldgen;
/**
* Pure (registry-free) geometry helpers for the latitude world type.
*
* <p>These methods exist so that the core invariants of {@link LatitudeBiomeSource}
* can be unit-tested without a loaded biome registry: temperature derivation, the
* 3-zone underground model, the spawn safe zone, and the dual-octave biome index
* math. {@link LatitudeBiomeSource} delegates to these helpers, so the two stay
* in lock-step by construction (no behaviour drift).</p>
*/
public final class LatitudeMath {
private LatitudeMath() {}
// ------------------------------------------------------------------
// Surface / climate tunables (must mirror LatitudeBiomeSource)
// ------------------------------------------------------------------
public static final double TEMPERATURE_SCALE = 16000.0;
public static final double BOUNDARY_NOISE_SCALE = 0.00015;
public static final double BOUNDARY_NOISE_AMPLITUDE = 0.15;
public static final double SURFACE_SELECTOR_SCALE = 0.00033;
public static final double OCEAN_NOISE_SCALE = 0.0011;
public static final double OCEAN_THRESHOLD = 0.30;
public static final double MOISTURE_SCALE = 0.0009;
public static final double MOISTURE_THRESHOLD = 0.55;
// ------------------------------------------------------------------
// Underground tunables (3-zone model)
// ------------------------------------------------------------------
public static final int DEEP_ZONE_TOP = -30;
public static final int MID_CAVE_TOP = 0;
public static final double CAVE_SCALE = 0.003;
public static final double CAVE_THRESHOLD = 0.38;
public static final double DEEP_DARK_SCALE = 0.0011;
public static final double DEEP_DARK_SCALE_Y = 0.012;
public static final double DEEP_DARK_THRESHOLD = 0.55;
// ------------------------------------------------------------------
// Spawn safe zone
// ------------------------------------------------------------------
public static final int SPAWN_SAFE_RADIUS = 540;
public static final double SPAWN_SELECTOR_SCALE = 0.004;
/** Underground vertical zone, mirroring {@link LatitudeBiomeSource#applyCaveOverrides}. */
public enum Zone {
/** {@code Y < DEEP_ZONE_TOP} — deep zone, legendary Deep Dark pockets. */
DEEP,
/** {@code DEEP_ZONE_TOP <= Y < MID_CAVE_TOP} — mid caves, lush/dripstone pockets. */
MID_CAVE,
/** {@code Y >= MID_CAVE_TOP} — no cave override; latitude biome as-is. */
SURFACE
}
/** Band the column falls into based on its Y coordinate. Pure function. */
public static Zone zoneForY(int blockY) {
if (blockY < DEEP_ZONE_TOP) return Zone.DEEP;
if (blockY < MID_CAVE_TOP) return Zone.MID_CAVE;
return Zone.SURFACE;
}
/**
* Raw latitude temperature contribution from the Z coordinate, before the boundary
* wobble is applied. {@code blockZ / TEMPERATURE_SCALE} clamped to {@code [-1, 1]}.
* Pure function.
*/
public static double rawLatitudeTemperature(int blockZ) {
return clamp(blockZ / TEMPERATURE_SCALE, -1.0, 1.0);
}
/**
* Combine the raw latitude temperature with a precomputed boundary wobble (the output of
* {@code boundaryNoise.noise(...) * BOUNDARY_NOISE_AMPLITUDE}), then clamp to the valid
* temperature range. Pure function.
*/
public static double temperature(int blockZ, double boundaryWobble) {
return clamp(blockZ / TEMPERATURE_SCALE + boundaryWobble, -1.0, 1.0);
}
/** True if the (blockX, blockZ) column is inside the guaranteed safe spawn square. Pure. */
public static boolean isInSpawnSafeZone(int blockX, int blockZ) {
return Math.abs(blockX) < SPAWN_SAFE_RADIUS && Math.abs(blockZ) < SPAWN_SAFE_RADIUS;
}
/**
* Normalised selector value in {@code [0, 1]} from two noise samples, mirroring the
* dual-octave flattening used by {@link LatitudeBiomeSource#pickBiome}.
*
* @param n1 first octave noise value (any range ImprovedNoise produces)
* @param n2 second octave noise value (at a different offset)
* @return a value in {@code [0, 1]}
*/
public static double normalisedSelector(double n1, double n2) {
double combined = (n1 + n2 * 0.5) / 1.5;
return clamp(combined * 0.5 + 0.5, 0.0, 1.0);
}
/**
* Map a normalised selector value in {@code [0, 1]} to a list index, clamped so it can
* never leave {@code [0, size-1]}. Mirrors the index math in {@code pickBiome}.
* Pure. {@code size} must be {@code >= 1}.
*/
public static int selectorIndex(double normalised, int size) {
if (size <= 0) {
throw new IllegalArgumentException("size must be >= 1, was " + size);
}
int idx = (int) (normalised * size);
if (idx >= size) idx = size - 1;
if (idx < 0) idx = 0;
return idx;
}
/**
* Is the deep-Dark override active for this column, given the precomputed deep-dark noise
* value (output of {@code deepDarkNoise.noise(...)})? Only meaningful in the DEEP zone;
* callers should gate on {@link #zoneForY} first. Pure.
*/
public static boolean isDeepDarkPocket(double deepDarkNoise) {
return deepDarkNoise > DEEP_DARK_THRESHOLD;
}
/** Is the mid-cave lush/dripstone pocket override active for this column? Pure. */
public static boolean isMidCavePocket(double caveNoise) {
return caveNoise > CAVE_THRESHOLD;
}
private static double clamp(double value, double min, double max) {
return value < min ? min : (value > max ? max : value);
}
}
@@ -0,0 +1,62 @@
package net.mcreator.customoregen.worldgen;
import com.mojang.serialization.MapCodec;
import net.minecraft.util.KeyDispatchDataCodec;
import net.minecraft.world.level.levelgen.DensityFunction;
/**
* A custom {@link DensityFunction} that exposes the world's <b>latitude</b> as a Z-only signal,
* something the vanilla density-function vocabulary cannot do (only {@code y} is reachable, via
* {@code y_clamped_gradient}).
*
* <p>The value is {@code clamp(blockZ / TEMPERATURE_SCALE, -1, 1)}. {@code -1} = far north
* (cold), {@code 0} = equator (temperate), {@code +1} = far south (hot). It feeds the
* {@code custom_ore_gen:latitude_temperature} density function, which replaces the vanilla
* temperature in the Lithosphere noise settings so that biome selection follows latitude.</p>
*
* <p>Registered as density-function type {@code custom_ore_gen:latitude_signal}; reference it from a
* density-function JSON as {@code { "type": "custom_ore_gen:latitude_signal" }}.</p>
*/
public record LatitudeSignalDensityFunction() implements DensityFunction.SimpleFunction {
/**
* Number of blocks for the temperature to go from 0 (equator) to &plusmn;1 (pole).
*
* <p>Tuned to match the ore-zone thresholds (default &plusmn;8000): at Z=8000 the latitude is
* 0.5, which is the vanilla boundary between temperate and warm climates, so hot ores
* (copper/gold/redstone) line up with hot-looking biomes. Lush caves (temperature ~0.7)
* therefore appear around Z=11000, just inside the hot ore zone.</p>
*/
private static final double TEMPERATURE_SCALE = 16000.0;
/** MapCodec (no fields): a single canonical instance, round-trips as a bare type tag. */
public static final MapCodec<LatitudeSignalDensityFunction> DATA_CODEC =
MapCodec.unit(new LatitudeSignalDensityFunction());
/** {@link DensityFunction#codec()} wrapper the framework expects. */
public static final KeyDispatchDataCodec<LatitudeSignalDensityFunction> CODEC =
KeyDispatchDataCodec.of(DATA_CODEC);
@Override
public double compute(DensityFunction.FunctionContext context) {
double latitude = context.blockZ() / TEMPERATURE_SCALE;
if (latitude < -1.0) return -1.0;
if (latitude > 1.0) return 1.0;
return latitude;
}
@Override
public double minValue() {
return -1.0;
}
@Override
public double maxValue() {
return 1.0;
}
@Override
public KeyDispatchDataCodec<? extends DensityFunction> codec() {
return CODEC;
}
}
@@ -1,118 +0,0 @@
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. Kept inside the BiomeSource */
/** SPAWN_SAFE_RADIUS (540) so the safe-square guarantee applies to everything we find. */
private static final int MAX_RADIUS = 512;
/** Step between sampled positions. */
private static final int STEP = 8;
/** Surface Y used for biome probing - must match LatitudeGameTest.SURFACE_Y semantics. */
private static final int SURFACE_Y = 64;
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;
}
// Probe biomes at the SURFACE (y=64), not at y=0. Underground the latitude biome extends
// downwards by design, so probing at y=0 could match a SPAWN_BIOMES that differs from what
// the player actually sees on the surface. The BiomeSource spawn-safe square covers the
// first 540 blocks around the origin, so probing at surface y and within that square reliably
// lands on plains/forest - never dark_forest (which is only in the temperate surface pool,
// used outside the safe square).
BlockPos current = levelData.getSpawnPos();
if (isSpawnBiomeAtSurface(level, current.getX(), current.getZ())) {
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 isSpawnBiomeAtSurface(ServerLevel level, int x, int z) {
Holder<Biome> biome = level.getBiome(new BlockPos(x, SURFACE_Y, z));
return biome.unwrapKey().map(SPAWN_BIOMES::contains).orElse(false);
}
private static BlockPos findSpawnBiome(ServerLevel level) {
// Concentric square rings starting from the origin. We probe at SURFACE_Y because the
// latitude biome at y=0 can legitimately differ from the surface biome.
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;
}
if (isSpawnBiomeAtSurface(level, x, z)) {
return new BlockPos(x, 0, z);
}
}
}
}
return null;
}
}
@@ -0,0 +1,77 @@
package net.mcreator.customoregen.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.levelgen.placement.PlacementContext;
import net.minecraft.world.level.levelgen.placement.PlacementFilter;
import net.minecraft.world.level.levelgen.placement.PlacementModifierType;
/**
* Placement modifier that filters ore placement by Z coordinate (latitude zone).
*
* <p>When the player's Z position is in the configured zone, the ore is placed; otherwise it is
* skipped. This replaces the biome-tag-based approach and works universally with any biome mod
* (BOP, Terralith, etc.) without needing to classify every biome manually.</p>
*
* <p>Three zones exist:
* <ul>
* <li>{@code COLD}: Z &lt; {@code coldThreshold} (negative Z, north)</li>
* <li>{@code HOT}: Z &gt; {@code hotThreshold} (positive Z, south)</li>
* <li>{@code TEMPERATE}: between the two thresholds</li>
* </ul>
*
* <p>Registered as placement-modifier type {@code custom_ore_gen:latitude_zone}.</p>
*/
public class LatitudeZonePlacement extends PlacementFilter {
public enum Zone {
COLD("cold"),
HOT("hot"),
TEMPERATE("temperate");
private final String serializedName;
Zone(String name) { this.serializedName = name; }
public String getSerializedName() { return serializedName; }
public static Zone byName(String name) {
for (Zone z : values()) {
if (z.serializedName.equals(name)) return z;
}
return TEMPERATE;
}
}
public static final MapCodec<LatitudeZonePlacement> CODEC = RecordCodecBuilder.mapCodec(
instance -> instance.group(
com.mojang.serialization.Codec.STRING.fieldOf("zone")
.xmap(Zone::byName, Zone::getSerializedName)
.forGetter(p -> p.zone)
).apply(instance, instance.stable(LatitudeZonePlacement::new))
);
private final Zone zone;
public LatitudeZonePlacement(Zone zone) {
this.zone = zone;
}
@Override
protected boolean shouldPlace(PlacementContext context, RandomSource random, BlockPos pos) {
int z = pos.getZ();
int coldZ = LatitudeConfig.getColdZoneZ();
int hotZ = LatitudeConfig.getHotZoneZ();
return switch (zone) {
case COLD -> z < coldZ;
case HOT -> z > hotZ;
case TEMPERATE -> z >= coldZ && z <= hotZ;
};
}
@Override
public PlacementModifierType<?> type() {
return WorldGenRegistration.LATITUDE_ZONE_PLACEMENT.value();
}
}
@@ -1,134 +0,0 @@
package net.mcreator.customoregen.worldgen;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.mcreator.customoregen.CustomOreGenMod;
import net.mcreator.customoregen.init.CustomOreGenModBlocks;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Temporary diagnostic: only active when a {@code .oreaudit} marker file exists in the
* server working directory (create it to run the audit, delete it afterwards). On server
* start it forces real chunk generation in a grid around the origin, counts custom ore blocks
* per surface biome, writes {@code ore_audit_report.txt}, and then stops the server.
*
* <p>Warning: this handler calls {@code System.exit(0)} at the end of the audit. It is
* therefore a developer-only utility that must <b>never</b> be enabled in production. The
* equivalent, non-destructive proof that ores place in every biome is covered by
* {@link LatitudeGameTest} (run with {@code ./gradlew runGameTestServer}).</p>
*/
@EventBusSubscriber
public class OreAuditHandler {
private static final int CHUNKS_PER_AXIS = 12; // 12x12 chunks = 192x192 blocks
private static final int Y_MIN = -8;
private static final int Y_MAX = 32;
@SubscribeEvent
public static void onServerStarted(ServerStartedEvent event) {
Path flag = Path.of(".oreaudit");
if (!java.nio.file.Files.exists(flag)) {
return;
}
MinecraftServer server = event.getServer();
new Thread(() -> runAudit(server), "ore-audit").start();
}
private static void runAudit(MinecraftServer server) {
try {
Thread.sleep(2000); // let the server settle
ServerLevel overworld = server.overworld();
// Pre-generate chunks in a grid so features are placed.
CustomOreGenMod.LOGGER.info("[ore-audit] Generating {}x{} chunks...", CHUNKS_PER_AXIS, CHUNKS_PER_AXIS);
for (int cx = 0; cx < CHUNKS_PER_AXIS; cx++) {
for (int cz = 0; cz < CHUNKS_PER_AXIS; cz++) {
overworld.getChunk(cx, cz);
}
}
CustomOreGenMod.LOGGER.info("[ore-audit] Chunks generated, scanning ores...");
// Map: biome -> ore block -> count
Map<String, Map<String, Integer>> byBiome = new LinkedHashMap<>();
int[] shardTotal = {0};
int[] ironTotal = {0};
int[] coalTotal = {0};
Block shardSurface = CustomOreGenModBlocks.SHARDDIAMONDBLOCKORE.get();
Block shardDeep = CustomOreGenModBlocks.DEEPSLATESHARDDIAMONDORE.get();
Block ironSurface = CustomOreGenModBlocks.IRONORE.get();
Block ironDeep = CustomOreGenModBlocks.DEEPSLATEIRONORE.get();
Block coal = CustomOreGenModBlocks.CONCENTRATEDCOALORE.get();
for (int cx = 0; cx < CHUNKS_PER_AXIS; cx++) {
for (int cz = 0; cz < CHUNKS_PER_AXIS; cz++) {
int baseX = cx << 4;
int baseZ = cz << 4;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
Holder<Biome> biomeHolder = overworld.getBiome(new BlockPos(baseX + x, 64, baseZ + z));
String biomeKey = biomeHolder.unwrapKey()
.map(k -> k.location().toString()).orElse("unknown");
Map<String, Integer> ores = byBiome.computeIfAbsent(biomeKey, k -> new LinkedHashMap<>());
for (int y = Y_MIN; y <= Y_MAX; y++) {
Block b = overworld.getBlockState(new BlockPos(baseX + x, y, baseZ + z)).getBlock();
String name = null;
if (b == shardSurface) { name = "shard_diamond"; shardTotal[0]++; }
else if (b == shardDeep) { name = "deepslate_shard_diamond"; shardTotal[0]++; }
else if (b == ironSurface) { name = "iron"; ironTotal[0]++; }
else if (b == ironDeep) { name = "deepslate_iron"; ironTotal[0]++; }
else if (b == coal) { name = "concentrated_coal"; coalTotal[0]++; }
if (name != null) ores.merge(name, 1, Integer::sum);
}
}
}
}
}
StringBuilder r = new StringBuilder();
r.append("=== ORE AUDIT (real chunk generation) ===\n");
r.append(String.format("Scanned %dx%d chunks, Y=%d..%d%n", CHUNKS_PER_AXIS, CHUNKS_PER_AXIS, Y_MIN, Y_MAX));
r.append(String.format("Totals: shard=%d iron=%d coal=%d%n%n", shardTotal[0], ironTotal[0], coalTotal[0]));
r.append("Per biome (only biomes with >=1 ore shown):\n");
for (var e : byBiome.entrySet()) {
if (e.getValue().isEmpty()) continue;
r.append(String.format(" %-40s %s%n", e.getKey(), e.getValue()));
}
r.append("\nBiomes with ZERO ores (sampled):\n");
int zeroCount = 0;
for (var e : byBiome.entrySet()) {
if (e.getValue().isEmpty()) {
r.append(" ").append(e.getKey()).append("\n");
zeroCount++;
}
}
if (zeroCount == 0) r.append(" (none - every sampled biome has ores)\n");
Path out = Path.of("ore_audit_report.txt");
Files.writeString(out, r.toString());
CustomOreGenMod.LOGGER.info("[ore-audit] Report written to {}", out.toAbsolutePath());
CustomOreGenMod.LOGGER.info("[ore-audit] Totals: shard={} iron={} coal={}", shardTotal[0], ironTotal[0], coalTotal[0]);
CustomOreGenMod.LOGGER.info("[ore-audit] Biomes with ores: {}, biomes with zero: {}",
byBiome.values().stream().filter(m -> !m.isEmpty()).count(), zeroCount);
} catch (Throwable t) {
CustomOreGenMod.LOGGER.error("[ore-audit] failed", t);
} finally {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
System.exit(0);
}
}
}
@@ -2,7 +2,8 @@ package net.mcreator.customoregen.worldgen;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.levelgen.DensityFunction;
import net.minecraft.world.level.levelgen.placement.PlacementModifierType;
import net.neoforged.neoforge.common.world.BiomeModifier; import net.neoforged.neoforge.common.world.BiomeModifier;
import net.neoforged.neoforge.registries.DeferredHolder; import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister; import net.neoforged.neoforge.registries.DeferredRegister;
@@ -10,23 +11,24 @@ import net.neoforged.neoforge.registries.NeoForgeRegistries;
/** /**
* Registration of custom worldgen codecs used by the mod. * Registration of custom worldgen codecs used by the mod.
*
* <ul>
* <li>{@code latitude} — the custom latitude {@link BiomeSource} codec
* referenced from world presets.</li>
* <li>{@code config_gated_features} — a {@link BiomeModifier} type that
* only adds ore features when their config toggle is enabled.</li>
* </ul>
*/ */
public class WorldGenRegistration { public class WorldGenRegistration {
public static final DeferredRegister<MapCodec<? extends BiomeSource>> BIOME_SOURCES = /** Custom density-function types. */
DeferredRegister.create(Registries.BIOME_SOURCE, "custom_ore_gen"); public static final DeferredRegister<MapCodec<? extends DensityFunction>> DENSITY_FUNCTION_TYPES =
DeferredRegister.create(Registries.DENSITY_FUNCTION_TYPE, "custom_ore_gen");
public static final DeferredHolder<MapCodec<? extends BiomeSource>, MapCodec<? extends BiomeSource>> LATITUDE = public static final DeferredHolder<MapCodec<? extends DensityFunction>, MapCodec<? extends DensityFunction>> LATITUDE_SIGNAL =
BIOME_SOURCES.register("latitude", () -> LatitudeBiomeSource.CODEC); DENSITY_FUNCTION_TYPES.register("latitude_signal", () -> LatitudeSignalDensityFunction.DATA_CODEC);
/** Serializer registry for config-gated biome modifiers (see {@link ConfigGatedFeaturesModifier}). */ /** Custom placement-modifier types (Z-coordinate-based ore filtering). */
public static final DeferredRegister<PlacementModifierType<?>> PLACEMENT_MODIFIERS =
DeferredRegister.create(Registries.PLACEMENT_MODIFIER_TYPE, "custom_ore_gen");
public static final DeferredHolder<PlacementModifierType<?>, PlacementModifierType<LatitudeZonePlacement>> LATITUDE_ZONE_PLACEMENT =
PLACEMENT_MODIFIERS.register("latitude_zone", () -> () -> LatitudeZonePlacement.CODEC);
/** Serializer registry for config-gated biome modifiers. */
public static final DeferredRegister<MapCodec<? extends BiomeModifier>> BIOME_MODIFIER_SERIALIZERS = public static final DeferredRegister<MapCodec<? extends BiomeModifier>> BIOME_MODIFIER_SERIALIZERS =
DeferredRegister.create(NeoForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS, "custom_ore_gen"); DeferredRegister.create(NeoForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS, "custom_ore_gen");
@@ -23,6 +23,14 @@ description='''${mod_description}'''
# Start of user code block dependencies configuration # Start of user code block dependencies configuration
[[dependencies."${mod_id}"]]
modId="mr_lithosphere"
mandatory=true
versionRange="[1.7,)"
ordering="AFTER"
side="BOTH"
# Note: optional mods (biomesoplenty, create, mekanism) are intentionally NOT declared # Note: optional mods (biomesoplenty, create, mekanism) are intentionally NOT declared
# here. Optional integration is handled via data tags with "required": false entries # here. Optional integration is handled via data tags with "required": false entries
# (see data/custom_ore_gen/tags/worldgen/biome/latitude_*_surface.json) and via # (see data/custom_ore_gen/tags/worldgen/biome/latitude_*_surface.json) and via
@@ -30,3 +38,7 @@ description='''${mod_description}'''
# absent. Declaring them here with mandatory=false + versionRange caused NeoForge to # absent. Declaring them here with mandatory=false + versionRange caused NeoForge to
# fail loading when the mods were not installed (see commit history). # fail loading when the mods were not installed (see commit history).
# End of user code block dependencies configuration # End of user code block dependencies configuration
# Mixin configuration
[[mixins]]
config = "custom_ore_gen.mixins.json"
@@ -0,0 +1,15 @@
{
"required": true,
"minVersion": "0.8.5",
"package": "net.mcreator.customoregen.mixin",
"compatibilityLevel": "JAVA_21",
"refmap": "custom_ore_gen.refmap.json",
"mixins": [
"MultiNoiseBiomeSourceMixin"
],
"client": [],
"server": [],
"injectors": {
"defaultRequire": 1
}
}
@@ -1,20 +1,22 @@
{ {
"type": "custom_ore_gen:config_gated_features", "type": "custom_ore_gen:config_gated_features",
"biomes": "#custom_ore_gen:latitude_cold_surface", "biomes": {
"step": "underground_ores", "type": "neoforge:any"
"gate_groups": [ },
{ "step": "underground_ores",
"toggle": "vanillaOreVariants", "gate_groups": [
"features": [ {
"custom_ore_gen:lapisore", "toggle": "vanillaOreVariants",
"custom_ore_gen:deepslatelapisore" "features": [
] "custom_ore_gen:lapisore",
}, "custom_ore_gen:deepslatelapisore"
{ ]
"toggle": "concentratedOres", },
"features": [ {
"custom_ore_gen:deepslatediamondore" "toggle": "concentratedOres",
] "features": [
} "custom_ore_gen:deepslatediamondore"
] ]
}
]
} }
@@ -1,28 +1,30 @@
{ {
"type": "custom_ore_gen:config_gated_features", "type": "custom_ore_gen:config_gated_features",
"biomes": "#custom_ore_gen:latitude_hot_surface", "biomes": {
"step": "underground_ores", "type": "neoforge:any"
"gate_groups": [ },
{ "step": "underground_ores",
"toggle": "pureGoldenOre", "gate_groups": [
"features": [ {
"custom_ore_gen:puregoldenore", "toggle": "pureGoldenOre",
"custom_ore_gen:deepslatepuregoldenore" "features": [
] "custom_ore_gen:puregoldenore",
}, "custom_ore_gen:deepslatepuregoldenore"
{ ]
"toggle": "customCopperOres", },
"features": [ {
"custom_ore_gen:copperhighore", "toggle": "customCopperOres",
"custom_ore_gen:copperlowerore" "features": [
] "custom_ore_gen:copperhighore",
}, "custom_ore_gen:copperlowerore"
{ ]
"toggle": "vanillaOreVariants", },
"features": [ {
"custom_ore_gen:redstoneore", "toggle": "vanillaOreVariants",
"custom_ore_gen:deepslateredstoneore" "features": [
] "custom_ore_gen:redstoneore",
} "custom_ore_gen:deepslateredstoneore"
] ]
}
]
} }
@@ -1,13 +1,13 @@
{ {
"type": "custom_ore_gen:config_gated_features", "type": "custom_ore_gen:config_gated_features",
"biomes": "#custom_ore_gen:mountain_biomes", "biomes": "#custom_ore_gen:mountain_biomes",
"step": "underground_ores", "step": "underground_ores",
"gate_groups": [ "gate_groups": [
{ {
"toggle": "customEmeraldOres", "toggle": "customEmeraldOres",
"features": [ "features": [
"custom_ore_gen:highemeraldore" "custom_ore_gen:highemeraldore"
] ]
} }
] ]
} }
@@ -1,13 +1,13 @@
{ {
"type": "custom_ore_gen:config_gated_features", "type": "custom_ore_gen:config_gated_features",
"biomes": "#custom_ore_gen:rare_biomes", "biomes": "#custom_ore_gen:rare_biomes",
"step": "underground_ores", "step": "underground_ores",
"gate_groups": [ "gate_groups": [
{ {
"toggle": "customEmeraldOres", "toggle": "customEmeraldOres",
"features": [ "features": [
"custom_ore_gen:loweremeraldore" "custom_ore_gen:loweremeraldore"
] ]
} }
] ]
} }
@@ -1,16 +1,16 @@
{ {
"type": "custom_ore_gen:config_gated_features", "type": "custom_ore_gen:config_gated_features",
"biomes": { "biomes": {
"type": "neoforge:any" "type": "neoforge:any"
}, },
"step": "underground_ores", "step": "underground_ores",
"gate_groups": [ "gate_groups": [
{ {
"toggle": "shardDiamondOre", "toggle": "shardDiamondOre",
"features": [ "features": [
"custom_ore_gen:sharddiamondblockore", "custom_ore_gen:sharddiamondblockore",
"custom_ore_gen:deepslatesharddiamondore" "custom_ore_gen:deepslatesharddiamondore"
] ]
} }
] ]
} }
@@ -1,20 +1,22 @@
{ {
"type": "custom_ore_gen:config_gated_features", "type": "custom_ore_gen:config_gated_features",
"biomes": "#custom_ore_gen:latitude_temperate_surface", "biomes": {
"step": "underground_ores", "type": "neoforge:any"
"gate_groups": [ },
{ "step": "underground_ores",
"toggle": "impureOres", "gate_groups": [
"features": [ {
"custom_ore_gen:ironore", "toggle": "impureOres",
"custom_ore_gen:deepslateironore" "features": [
] "custom_ore_gen:ironore",
}, "custom_ore_gen:deepslateironore"
{ ]
"toggle": "concentratedOres", },
"features": [ {
"custom_ore_gen:concentratedcoalore" "toggle": "concentratedOres",
] "features": [
} "custom_ore_gen:concentratedcoalore"
] ]
}
]
} }
@@ -5,18 +5,21 @@
"minecraft:ore_coal_lower", "minecraft:ore_coal_lower",
"minecraft:ore_coal_upper", "minecraft:ore_coal_upper",
"minecraft:ore_copper_large", "minecraft:ore_copper_large",
"minecraft:ore_diamond_large",
"minecraft:ore_emerald",
"minecraft:ore_gold",
"minecraft:ore_iron_small",
"minecraft:ore_iron_middle",
"minecraft:ore_lapis_buried",
"minecraft:ore_lapis",
"minecraft:ore_redstone",
"minecraft:ore_copper", "minecraft:ore_copper",
"minecraft:ore_diamond", "minecraft:ore_diamond",
"minecraft:ore_diamond_buried",
"minecraft:ore_diamond_large",
"minecraft:ore_diamond_medium",
"minecraft:ore_emerald",
"minecraft:ore_gold",
"minecraft:ore_gold_extra",
"minecraft:ore_gold_lower", "minecraft:ore_gold_lower",
"minecraft:ore_iron_middle",
"minecraft:ore_iron_small",
"minecraft:ore_iron_upper", "minecraft:ore_iron_upper",
"minecraft:ore_lapis",
"minecraft:ore_lapis_buried",
"minecraft:ore_redstone",
"minecraft:ore_redstone_lower" "minecraft:ore_redstone_lower"
], ],
"step": "underground_ores" "step": "underground_ores"
@@ -1,68 +0,0 @@
{
"replace": false,
"values": [
"minecraft:taiga",
"minecraft:frozen_ocean",
"minecraft:frozen_river",
"minecraft:snowy_plains",
"minecraft:snowy_beach",
"minecraft:snowy_taiga",
"minecraft:old_growth_pine_taiga",
"minecraft:grove",
"minecraft:snowy_slopes",
"minecraft:jagged_peaks",
"minecraft:frozen_peaks",
"minecraft:cold_ocean",
"minecraft:deep_cold_ocean",
"minecraft:deep_frozen_ocean",
"minecraft:ice_spikes",
{
"id": "biomesoplenty:auroral_garden",
"required": false
},
{
"id": "biomesoplenty:cold_desert",
"required": false
},
{
"id": "biomesoplenty:maple_woods",
"required": false
},
{
"id": "biomesoplenty:dead_forest",
"required": false
},
{
"id": "biomesoplenty:tundra",
"required": false
},
{
"id": "biomesoplenty:bog",
"required": false
},
{
"id": "biomesoplenty:muskeg",
"required": false
},
{
"id": "biomesoplenty:old_growth_dead_forest",
"required": false
},
{
"id": "biomesoplenty:snowblossom_grove",
"required": false
},
{
"id": "biomesoplenty:snowy_coniferous_forest",
"required": false
},
{
"id": "biomesoplenty:snowy_fir_clearing",
"required": false
},
{
"id": "biomesoplenty:snowy_maple_woods",
"required": false
}
]
}
@@ -1,106 +0,0 @@
{
"replace": false,
"values": [
"minecraft:desert",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:stony_peaks",
"minecraft:warm_ocean",
"minecraft:windswept_savanna",
"minecraft:eroded_badlands",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:badlands",
"minecraft:wooded_badlands",
{
"id": "biomesoplenty:bayou",
"required": false
},
{
"id": "biomesoplenty:dryland",
"required": false
},
{
"id": "biomesoplenty:dune_beach",
"required": false
},
{
"id": "biomesoplenty:mystic_grove",
"required": false
},
{
"id": "biomesoplenty:jade_cliffs",
"required": false
},
{
"id": "biomesoplenty:lavender_field",
"required": false
},
{
"id": "biomesoplenty:lavender_forest",
"required": false
},
{
"id": "biomesoplenty:mediterranean_forest",
"required": false
},
{
"id": "biomesoplenty:orchard",
"required": false
},
{
"id": "biomesoplenty:pasture",
"required": false
},
{
"id": "biomesoplenty:prairie",
"required": false
},
{
"id": "biomesoplenty:lush_desert",
"required": false
},
{
"id": "biomesoplenty:lush_savanna",
"required": false
},
{
"id": "biomesoplenty:fungal_jungle",
"required": false
},
{
"id": "biomesoplenty:floodplain",
"required": false
},
{
"id": "biomesoplenty:rainforest",
"required": false
},
{
"id": "biomesoplenty:rocky_rainforest",
"required": false
},
{
"id": "biomesoplenty:tropics",
"required": false
},
{
"id": "biomesoplenty:volcanic_plains",
"required": false
},
{
"id": "biomesoplenty:volcano",
"required": false
},
{
"id": "biomesoplenty:wasteland",
"required": false
},
{
"id": "biomesoplenty:erupting_inferno",
"required": false
}
]
}
@@ -7,8 +7,6 @@
"minecraft:taiga", "minecraft:taiga",
"minecraft:old_growth_pine_taiga", "minecraft:old_growth_pine_taiga",
"minecraft:old_growth_spruce_taiga", "minecraft:old_growth_spruce_taiga",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:grove", "minecraft:grove",
"minecraft:snowy_slopes", "minecraft:snowy_slopes",
{ {
@@ -60,20 +58,14 @@
"required": false "required": false
}, },
{ {
"id": "biomesoplenty:coniferous_forest", "id": "biomesoplenty:hot_springs",
"required": false "required": false
}, },
{ {
"id": "biomesoplenty:fir_clearing", "id": "biomesoplenty:ominous_woods",
"required": false "required": false
}, },
{ "minecraft:jagged_peaks",
"id": "biomesoplenty:seasonal_forest", "minecraft:frozen_peaks"
"required": false
},
{
"id": "biomesoplenty:seasonal_orchard",
"required": false
}
] ]
} }
@@ -15,14 +15,6 @@
"id": "biomesoplenty:dryland", "id": "biomesoplenty:dryland",
"required": false "required": false
}, },
{
"id": "biomesoplenty:dune_beach",
"required": false
},
{
"id": "biomesoplenty:bayou",
"required": false
},
{ {
"id": "biomesoplenty:fungal_jungle", "id": "biomesoplenty:fungal_jungle",
"required": false "required": false
@@ -48,11 +40,31 @@
"required": false "required": false
}, },
{ {
"id": "biomesoplenty:rocky_shrubland", "id": "biomesoplenty:bayou",
"required": false "required": false
}, },
{ {
"id": "biomesoplenty:jade_cliffs", "id": "biomesoplenty:lush_desert",
"required": false
},
{
"id": "biomesoplenty:lush_savanna",
"required": false
},
{
"id": "biomesoplenty:volcanic_plains",
"required": false
},
{
"id": "biomesoplenty:volcano",
"required": false
},
{
"id": "biomesoplenty:wasteland",
"required": false
},
{
"id": "biomesoplenty:wasteland_steppe",
"required": false "required": false
} }
] ]
@@ -90,6 +90,64 @@
{ {
"id": "biomesoplenty:origin_valley", "id": "biomesoplenty:origin_valley",
"required": false "required": false
},
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
{
"id": "biomesoplenty:seasonal_forest",
"required": false
},
{
"id": "biomesoplenty:seasonal_orchard",
"required": false
},
{
"id": "biomesoplenty:prairie",
"required": false
},
{
"id": "biomesoplenty:coniferous_forest",
"required": false
},
{
"id": "biomesoplenty:fir_clearing",
"required": false
},
{
"id": "biomesoplenty:jade_cliffs",
"required": false
},
{
"id": "biomesoplenty:rocky_shrubland",
"required": false
},
{
"id": "biomesoplenty:aspen_glade",
"required": false
},
{
"id": "biomesoplenty:gravel_beach",
"required": false
},
{
"id": "biomesoplenty:highland",
"required": false
},
{
"id": "biomesoplenty:jacaranda_glade",
"required": false
},
{
"id": "biomesoplenty:overgrown_greens",
"required": false
},
{
"id": "biomesoplenty:pasture",
"required": false
},
{
"id": "biomesoplenty:wintry_origin_valley",
"required": false
} }
] ]
} }
@@ -4,9 +4,6 @@
"minecraft:jagged_peaks", "minecraft:jagged_peaks",
"minecraft:frozen_peaks", "minecraft:frozen_peaks",
"minecraft:stony_peaks", "minecraft:stony_peaks",
"minecraft:savanna_plateau",
"minecraft:wooded_badlands",
"minecraft:meadow",
"minecraft:grove", "minecraft:grove",
"minecraft:snowy_slopes", "minecraft:snowy_slopes",
{ {
@@ -3,7 +3,6 @@
"values": [ "values": [
"minecraft:mushroom_fields", "minecraft:mushroom_fields",
"minecraft:sparse_jungle", "minecraft:sparse_jungle",
"minecraft:savanna_plateau",
"minecraft:sunflower_plains", "minecraft:sunflower_plains",
"minecraft:windswept_gravelly_hills", "minecraft:windswept_gravelly_hills",
"minecraft:flower_forest", "minecraft:flower_forest",
@@ -1,109 +0,0 @@
{
"replace": false,
"values": [
"minecraft:plains",
"minecraft:meadow",
"minecraft:sunflower_plains",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:swamp",
"minecraft:taiga",
"minecraft:old_growth_pine_taiga",
"minecraft:river",
"minecraft:beach",
"minecraft:stony_shore",
"minecraft:ocean",
"minecraft:lukewarm_ocean",
"minecraft:deep_ocean",
"minecraft:deep_lukewarm_ocean",
"minecraft:dripstone_caves",
"minecraft:lush_caves",
{
"id": "biomesoplenty:coniferous_forest",
"required": false
},
{
"id": "biomesoplenty:fir_clearing",
"required": false
},
{
"id": "biomesoplenty:field",
"required": false
},
{
"id": "biomesoplenty:forested_field",
"required": false
},
{
"id": "biomesoplenty:seasonal_forest",
"required": false
},
{
"id": "biomesoplenty:seasonal_orchard",
"required": false
},
{
"id": "biomesoplenty:pumpkin_patch",
"required": false
},
{
"id": "biomesoplenty:grassland",
"required": false
},
{
"id": "biomesoplenty:spider_nest",
"required": false
},
{
"id": "biomesoplenty:moor",
"required": false
},
{
"id": "biomesoplenty:origin_valley",
"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:rocky_shrubland",
"required": false
},
{
"id": "biomesoplenty:scrubland",
"required": false
},
{
"id": "biomesoplenty:woodland",
"required": false
},
{
"id": "biomesoplenty:old_growth_woodland",
"required": false
},
{
"id": "biomesoplenty:marsh",
"required": false
},
{
"id": "biomesoplenty:undergrowth",
"required": false
},
"minecraft:cherry_grove",
"minecraft:windswept_hills",
"minecraft:windswept_forest"
]
}
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:concentratedcoalore" "Name": "minecraft:coal_ore"
} }
} }
] ]
@@ -7,10 +7,19 @@
{ {
"target": { "target": {
"predicate_type": "tag_match", "predicate_type": "tag_match",
"tag": "c:stones" "tag": "minecraft:stone_ore_replaceables"
}, },
"state": { "state": {
"Name": "custom_ore_gen:copperhighore" "Name": "minecraft:copper_ore"
}
},
{
"target": {
"predicate_type": "tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
},
"state": {
"Name": "minecraft:deepslate_copper_ore"
} }
} }
] ]
@@ -7,10 +7,19 @@
{ {
"target": { "target": {
"predicate_type": "tag_match", "predicate_type": "tag_match",
"tag": "c:stones" "tag": "minecraft:stone_ore_replaceables"
}, },
"state": { "state": {
"Name": "custom_ore_gen:copperlowerore" "Name": "minecraft:copper_ore"
}
},
{
"target": {
"predicate_type": "tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
},
"state": {
"Name": "minecraft:deepslate_copper_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:deepslatediamondore" "Name": "minecraft:deepslate_diamond_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:deepslateironore" "Name": "minecraft:deepslate_iron_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:deepslatelapisore" "Name": "minecraft:deepslate_lapis_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:deepslatepuregoldenore" "Name": "minecraft:deepslate_gold_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:deepslateredstoneore" "Name": "minecraft:deepslate_redstone_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:highemeraldore" "Name": "minecraft:emerald_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:ironore" "Name": "minecraft:iron_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:lapisore" "Name": "minecraft:lapis_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:loweremeraldore" "Name": "minecraft:emerald_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:puregoldenore" "Name": "minecraft:gold_ore"
} }
} }
] ]
@@ -10,7 +10,7 @@
"tag": "c:stones" "tag": "c:stones"
}, },
"state": { "state": {
"Name": "custom_ore_gen:redstoneore" "Name": "minecraft:redstone_ore"
} }
} }
] ]
@@ -0,0 +1,3 @@
{
"type": "custom_ore_gen:latitude_signal"
}
@@ -0,0 +1,22 @@
{
"type": "minecraft:clamp",
"input": {
"type": "minecraft:add",
"argument1": "custom_ore_gen:latitude_signal",
"argument2": {
"type": "minecraft:mul",
"argument1": 0.15,
"argument2": {
"type": "minecraft:shifted_noise",
"noise": "minecraft:temperature",
"shift_x": "minecraft:shift_x",
"shift_y": 0.0,
"shift_z": "minecraft:shift_z",
"xz_scale": 0.04,
"y_scale": 0.0
}
}
},
"min": -1.0,
"max": 1.0
}
@@ -3,7 +3,7 @@
"placement": [ "placement": [
{ {
"type": "minecraft:count", "type": "minecraft:count",
"count": 10 "count": 20
}, },
{ {
"type": "minecraft:in_square" "type": "minecraft:in_square"
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "temperate"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "hot"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "hot"
} }
] ]
} }
@@ -3,7 +3,7 @@
"placement": [ "placement": [
{ {
"type": "minecraft:count", "type": "minecraft:count",
"count": 6 "count": 3
}, },
{ {
"type": "minecraft:in_square" "type": "minecraft:in_square"
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "cold"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "temperate"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "cold"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "hot"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "hot"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "temperate"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "cold"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "hot"
} }
] ]
} }
@@ -21,7 +21,8 @@
} }
}, },
{ {
"type": "minecraft:biome" "type": "custom_ore_gen:latitude_zone",
"zone": "hot"
} }
] ]
} }
@@ -1,36 +0,0 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "minecraft:overworld",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "custom_ore_gen:latitude",
"seed": 0
},
"settings": "minecraft:large_biomes"
}
},
"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"
}
}
}
}
@@ -1,11 +0,0 @@
{
"replace": false,
"values": [
"minecraft:normal",
"minecraft:flat",
"minecraft:large_biomes",
"minecraft:amplified",
"minecraft:single_biome_surface",
"custom_ore_gen:ultra_wide_biome"
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,196 +0,0 @@
package net.mcreator.customoregen.worldgen;
import net.minecraft.world.level.biome.Biomes;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Pure unit tests for {@link BiomeBand}, the climate-band logic that drives
* {@link LatitudeBiomeSource}.
*
* <p>These tests run as plain JUnit (no game server required): {@code fromTemperature}
* is a pure function of a double, and the {@code surfaceVanilla}/{@code ocean}/
* {@code surfaceTag} helpers only ever touch {@link net.minecraft.resources.ResourceKey}
* and {@link net.minecraft.tags.TagKey} objects, which are pure value types that
* never consult a live registry. They therefore protect the most important
* behaviour of the latitude system without needing {@code runGameTestServer}.</p>
*/
@DisplayName("BiomeBand climate logic")
class BiomeBandTest {
// ------------------------------------------------------------------
// fromTemperature: boundary conditions
// ------------------------------------------------------------------
@Test
@DisplayName("FROZEN at the extreme cold pole")
void fromTemperature_frozenAtExtremeCold() {
assertEquals(BiomeBand.FROZEN, BiomeBand.fromTemperature(-1.0));
assertEquals(BiomeBand.FROZEN, BiomeBand.fromTemperature(-0.9));
}
@Test
@DisplayName("-0.7 boundary is FROZEN (inclusive: <=)")
void fromTemperature_boundaryMinus07_isFrozenInclusive() {
assertEquals(BiomeBand.FROZEN, BiomeBand.fromTemperature(-0.7));
}
@Test
@DisplayName("just above -0.7 is COLD")
void fromTemperature_justAboveMinus07_isCold() {
assertEquals(BiomeBand.COLD, BiomeBand.fromTemperature(-0.69));
assertEquals(BiomeBand.COLD, BiomeBand.fromTemperature(-0.5));
}
@Test
@DisplayName("-0.25 boundary is COLD (inclusive: <=)")
void fromTemperature_boundaryMinus025_isColdInclusive() {
assertEquals(BiomeBand.COLD, BiomeBand.fromTemperature(-0.25));
}
@Test
@DisplayName("just above -0.25 is TEMPERATE")
void fromTemperature_justAboveMinus025_isTemperate() {
assertEquals(BiomeBand.TEMPERATE, BiomeBand.fromTemperature(-0.24));
assertEquals(BiomeBand.TEMPERATE, BiomeBand.fromTemperature(0.0));
assertEquals(BiomeBand.TEMPERATE, BiomeBand.fromTemperature(0.2499));
}
@Test
@DisplayName("+0.25 boundary is WARM (TEMPERATE uses strict <)")
void fromTemperature_at025_isWarm_boundaryNotInclusive() {
assertEquals(BiomeBand.WARM, BiomeBand.fromTemperature(0.25));
assertEquals(BiomeBand.WARM, BiomeBand.fromTemperature(0.5));
}
@Test
@DisplayName("+0.7 boundary and above is HOT")
void fromTemperature_atAndAbove07_isHot() {
assertEquals(BiomeBand.HOT, BiomeBand.fromTemperature(0.7));
assertEquals(BiomeBand.HOT, BiomeBand.fromTemperature(0.9));
assertEquals(BiomeBand.HOT, BiomeBand.fromTemperature(1.0));
}
@Test
@DisplayName("fromTemperature never returns null across the full range")
void fromTemperature_neverReturnsNull() {
double[] temps = {-1.0, -0.95, -0.71, -0.7, -0.69, -0.5, -0.3, -0.26, -0.25,
-0.24, -0.1, 0.0, 0.1, 0.2499, 0.25, 0.3, 0.5, 0.6999,
0.7, 0.8, 0.95, 1.0};
for (double t : temps) {
assertNotNull(BiomeBand.fromTemperature(t),
"fromTemperature must never return null for t=" + t);
}
}
@Test
@DisplayName("surface temperature 0.0 (equator / spawn) is TEMPERATE")
void fromTemperature_equatorIsTemperate() {
// The spawn safe zone sits around Z=0, i.e. temperature 0 -> TEMPERATE.
assertEquals(BiomeBand.TEMPERATE, BiomeBand.fromTemperature(0.0));
}
// ------------------------------------------------------------------
// Band metadata invariants
// ------------------------------------------------------------------
@Test
@DisplayName("band center temperatures are strictly increasing from pole to pole")
void bandsCenterTemperaturesAreMonotonic() {
BiomeBand[] bands = BiomeBand.values();
for (int i = 1; i < bands.length; i++) {
assertTrue(bands[i - 1].centerTemperature < bands[i].centerTemperature,
"Band centers must be strictly increasing: "
+ bands[i - 1] + " -> " + bands[i]);
}
assertEquals(-1.0, BiomeBand.FROZEN.centerTemperature);
assertEquals(1.0, BiomeBand.HOT.centerTemperature);
assertEquals(5, BiomeBand.values().length, "exactly five climate bands expected");
}
// ------------------------------------------------------------------
// surfaceTag (must point at the mod's own tags so BoP can extend safely)
// ------------------------------------------------------------------
@Test
@DisplayName("every band has a non-null surface tag under the mod namespace")
void surfaceTag_isNotNullAndNamespacedForEveryBand() {
for (BiomeBand band : BiomeBand.values()) {
assertNotNull(band.surfaceTag(), "surfaceTag must not be null for " + band);
assertEquals("custom_ore_gen", band.surfaceTag().location().getNamespace(),
"surface tag namespace must be the mod id for " + band);
}
}
@Test
@DisplayName("cold/frozen bands share the latitude_cold_surface tag")
void surfaceTag_frozenAndColdShareColdSurfaceTag() {
assertEquals(BiomeBand.FROZEN.surfaceTag(),
BiomeBand.COLD.surfaceTag(),
"FROZEN and COLD must pull surface biomes from the same cold tag");
}
@Test
@DisplayName("warm/hot bands share the latitude_hot_surface tag")
void surfaceTag_warmAndHotShareHotSurfaceTag() {
assertEquals(BiomeBand.WARM.surfaceTag(),
BiomeBand.HOT.surfaceTag(),
"WARM and HOT must pull surface biomes from the same hot tag");
}
// ------------------------------------------------------------------
// surfaceVanilla / ocean pools (must be non-empty for every band)
// ------------------------------------------------------------------
@Test
@DisplayName("every band has a non-empty vanilla surface pool")
void surfaceVanilla_isNonEmptyForEveryBand() {
for (BiomeBand band : BiomeBand.values()) {
assertFalse(band.surfaceVanilla().isEmpty(),
"surfaceVanilla pool must not be empty for " + band);
// Note: List.of (used by surfaceVanilla) already guarantees non-null elements by
// construction (it throws NPE if any Biomes.X constant were null), so there is
// no need to assert contains(null) here - and List.of rejects contains(null) too.
}
}
@Test
@DisplayName("every band has a non-empty ocean pool")
void ocean_isNonEmptyForEveryBand() {
for (BiomeBand band : BiomeBand.values()) {
assertFalse(band.ocean().isEmpty(),
"ocean pool must not be empty for " + band);
}
}
@Test
@DisplayName("PLAINS is in the TEMPERATE surface pool (spawn must be buildable)")
void surfaceVanilla_plainsIsInTemperatePool() {
assertTrue(BiomeBand.TEMPERATE.surfaceVanilla().contains(Biomes.PLAINS),
"PLAINS must be in the TEMPERATE surface pool (spawn safe zone relies on it)");
}
@Test
@DisplayName("DESERT is in the HOT surface pool")
void surfaceVanilla_desertIsInHotPool() {
assertTrue(BiomeBand.HOT.surfaceVanilla().contains(Biomes.DESERT),
"DESERT must be in the HOT surface pool");
}
@Test
@DisplayName("deepDark-style cave biomes are NOT surfaced in any band")
void surfaceVanilla_doesNotContainCaveBiomes() {
// Cave biomes are vanilla-only, eager, injected underground by LatitudeBiomeSource.
// They must never be in a surface pool.
for (BiomeBand band : BiomeBand.values()) {
assertFalse(band.surfaceVanilla().contains(Biomes.DEEP_DARK),
"DEEP_DARK must never be in the surface pool of " + band);
assertFalse(band.surfaceVanilla().contains(Biomes.LUSH_CAVES),
"LUSH_CAVES must never be in the surface pool of " + band);
assertFalse(band.surfaceVanilla().contains(Biomes.DRIPSTONE_CAVES),
"DRIPSTONE_CAVES must never be in the surface pool of " + band);
}
}
}
@@ -1,315 +0,0 @@
package net.mcreator.customoregen.worldgen;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Pure (registry-free) unit tests for {@link LatitudeMath}, the single source of truth
* for the latitude world geometry. Because {@link LatitudeBiomeSource} now delegates
* every climate/zone/index calculation here, these tests cover the geometry the BiomeSource
* actually uses at runtime - without needing a loaded biome registry.
*/
@DisplayName("LatitudeMath geometry (temperature, zones, spawn, selection)")
class LatitudeMathTest {
// ------------------------------------------------------------------
// Temperature derivation
// ------------------------------------------------------------------
@Test
@DisplayName("equator (Z=0) has raw temperature 0")
void rawLatitudeTemperature_equatorIsZero() {
assertEquals(0.0, LatitudeMath.rawLatitudeTemperature(0), 1e-9);
}
@Test
@DisplayName("north pole (Z=-16000) clamps to -1, south pole (Z=+16000) to +1")
void rawLatitudeTemperature_polesClamp() {
assertEquals(-1.0, LatitudeMath.rawLatitudeTemperature(-16000), 1e-9);
assertEquals(1.0, LatitudeMath.rawLatitudeTemperature(16000), 1e-9);
}
@Test
@DisplayName("temperature beyond the poles stays clamped to [-1, 1]")
void rawLatitudeTemperature_extremeClamped() {
assertEquals(-1.0, LatitudeMath.rawLatitudeTemperature(-1_000_000), 1e-9);
assertEquals(1.0, LatitudeMath.rawLatitudeTemperature(1_000_000), 1e-9);
}
@Test
@DisplayName("temperature is continuous and monotonic in Z (north=cold, south=hot)")
void rawLatitudeTemperature_monotonic() {
double tNeg = LatitudeMath.rawLatitudeTemperature(-8000);
double tZero = LatitudeMath.rawLatitudeTemperature(0);
double tPos = LatitudeMath.rawLatitudeTemperature(8000);
assertTrue(tNeg < tZero, "north must be colder than equator: " + tNeg + " < " + tZero);
assertTrue(tPos > tZero, "south must be warmer than equator: " + tPos + " > " + tZero);
assertTrue(tNeg < 0, "north must be negative");
assertTrue(tPos > 0, "south must be positive");
}
@Test
@DisplayName("temperature(blockZ, 0 wobble) equals rawLatitudeTemperature(blockZ)")
void temperature_zeroWobbleMatchesRaw() {
for (int z : new int[]{-1_000_000, -32000, -16000, -8000, -1, 0, 1, 8000, 16000, 32000, 1_000_000}) {
assertEquals(LatitudeMath.rawLatitudeTemperature(z), LatitudeMath.temperature(z, 0.0), 1e-9,
"temperature(z, 0) must equal rawLatitudeTemperature(z) at z=" + z);
}
}
@Test
@DisplayName("positive wobble warms, negative wobble cools, both clamp at +/-0.15 amplitude")
void temperature_wobbleDirectionAndClamp() {
// At the equator, full +wobble and -wobble should land exactly at +0.15 / -0.15.
assertEquals(0.15, LatitudeMath.temperature(0, 0.15), 1e-9);
assertEquals(-0.15, LatitudeMath.temperature(0, -0.15), 1e-9);
// At the pole, clamping kicks in: the wobble cannot push past +/-1.
assertEquals(1.0, LatitudeMath.temperature(16000, 0.5), 1e-9);
assertEquals(-1.0, LatitudeMath.temperature(-16000, -0.5), 1e-9);
}
// ------------------------------------------------------------------
// Underground zone model (3 zones)
// ------------------------------------------------------------------
@Test
@DisplayName("zone boundaries: deep < -30, mid -30..-1, surface >= 0")
void zoneForY_boundaries() {
assertEquals(LatitudeMath.Zone.SURFACE, LatitudeMath.zoneForY(0),
"Y=0 is the top of the mid-cave zone -> SURFACE zone (no override)");
assertEquals(LatitudeMath.Zone.SURFACE, LatitudeMath.zoneForY(1));
assertEquals(LatitudeMath.Zone.SURFACE, LatitudeMath.zoneForY(64));
assertEquals(LatitudeMath.Zone.SURFACE, LatitudeMath.zoneForY(320));
assertEquals(LatitudeMath.Zone.MID_CAVE, LatitudeMath.zoneForY(-1),
"Y=-1 is the bottom-most MID_CAVE cell");
assertEquals(LatitudeMath.Zone.MID_CAVE, LatitudeMath.zoneForY(-15));
assertEquals(LatitudeMath.Zone.MID_CAVE, LatitudeMath.zoneForY(-30),
"Y=-30 is the top of the mid-cave zone (inclusive)");
assertEquals(LatitudeMath.Zone.DEEP, LatitudeMath.zoneForY(-31),
"Y=-31 is the first DEEP cell (DEEP_ZONE_TOP is exclusive)");
assertEquals(LatitudeMath.Zone.DEEP, LatitudeMath.zoneForY(-50));
assertEquals(LatitudeMath.Zone.DEEP, LatitudeMath.zoneForY(-64));
}
@Test
@DisplayName("zoneForY covers every Y without gaps or overlaps")
void zoneForY_noGapsOrOverlaps() {
// Walk Y from +5 down to -65 and confirm every cell maps to exactly one zone,
// and the sequence transitions in order SURFACE -> MID_CAVE -> DEEP.
LatitudeMath.Zone prev = null;
int transitions = 0;
for (int y = 5; y >= -65; y--) {
LatitudeMath.Zone z = LatitudeMath.zoneForY(y);
assertNotNull(z, "zone must never be null at y=" + y);
if (prev != null && prev != z) {
transitions++;
// Allowed transitions only in this order.
if (prev == LatitudeMath.Zone.SURFACE) {
assertEquals(LatitudeMath.Zone.MID_CAVE, z, "SURFACE must be followed by MID_CAVE");
} else if (prev == LatitudeMath.Zone.MID_CAVE) {
assertEquals(LatitudeMath.Zone.DEEP, z, "MID_CAVE must be followed by DEEP");
} else {
fail("unexpected transition out of " + prev);
}
}
prev = z;
}
assertEquals(2, transitions, "exactly two zone transitions expected over the Y range");
}
// ------------------------------------------------------------------
// Cave pocket + Deep Dark predicate gates
// ------------------------------------------------------------------
@Test
@DisplayName("isDeepDarkPocket triggers above the threshold only")
void deepDarkPocket_threshold() {
assertFalse(LatitudeMath.isDeepDarkPocket(0.0));
assertFalse(LatitudeMath.isDeepDarkPocket(0.5499));
assertFalse(LatitudeMath.isDeepDarkPocket(0.55),
"Deep Dark threshold is strict >, so exactly 0.55 is NOT a pocket");
assertTrue(LatitudeMath.isDeepDarkPocket(0.55001));
assertTrue(LatitudeMath.isDeepDarkPocket(1.0));
}
@Test
@DisplayName("isMidCavePocket triggers above the threshold only")
void midCavePocket_threshold() {
assertFalse(LatitudeMath.isMidCavePocket(0.0));
assertFalse(LatitudeMath.isMidCavePocket(0.3799));
assertFalse(LatitudeMath.isMidCavePocket(0.38),
"cave threshold is strict >, so exactly 0.38 is NOT a pocket");
assertTrue(LatitudeMath.isMidCavePocket(0.38001));
assertTrue(LatitudeMath.isMidCavePocket(1.0));
}
@Test
@DisplayName("deep-dark pockets are rare (~<=2%) under a uniform noise model")
void deepDarkPocket_approximatelyRare() {
// Under a uniformly distributed noise value in [0,1), the fraction of the deep zone
// that flips to Deep Dark equals (1 - threshold) = 1 - 0.55 = 45%.
// The DEEP_DARK_THRESHOLD is documented as tuned for ~1-2%, which must therefore
// come from the shape of the ImprovedNoise distribution, not from the threshold alone.
// This test pins that contract: the threshold is 0.55, so a uniform sample would give 45%.
// If someone "fixes" rarity by lowering the threshold, the test breaks loudly.
int hits = 0;
int n = 100_000;
for (int i = 0; i < n; i++) {
double v = (i + 0.5) / n; // deterministic uniform grid in (0,1)
if (LatitudeMath.isDeepDarkPocket(v)) hits++;
}
assertEquals(0.45, hits / (double) n, 1e-9,
"threshold 0.55 -> uniform fraction must be 45% (real rarity comes from noise shape)");
}
// ------------------------------------------------------------------
// Spawn safe zone
// ------------------------------------------------------------------
@Test
@DisplayName("origin is inside the spawn safe zone")
void spawnSafeZone_origin() {
assertTrue(LatitudeMath.isInSpawnSafeZone(0, 0));
assertTrue(LatitudeMath.isInSpawnSafeZone(539, 539));
}
@Test
@DisplayName("just outside the safe radius is NOT safe")
void spawnSafeZone_outside() {
assertFalse(LatitudeMath.isInSpawnSafeZone(540, 0),
"x=540 equals radius, must be outside (strict <)");
assertFalse(LatitudeMath.isInSpawnSafeZone(0, 540));
assertFalse(LatitudeMath.isInSpawnSafeZone(-540, -540));
assertFalse(LatitudeMath.isInSpawnSafeZone(1000, 1000));
}
@Test
@DisplayName("spawn safe zone is a square symmetric around the origin")
void spawnSafeZone_symmetricSquare() {
for (int x : new int[]{0, 50, 539, -50, -539}) {
assertEquals(LatitudeMath.isInSpawnSafeZone(x, 50),
LatitudeMath.isInSpawnSafeZone(-x, 50),
"safe zone must be X-symmetric at x=" + x);
}
for (int z : new int[]{0, 50, 539, -50, -539}) {
assertEquals(LatitudeMath.isInSpawnSafeZone(50, z),
LatitudeMath.isInSpawnSafeZone(50, -z),
"safe zone must be Z-symmetric at z=" + z);
}
}
@Test
@DisplayName("spawn safe zone edge count matches a 2*radius x 2*radius square (exclusive)")
void spawnSafeZone_edgeCount() {
// SPAWN_SAFE_RADIUS = 540, strict <, so the square is [-539, 539] inclusive on both axes.
int count = 0;
for (int x = -545; x <= 545; x++) {
for (int z = -545; z <= 545; z++) {
if (LatitudeMath.isInSpawnSafeZone(x, z)) count++;
}
}
assertEquals(2 * 539 + 1, (int) Math.sqrt(count),
"safe zone side length must be 2*(540-1)+1 = 1079");
assertEquals(1079 * 1079, count);
}
// ------------------------------------------------------------------
// Biome selector index math (dual-octave flattening)
// ------------------------------------------------------------------
@Test
@DisplayName("normalisedSelector always lands in [0, 1]")
void normalisedSelector_range() {
// ImprovedNoise output is roughly in [-1, 1] but can overshoot a little; the normaliser
// must clamp no matter the input.
double[] extremeInputs = {-10, -2, -1, -0.5, 0, 0.5, 1, 2, 10, 1e6, -1e6};
for (double n1 : extremeInputs) {
for (double n2 : extremeInputs) {
double v = LatitudeMath.normalisedSelector(n1, n2);
assertTrue(v >= 0.0 && v <= 1.0,
"normalisedSelector must be in [0,1] for n1=" + n1 + " n2=" + n2 + " got " + v);
}
}
}
@Test
@DisplayName("normalisedSelector is symmetric around 0 (n and -n map to complements)")
void normalisedSelector_symmetric() {
// (n1,n2) and (-n1,-n2) must map to v and (1-v).
for (double n1 : new double[]{-0.8, -0.3, 0.0, 0.3, 0.8}) {
for (double n2 : new double[]{-0.8, -0.3, 0.0, 0.3, 0.8}) {
double v = LatitudeMath.normalisedSelector(n1, n2);
double vNeg = LatitudeMath.normalisedSelector(-n1, -n2);
assertEquals(1.0, v + vNeg, 1e-9,
"v + (-v) must equal 1 for n1=" + n1 + " n2=" + n2);
}
}
}
@Test
@DisplayName("selectorIndex never goes out of bounds, even for normalised=1.0")
void selectorIndex_bounds() {
// size=1 (degenerate): every input must map to index 0.
assertEquals(0, LatitudeMath.selectorIndex(0.0, 1));
assertEquals(0, LatitudeMath.selectorIndex(0.5, 1));
assertEquals(0, LatitudeMath.selectorIndex(1.0, 1));
// size=4: index in [0,3], and normalised=1.0 must clamp to last, not 4.
for (double v = 0.0; v <= 1.0001; v += 0.001) {
int idx = LatitudeMath.selectorIndex(v, 4);
assertTrue(idx >= 0 && idx <= 3,
"index out of [0,3] for v=" + v + ": " + idx);
}
assertEquals(0, LatitudeMath.selectorIndex(0.0, 4));
assertEquals(3, LatitudeMath.selectorIndex(1.0, 4));
assertEquals(3, LatitudeMath.selectorIndex(1e9, 4), "huge input must clamp to last");
assertEquals(0, LatitudeMath.selectorIndex(-1e9, 4), "huge negative input must clamp to 0");
}
@Test
@DisplayName("selectorIndex is monotonically non-decreasing in normalised value")
void selectorIndex_monotonic() {
int prev = -1;
for (double v = 0.0; v <= 1.0; v += 0.0001) {
int idx = LatitudeMath.selectorIndex(v, 8);
assertTrue(idx >= prev, "selectorIndex must be non-decreasing; decreased at v=" + v);
prev = idx;
}
assertEquals(7, prev, "last normalised value must map to last index");
}
@Test
@DisplayName("selectorIndex rejects size <= 0")
void selectorIndex_rejectsNonPositiveSize() {
assertThrows(IllegalArgumentException.class, () -> LatitudeMath.selectorIndex(0.5, 0));
assertThrows(IllegalArgumentException.class, () -> LatitudeMath.selectorIndex(0.5, -1));
}
@Test
@DisplayName("selectorIndex distributes roughly uniformly (no single index dominates)")
void selectorIndex_uniformDistribution() {
// The whole point of the dual-octave flattening is that the index distribution is
// near-uniform; if it's badly skewed, one biome will be over-represented.
// ImprovedNoise roughly outputs a bell curve in [-1,1], so we sample that.
int size = 5;
int[] counts = new int[size];
int n = 200_000;
// Synthetic bell-curve-ish input: sum of cosines to mimic the (n1 + n2*0.5)/1.5 shape.
for (int i = 0; i < n; i++) {
double t = (i + 0.5) / n * Math.PI;
double n1 = Math.cos(t * 7.0);
double n2 = Math.cos(t * 13.0 + 0.3);
counts[LatitudeMath.selectorIndex(LatitudeMath.normalisedSelector(n1, n2), size)]++;
}
// No biome should take more than 40% and none less than 8% (~uniform target is 20%).
for (int c : counts) {
double ratio = c / (double) n;
assertTrue(ratio < 0.40, "a biome index is over-represented: " + ratio);
assertTrue(ratio > 0.08, "a biome index is under-represented: " + ratio);
}
}
}