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 00000000..47bd88ff
Binary files /dev/null and b/src/main/resources/data/custom_ore_gen/structure/empty_1x1.nbt differ