From 169a447f7be16d23fa77e78c73ec2b238e53ab32 Mon Sep 17 00:00:00 2001 From: feldenr <135638674+feldenr@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:07:43 +0200 Subject: [PATCH] test: add automated latitude validation via GameTest server Run `./gradlew runGameTestServer` to validate the latitude biome system without manual in-game testing. Boots a headless game server (full biome registry), samples a 32000x32000 grid through LatitudeBiomeSource, renders a PNG map and writes a distribution report, then asserts climate invariants: - spawn on a safe biome (plains/forest) - FROZEN band dominated by cold/frozen biomes (>60%) - HOT band dominated by warm/hot biomes (>60%) - swamp remains rare in the temperate band (<15%) Outputs land in run/gametest-results/latitude/ (latitude_map.png + latitude_report.txt). Sampling is BiomeSource-only (no chunk generation), so it runs in ~35s regardless of installed mods. - LatitudeGameTest: @GameTestHolder + @PrefixGameTestTemplate(false) - structure/empty_1x1.nbt: minimal 1x1x1 air structure required by GameTest - build.gradle: add gameTestServer run configuration --- build.gradle | 5 + .../worldgen/LatitudeGameTest.java | 180 ++++++++++++++++++ .../custom_ore_gen/structure/empty_1x1.nbt | Bin 0 -> 123 bytes 3 files changed, 185 insertions(+) create mode 100644 src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java create mode 100644 src/main/resources/data/custom_ore_gen/structure/empty_1x1.nbt diff --git a/build.gradle b/build.gradle index 84f84a51..6079a85c 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,11 @@ neoForge { systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } + gameTestServer { + type = 'gameTestServer' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + data { data() programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() diff --git a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java new file mode 100644 index 00000000..3952809e --- /dev/null +++ b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java @@ -0,0 +1,180 @@ +package net.mcreator.customoregen.worldgen; + +import net.mcreator.customoregen.CustomOreGenMod; +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. + * + *

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.

+ */ +@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"); + + private static final Map, 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); + col(0x6B4E3A, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK); + } + + private static void col(int rgb, ResourceKey... keys) { + for (ResourceKey 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()); + } + } + + private static void runLatitudeValidation(GameTestHelper helper) throws Exception { + ServerLevel level = helper.getLevel(); + HolderGetter getter = level.registryAccess().lookupOrThrow(Registries.BIOME); + LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter); + + Files.createDirectories(OUT_DIR); + int dim = (2 * RADIUS) / STEP + 1; + BufferedImage img = new BufferedImage(dim, dim, BufferedImage.TYPE_INT_RGB); + Map, Integer> global = new HashMap<>(); + Map, 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 holder = source.getNoiseBiome(blockX >> 2, 64 >> 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); + } + } + + ImageIO.write(img, "png", OUT_DIR.resolve("latitude_map.png").toFile()); + + ResourceKey spawnKey = source.getNoiseBiome(0, 64 >> 2, 0, null).unwrapKey().orElse(null); + + 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(); + global.entrySet().stream() + .sorted(Map.Entry., Integer>comparingByValue().reversed()) + .forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total))); + Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString()); + + // ---- Assertions ---- + assertNotNull(spawnKey, "spawn biome resolved"); + assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey); + + double north = bandRatio(perBand.get(BiomeBand.FROZEN), frozenColdKeys()); + assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen, was %.1f%%".formatted(north)); + + double south = bandRatio(perBand.get(BiomeBand.HOT), hotWarmKeys()); + assertTrue(south > 60.0, "HOT band should be >60%% warm/hot, 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)); + } + + private static double bandRatio(Map, Integer> m, Set> expected) { + int total = m.values().stream().mapToInt(Integer::intValue).sum(); + if (total == 0) return 0; + int match = 0; + for (ResourceKey k : expected) match += m.getOrDefault(k, 0); + return 100.0 * match / total; + } + + private static Set> frozenColdKeys() { + return new HashSet<>(List.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)); + } + + private static Set> hotWarmKeys() { + return new HashSet<>(List.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)); + } + + private static boolean isSafeSpawn(ResourceKey key) { + return Set.of(Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS, Biomes.FOREST, Biomes.BIRCH_FOREST, + Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE).contains(key); + } + + // 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); + } +} diff --git a/src/main/resources/data/custom_ore_gen/structure/empty_1x1.nbt b/src/main/resources/data/custom_ore_gen/structure/empty_1x1.nbt new file mode 100644 index 0000000000000000000000000000000000000000..47bd88ffd891779de8c34131577f5bf9ec1187bd GIT binary patch literal 123 zcmV->0EGV^iwFqALNICq|7C4(ba`Jfcrh++VsrpCi7^U;Ko9^YDA7|S|4D6aX}^&b z7WI_7t@8SCNCu`Drh|%p6qi|3wll&j2kLBJttb;7e*?`WySi&vvZbEnP=>>zE=g8@ dizNcBMmIOj6avrBelFWx`~Z%7{6KO5006s+GED#g literal 0 HcmV?d00001