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:
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user