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:
@@ -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!** 🌍⛏️✨
|
||||||
@@ -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)
|
||||||
@@ -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 iron–diamond 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
@@ -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 ≈ 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 < 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 ≤ Y < 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 < {@value #DEEP_ZONE_TOP}): legendary Deep Dark pockets (~1%), else surface biome.</li>
|
|
||||||
* <li>Mid caves (DEEP_ZONE_TOP ≤ Y < {@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 < 0 for lush/dripstone, Y < -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 ±1 (pole).
|
||||||
|
*
|
||||||
|
* <p>Tuned to match the ore-zone thresholds (default ±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 < {@code coldThreshold} (negative Z, north)</li>
|
||||||
|
* <li>{@code HOT}: Z > {@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
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"type": "custom_ore_gen:config_gated_features",
|
"type": "custom_ore_gen:config_gated_features",
|
||||||
"biomes": "#custom_ore_gen:latitude_cold_surface",
|
"biomes": {
|
||||||
|
"type": "neoforge:any"
|
||||||
|
},
|
||||||
"step": "underground_ores",
|
"step": "underground_ores",
|
||||||
"gate_groups": [
|
"gate_groups": [
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"type": "custom_ore_gen:config_gated_features",
|
"type": "custom_ore_gen:config_gated_features",
|
||||||
"biomes": "#custom_ore_gen:latitude_hot_surface",
|
"biomes": {
|
||||||
|
"type": "neoforge:any"
|
||||||
|
},
|
||||||
"step": "underground_ores",
|
"step": "underground_ores",
|
||||||
"gate_groups": [
|
"gate_groups": [
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"type": "custom_ore_gen:config_gated_features",
|
"type": "custom_ore_gen:config_gated_features",
|
||||||
"biomes": "#custom_ore_gen:latitude_temperate_surface",
|
"biomes": {
|
||||||
|
"type": "neoforge:any"
|
||||||
|
},
|
||||||
"step": "underground_ores",
|
"step": "underground_ores",
|
||||||
"gate_groups": [
|
"gate_groups": [
|
||||||
{
|
{
|
||||||
|
|||||||
+11
-8
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+4
-12
@@ -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
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+22
-10
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+58
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:concentratedcoalore"
|
"Name": "minecraft:coal_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+11
-2
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+11
-2
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:deepslatediamondore"
|
"Name": "minecraft:deepslate_diamond_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:deepslateironore"
|
"Name": "minecraft:deepslate_iron_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:deepslatelapisore"
|
"Name": "minecraft:deepslate_lapis_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:deepslatepuregoldenore"
|
"Name": "minecraft:deepslate_gold_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:deepslateredstoneore"
|
"Name": "minecraft:deepslate_redstone_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"tag": "c:stones"
|
"tag": "c:stones"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"Name": "custom_ore_gen:loweremeraldore"
|
"Name": "minecraft:emerald_ore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
}
|
||||||
+22
@@ -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
-2
@@ -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
-2
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+2
-1
@@ -21,7 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "minecraft:biome"
|
"type": "custom_ore_gen:latitude_zone",
|
||||||
|
"zone": "temperate"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+2
-1
@@ -21,7 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "minecraft:biome"
|
"type": "custom_ore_gen:latitude_zone",
|
||||||
|
"zone": "cold"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+2
-1
@@ -21,7 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "minecraft:biome"
|
"type": "custom_ore_gen:latitude_zone",
|
||||||
|
"zone": "hot"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+2
-1
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user