diff --git a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java index 9f947aa4..32059803 100644 --- a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java +++ b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java @@ -95,8 +95,8 @@ public class LatitudeBiomeSource extends BiomeSource { /** Frequency of the mid-cave pocket noise (lush/dripstone). */ private static final double CAVE_SCALE = 0.003; - /** Above this value a lush/dripstone pocket overrides the surface biome. ~8% of the mid zone. */ - private static final double CAVE_THRESHOLD = 0.55; + /** 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; /** Frequency of the Deep Dark noise (very low = large, rare regions). */ private static final double DEEP_DARK_SCALE = 0.0011; @@ -104,8 +104,8 @@ public class LatitudeBiomeSource extends BiomeSource { /** Vertical frequency of the Deep Dark noise (keeps it coherent in tall sections). */ private static final double DEEP_DARK_SCALE_Y = 0.012; - /** Above this value the Deep Dark overrides. Tuned for ~1% of the deep zone (legendary). */ - private static final double DEEP_DARK_THRESHOLD = 0.88; + /** 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; // ------------------------------------------------------------------ // Spawn safe zone diff --git a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java index 1923c9d5..5caae0df 100644 --- a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java +++ b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java @@ -29,9 +29,9 @@ import java.util.Set; * {@link GameTest} so it executes inside a real game server with the full biome registry. * *

Launch with {@code ./gradlew runGameTestServer}. The test samples a large grid through a - * freshly built {@link LatitudeBiomeSource}, renders a PNG map, writes a distribution report and - * asserts the climate invariants (frozen north, hot south, safe spawn, rare swamp). No manual - * in-game testing required.

+ * 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.

*/ @GameTestHolder(CustomOreGenMod.MODID) @PrefixGameTestTemplate(false) @@ -42,6 +42,11 @@ public class LatitudeGameTest { 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, Integer> COLORS = new HashMap<>(); static { @@ -61,7 +66,10 @@ public class LatitudeGameTest { Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE, Biomes.WARM_OCEAN); col(0x5B7A3A, Biomes.SWAMP); col(0x3A5A3A, Biomes.MANGROVE_SWAMP); - col(0x6B4E3A, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK); + // 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... keys) { @@ -85,6 +93,85 @@ public class LatitudeGameTest { 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 spawnKey = source.getNoiseBiome(0, SURFACE_Y >> 2, 0, null).unwrapKey().orElse(null); + + Set> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag()); + Set> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag()); + Set> coldOceans = new HashSet<>(List.of( + Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN, + Biomes.FROZEN_RIVER)); + Set> 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> 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> 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, Integer> global = new HashMap<>(); @@ -95,89 +182,31 @@ public class LatitudeGameTest { for (int xi = 0; xi < dim; xi++) { int blockX = -RADIUS + xi * STEP; int blockZ = -RADIUS + zi * STEP; - Holder holder = source.getNoiseBiome(blockX >> 2, 64 >> 2, blockZ >> 2, null); + Holder holder = source.getNoiseBiome(blockX >> 2, blockY >> 2, blockZ >> 2, null); ResourceKey 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); + img.setRGB(xi, dim - 1 - zi, rgb); // flip Z so north is at the top } } - - ImageIO.write(img, "png", OUT_DIR.resolve("latitude_map.png").toFile()); - - ResourceKey spawnKey = source.getNoiseBiome(0, 64 >> 2, 0, null).unwrapKey().orElse(null); - - // Resolve the three surface tags so assertions can use tag membership (the source of - // truth) instead of a hardcoded vanilla-only biome list. This correctly counts BOP biomes. - Set> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag()); - Set> temperateTag = resolveTagKeys(getter, BiomeBand.TEMPERATE.surfaceTag()); - Set> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag()); - Set> coldOceans = new HashSet<>(List.of( - Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN, - Biomes.FROZEN_RIVER)); - Set> hotOceans = new HashSet<>(List.of( - Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN)); - - 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"); - int total = global.values().stream().mapToInt(Integer::intValue).sum(); - r.append("Global biome distribution (top 20):\n"); - global.entrySet().stream() - .sorted(Map.Entry., Integer>comparingByValue().reversed()) - .limit(20) - .forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total))); - - // Per-band breakdown for diagnostics. - for (BiomeBand band : BiomeBand.values()) { - Map, Integer> m = perBand.get(band); - int bt = m.values().stream().mapToInt(Integer::intValue).sum(); - if (bt == 0) continue; - r.append("\nBand ").append(band).append(" (").append(bt).append(" samples):\n"); - m.entrySet().stream() - .sorted(Map.Entry., Integer>comparingByValue().reversed()) - .limit(8) - .forEach(e -> r.append(String.format(" %-43s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / bt))); - } - Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString()); - - // ---- Assertions (tag-based: correctly counts vanilla + BOP biomes) ---- - assertNotNull(spawnKey, "spawn biome resolved"); - assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey); - - Set> northExpected = new HashSet<>(); - northExpected.addAll(coldTag); - northExpected.addAll(coldOceans); - Set> undergroundCold = new HashSet<>(List.of( - Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK)); - northExpected.addAll(undergroundCold); - double north = bandRatio(perBand.get(BiomeBand.FROZEN), northExpected); - assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen (by tag), was %.1f%%".formatted(north)); - - Set> southExpected = new HashSet<>(); - southExpected.addAll(hotTag); - southExpected.addAll(hotOceans); - southExpected.add(Biomes.MANGROVE_SWAMP); - double south = bandRatio(perBand.get(BiomeBand.HOT), southExpected); - assertTrue(south > 60.0, "HOT band should be >60%% warm/hot (by tag), was %.1f%%".formatted(south)); - - Map, Integer> temperate = perBand.get(BiomeBand.TEMPERATE); - int temperateTotal = temperate.values().stream().mapToInt(Integer::intValue).sum(); - int temperateSwamp = temperate.getOrDefault(Biomes.SWAMP, 0); - double swampRatio = 100.0 * temperateSwamp / temperateTotal; - assertTrue(swampRatio < 15.0, - "swamp must be <15%% of temperate band, was %.2f%%".formatted(swampRatio)); - - // Log a one-line summary so it shows up in the game test output. - CustomOreGenMod.LOGGER.info(String.format( - "Latitude game test OK: spawn=%s | north=%.0f%% | south=%.0f%% | swamp=%.2f%%", - spawnKey, north, south, swampRatio)); + ImageIO.write(img, "png", OUT_DIR.resolve(pngName).toFile()); + return new Slice(global, perBand); } - private static double bandRatio(Map, Integer> m, Set> expected) { + private static void appendTop(StringBuilder r, Map, Integer> counts, int limit) { + int total = counts.values().stream().mapToInt(Integer::intValue).sum(); + counts.entrySet().stream() + .sorted(Map.Entry., 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> expected) { + Map, Integer> m = slice.perBand.get(band); int total = m.values().stream().mapToInt(Integer::intValue).sum(); if (total == 0) return 0; int match = 0; @@ -185,6 +214,23 @@ public class LatitudeGameTest { return 100.0 * match / total; } + /** Ratio of one or more biomes across a whole slice. */ + private static double biomeRatio(Slice slice, ResourceKey... keys) { + int total = slice.global.values().stream().mapToInt(Integer::intValue).sum(); + if (total == 0) return 0; + int match = 0; + for (ResourceKey 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 key) { + Map, 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> resolveTagKeys(HolderGetter getter, net.minecraft.tags.TagKey tag) { Set> keys = new HashSet<>(); @@ -197,6 +243,11 @@ public class LatitudeGameTest { Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE).contains(key); } + /** A sampled depth slice: global counts + per-band counts. */ + private record Slice(Map, Integer> global, + Map, 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);