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
This commit is contained in:
feldenr
2026-06-15 21:07:43 +02:00
parent 7729eeb65c
commit 169a447f7b
3 changed files with 185 additions and 0 deletions
@@ -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.
*
* <p>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.</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");
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);
col(0x6B4E3A, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
}
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());
}
}
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);
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, 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);
}
}
ImageIO.write(img, "png", OUT_DIR.resolve("latitude_map.png").toFile());
ResourceKey<Biome> 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.<ResourceKey<Biome>, 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<ResourceKey<Biome>, 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<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 Set<ResourceKey<Biome>> 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<ResourceKey<Biome>> 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<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);
}
// 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);
}
}