test+fix: deep generation tests + bug cleanup (licence, docs, deps)
Two cohesive passes that harden the latitude worldgen and clean up
long-standing inconsistencies.
== Bug fixes & doc alignment ==
- licence: set MIT in gradle.properties and neoforge.mods.toml
(was "Not specified" - blocks distribution)
- OreAuditHandler: fix javadoc that referenced a non-existent system
property; the trigger is the .oreaudit marker file. Add an explicit
warning about the System.exit(0) and point to LatitudeGameTest as the
non-destructive alternative
- KubeJS: remove the dead kubejs_version, the commented dependency and
all README claims about an "automatic KubeJS script"/KubeJSIntegration
class that no longer exists. Vanilla ore removal is now native via the
neoforge:remove_features biome modifier
- README: refresh the technical header (1.21.1 / NeoForge 21.1.219 /
Java 21 / v3.2, was 1.20.1/Forge/Java17/v2.1.5) and the shard-diamond
surface note (both variants are generated, gated by shardDiamondOre)
- CLAUDE.md / CONFIG_INTEGRATION_GUIDE.md: mark the feature toggles and
tool stats as wired (they read ModConfigs at runtime); correct the
"hardcoded tools" claim
== Generation tests (56 unit + 6 GameTest, all green) ==
- Extract the pure geometry of LatitudeBiomeSource into LatitudeMath
(temperature, 3-zone underground model, spawn safe zone, dual-octave
selector index). LatitudeBiomeSource now delegates to it, so one
source of truth drives both runtime and tests - no behaviour drift
- LatitudeMathTest (21): temperature clamp/monotonicity, zone boundaries
without gaps, Deep Dark / cave threshold predicates, spawn-safe square
symmetry, selector index bounds + near-uniform distribution
- BiomeBandTest (18): fromTemperature boundary cases, surface/ocean
pool non-empty, cave biomes never in a surface pool
- ModConfigsTest (+2 guards): every ConfigHelper toggle string maps to
a real FeatureToggleConfig field, so a typo cannot silently produce a
dead toggle (default: return true)
- LatitudeGameTest (+4 server tests): determinism (1485 pts, 0 mismatch),
no cave biome at surface (160k samples, 0 leak), surface continuity
(7% transitions - large biomes), climate gradient (north=cold 100%,
south=hot 100%, equator cold 0%)
== Build ==
- build.gradle: addModdingDependenciesTo(sourceSets.test) so pure unit
tests can reference Minecraft types without a full game server
- neoforge.mods.toml: optional BOP/create/mekanism dependencies removed;
declared with mandatory=false + versionRange="[0,)" they broke mod
loading when the mods were absent ("requires X 0 or above"). Optional
integration is already handled via data tags (required:false) and
data-only recipes that no-op if the mod is missing
- gradlew: restore executable bit
Verification: ./gradlew test build -> BUILD SUCCESSFUL, 56/56 unit tests
./gradlew runGameTestServer -> 6/6 GameTests passed in 840ms
This commit is contained in:
@@ -59,63 +59,63 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Number of blocks for the temperature to go from 0 (equator) to +-1 (pole). */
|
||||
private static final double TEMPERATURE_SCALE = 16000.0;
|
||||
private static final double TEMPERATURE_SCALE = LatitudeMath.TEMPERATURE_SCALE;
|
||||
|
||||
/** Noise frequency for the climate band boundary wobble. */
|
||||
private static final double BOUNDARY_NOISE_SCALE = 0.00015;
|
||||
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 = 0.15;
|
||||
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 = 0.00033;
|
||||
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 = 0.0011;
|
||||
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 = 0.30;
|
||||
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 = 0.0009;
|
||||
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 = 0.55;
|
||||
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 = -30;
|
||||
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 = 0;
|
||||
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 = 0.003;
|
||||
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 = 0.38;
|
||||
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 = 0.0011;
|
||||
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 = 0.012;
|
||||
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 = 0.55;
|
||||
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 = 96;
|
||||
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 = 0.02;
|
||||
private static final double SPAWN_SELECTOR_SCALE = LatitudeMath.SPAWN_SELECTOR_SCALE;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Fields
|
||||
@@ -231,7 +231,7 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
// Guaranteed safe spawn zone (open, buildable biomes) around the origin.
|
||||
if (spawnSafeBiomes != null && Math.abs(blockX) < SPAWN_SAFE_RADIUS && Math.abs(blockZ) < SPAWN_SAFE_RADIUS) {
|
||||
if (spawnSafeBiomes != null && LatitudeMath.isInSpawnSafeZone(blockX, blockZ)) {
|
||||
return pickBiome(spawnSafeBiomes, blockX, blockZ, SPAWN_SELECTOR_SCALE);
|
||||
}
|
||||
|
||||
@@ -278,11 +278,12 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
* </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 (blockY < DEEP_ZONE_TOP) {
|
||||
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 (dd > DEEP_DARK_THRESHOLD) {
|
||||
if (LatitudeMath.isDeepDarkPocket(dd)) {
|
||||
return deepDarkBiome;
|
||||
}
|
||||
}
|
||||
@@ -290,11 +291,11 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
// Mid caves: rare lush/dripstone pockets by climate.
|
||||
if (blockY < MID_CAVE_TOP) {
|
||||
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 (cave > CAVE_THRESHOLD) {
|
||||
if (LatitudeMath.isMidCavePocket(cave)) {
|
||||
return caveBiome;
|
||||
}
|
||||
}
|
||||
@@ -341,11 +342,8 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
// second octave at an offset flattens the curve so no biome is disproportionately common.
|
||||
double n1 = selectorNoise.noise(blockX * scale, blockZ * scale, 1000.0);
|
||||
double n2 = selectorNoise.noise(blockX * scale * 1.9 + 137.0, blockZ * scale * 1.9 - 211.0, 2000.0);
|
||||
double combined = (n1 + n2 * 0.5) / 1.5;
|
||||
double normalized = combined * 0.5 + 0.5;
|
||||
int idx = (int) (normalized * biomes.size());
|
||||
if (idx >= biomes.size()) idx = biomes.size() - 1;
|
||||
if (idx < 0) idx = 0;
|
||||
double normalized = LatitudeMath.normalisedSelector(n1, n2);
|
||||
int idx = LatitudeMath.selectorIndex(normalized, biomes.size());
|
||||
return biomes.get(idx);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,188 @@ public class LatitudeGameTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -327,4 +509,10 @@ public class LatitudeGameTest {
|
||||
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 + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
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 = 96;
|
||||
public static final double SPAWN_SELECTOR_SCALE = 0.02;
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,15 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Temporary diagnostic: only active when the system property
|
||||
* {@code customoregen.oreaudit} is set. On server start it forces real chunk
|
||||
* generation in a grid around the origin, counts custom ore blocks per surface
|
||||
* biome, writes a report, and stops the server. Used to prove (without manual
|
||||
* in-game testing) that ores actually place in the terrain of every biome,
|
||||
* including Biomes O' Plenty biomes, under the latitude world type.
|
||||
* 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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
modLoader="javafml"
|
||||
loaderVersion="[1,)"
|
||||
license="Not specified"
|
||||
license="MIT"
|
||||
|
||||
[[mods]]
|
||||
modId="${mod_id}"
|
||||
@@ -23,4 +23,10 @@ description='''${mod_description}'''
|
||||
|
||||
|
||||
# Start of user code block dependencies configuration
|
||||
# Note: optional mods (biomesoplenty, create, mekanism) are intentionally NOT declared
|
||||
# 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
|
||||
# data-only recipes (data/create, data/mekanism) which simply no-op if the mod is
|
||||
# absent. Declaring them here with mandatory=false + versionRange caused NeoForge to
|
||||
# fail loading when the mods were not installed (see commit history).
|
||||
# End of user code block dependencies configuration
|
||||
|
||||
Reference in New Issue
Block a user