From e478d926dcd764e05fa0f286fe23276ea17b2b18 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sat, 29 Aug 2026 10:52:12 +0100 Subject: [PATCH 1/6] Fix dynamic registry biome rule matching --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 + README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 8 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 144 +++++++++++++-- .../forge/tags/worldgen/biome/is_cold.json | 6 + .../worldgen/BiomeTypeCompatibility.java | 168 ++++++++++++++++++ .../mc/orespawn/worldgen/GeomeConfig.java | 50 +++++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/OreSpawnOreGeneration.java | 47 +++-- .../worldgen/WorldGeologyProfileManager.java | 4 + .../worldgen/GeomeTransitionTest.java | 17 ++ .../worldgen/OreSpawnOreGenerationTest.java | 40 +++++ 16 files changed, 460 insertions(+), 57 deletions(-) create mode 100644 src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json create mode 100644 src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf489061..e0e83ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.10.118021.jar - build/libs/OreSpawn-4.0.10.118021-sources.jar - build/libs/OreSpawn-4.0.10.118021-javadoc.jar + build/libs/OreSpawn-4.0.11.118021.jar + build/libs/OreSpawn-4.0.11.118021-sources.jar + build/libs/OreSpawn-4.0.11.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 868f0318..0c2ee7a4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.11.118021 + +* Preserve biome-dictionary geome weights when a data-driven biome is reached + through its stable registry key rather than the object baked at startup. +* Apply ore biome include and exclude filters by stable registry key so + dynamic-registry biome instances with the same ID are treated consistently. +* Existing chunks and profile formats are unchanged; the corrections apply to + generation in affected provider biomes. + Version 4.0.10.118021 * Evaluate Stable Layers rock min_y and max_y bounds against actual world Y diff --git a/README.md b/README.md index f2ee7be0..8261f345 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.10.118021`: the OreSpawn 4.0.10 +This branch builds target-qualified version `4.0.11.118021`: the OreSpawn 4.0.11 feature set for Minecraft 1.18.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 9ac28828..dee5ede8 100644 --- a/build.gradle +++ b/build.gradle @@ -745,7 +745,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.10.118021' + if (project.mod_version != '4.0.11.118021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.18.2' || project.forge_version != '40.3.0' @@ -759,9 +759,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.10.118021.jar', - 'OreSpawn-4.0.10.118021-sources.jar', - 'OreSpawn-4.0.10.118021-javadoc.jar' + 'OreSpawn-4.0.11.118021.jar', + 'OreSpawn-4.0.11.118021-sources.jar', + 'OreSpawn-4.0.11.118021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -778,7 +778,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.10.118021')) { + if (!file(path).getText('UTF-8').contains('4.0.11.118021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index e2c89497..b1429e85 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -56,7 +56,7 @@ Examples: | 1.15.2 | Forge | `115021` | `4.0.9.115021` | | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | -| 1.18.2 | Forge | `118021` | `4.0.10.118021` | +| 1.18.2 | Forge | `118021` | `4.0.11.118021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -151,8 +151,10 @@ Bug number, such as Forge 1.14.4's `4.0.8.114041`, even though the 4.0.7 repair was not applicable there. Forge 1.15.2, 1.16.5, and 1.17.1 then advanced to their target-qualified 4.0.9 releases for the provider terrain-host ordering repair. Forge 1.18.2 then advanced to `4.0.10.118021` for the distinct Stable -Layers actual-height eligibility repair. A branch may therefore legitimately -skip functional version numbers. +Layers actual-height eligibility repair, and to `4.0.11.118021` to retain +biome-dictionary weights and ore biome filters for dynamic-registry biome +instances. A branch may therefore legitimately skip functional version +numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 861b7e4f..22b22d53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20220404.173914 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.10.118021 +mod_version=4.0.11.118021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java index c3377a88..a6b3fa1e 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -96,6 +96,8 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation BIOME_A = new ResourceLocation(MODID + ":surface_a"); private static final ResourceLocation BIOME_B = new ResourceLocation(MODID + ":surface_b"); private static final ResourceLocation PROBE_GEOME = new ResourceLocation(MODID + ":dynamic_biome_geome"); + private static final ResourceLocation PROBE_GEOME_ALTERNATIVE = + new ResourceLocation(MODID + ":dynamic_biome_geome_alternative"); private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water"); private static final Block[] NATURAL_SOURCES = { Blocks.DIRT, Blocks.GRASS_BLOCK, Blocks.COARSE_DIRT, Blocks.PODZOL, @@ -204,19 +206,34 @@ private static void addDynamicBiomeGeology(WorldgenProvider.Builder provider) { provider.geome(PROBE_GEOME, geome -> geome .baseWeight(0.0D) .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); + provider.geome(PROBE_GEOME_ALTERNATIVE, geome -> geome + .baseWeight(0.0D) + .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); provider.rock(new ResourceLocation(MODID + ":rock/dynamic_biome"), blockId(Blocks.CALCITE), GeologyFamily.SEDIMENTARY, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); rock.geomeWeight(PROBE_GEOME, 1.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 0.0D); + for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); + }); + provider.rock(new ResourceLocation(MODID + ":rock/dynamic_biome_alternative"), blockId(Blocks.BASALT), + GeologyFamily.SEDIMENTARY, rock -> { + rock.dimensions(java.util.Collections.singleton(OPEN_ID)); + rock.geomeWeight(PROBE_GEOME, 0.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 1.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); }); - provider.rock(new ResourceLocation(MODID + ":rock/fallback"), blockId(Blocks.BASALT), + provider.rock(new ResourceLocation(MODID + ":rock/fallback"), blockId(Blocks.DEEPSLATE), GeologyFamily.SEDIMENTARY, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); rock.geomeWeight(PROBE_GEOME, 0.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 0.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 1.0D); }); - provider.biome(BIOME_A, java.util.Collections.singletonMap(PROBE_GEOME, 100.0D)); + Map biomeAWeights = new LinkedHashMap<>(); + biomeAWeights.put(PROBE_GEOME, 6.0D); + biomeAWeights.put(PROBE_GEOME_ALTERNATIVE, 14.0D); + provider.biome(BIOME_A, biomeAWeights); provider.biome(BIOME_B, java.util.Collections.singletonMap(PROBE_GEOME, 100.0D)); } @@ -231,6 +248,19 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) { } try { root.addProperty("place_fluid_deposits", true); + root.addProperty("place_ores", true); + JsonObject dictionary = root.getAsJsonObject("biome_dictionary"); + if (dictionary == null) { + dictionary = new JsonObject(); + root.add("biome_dictionary", dictionary); + } + JsonObject cold = dictionary.getAsJsonObject("COLD"); + if (cold == null) { + cold = new JsonObject(); + dictionary.add("COLD", cold); + } + cold.addProperty(PROBE_GEOME.toString(), 8.0D); + addDynamicBiomeOre(root); JsonObject terrain = root.getAsJsonObject("terrain_dimensions"); if (terrain == null) { terrain = new JsonObject(); @@ -260,6 +290,49 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) { } } + private static void addDynamicBiomeOre(JsonObject root) { + JsonObject ores = root.getAsJsonObject("ores"); + if (ores == null) { + ores = new JsonObject(); + root.add("ores", ores); + } + JsonObject ore = new JsonObject(); + ore.addProperty("block", blockId(Blocks.DIAMOND_BLOCK).toString()); + ore.addProperty("enabled", true); + ore.addProperty("native_generation", false); + ore.addProperty("suppress_vanilla", false); + ore.addProperty("retrogen", false); + JsonObject dimensions = new JsonObject(); + JsonObject end = new JsonObject(); + end.addProperty("enabled", true); + end.addProperty("min_y", 16); + end.addProperty("max_y", 48); + end.addProperty("frequency", 16.0D); + end.addProperty("quantity", 8); + end.addProperty("pattern", "cluster"); + end.addProperty("height_distribution", "uniform"); + end.addProperty("discard_chance_on_air_exposure", 0.0D); + end.addProperty("spread", 4); + end.addProperty("vertical_spread", 3); + end.addProperty("node_size", 3); + end.add("host_families", new JsonArray()); + JsonArray hosts = new JsonArray(); + hosts.add(blockId(Blocks.CALCITE).toString()); + hosts.add(blockId(Blocks.BASALT).toString()); + end.add("host_blocks", hosts); + end.add("host_tags", new JsonArray()); + end.add("geomes", new JsonObject()); + JsonArray biomes = new JsonArray(); + biomes.add(BIOME_A.toString()); + end.add("biome_ids", biomes); + end.add("excluded_biome_ids", new JsonArray()); + end.add("biome_dictionary", new JsonArray()); + end.add("excluded_biome_dictionary", new JsonArray()); + dimensions.add(OPEN_ID.toString(), end); + ore.add("dimensions", dimensions); + ores.add(MODID + ":ore/dynamic_biome_filter", ore); + } + private static void addPalette(WorldgenProvider.Builder provider, String name, ResourceLocation dimension, boolean ceiling) { BiomeSurfaceDefinition surfaceA = surface(DyeColor.PINK, DyeColor.WHITE, @@ -368,6 +441,8 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { long underwaterPockets = 0L; long rawBedrock = 0L; long rawBlockEntities = 0L; + long dictionaryPrimary = 0L; + long dictionaryAlternative = 0L; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -377,6 +452,10 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { LevelChunk chunk = level.getChunk(chunkX, chunkZ); int chunkMinX = chunkX << 4; int chunkMinZ = chunkZ << 4; + int centerGroundY = findMarkedGround(chunk, pos, chunkMinX + 8, chunkMinZ + 8, + level.getMinBuildHeight(), level.getMaxBuildHeight()); + ResourceLocation generationBiomeId = biomeId(level.getBiome( + pos.set(chunkMinX + 8, centerGroundY, chunkMinZ + 8))); for (int localZ = 0; localZ < 16; localZ++) { for (int localX = 0; localX < 16; localX++) { int x = chunkMinX + localX; @@ -408,8 +487,15 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } if (!roofed) { for (int depth = 6; depth <= 8; depth++) { - assertBlock(chunk, pos, x, groundY - depth, z, - Blocks.CALCITE.defaultBlockState(), "dynamic-biome geome rock"); + BlockState geologyState = chunk.getBlockState(pos.set(x, groundY - depth, z)); + if (geologyState.is(Blocks.BASALT)) { + dictionaryAlternative++; + } else if (geologyState.is(Blocks.CALCITE)) { + dictionaryPrimary++; + } else { + throw new IllegalStateException("Unexpected dynamic-biome geome rock at " + + pos + " in " + biomeId + ": " + geologyState); + } geology++; } } @@ -426,11 +512,8 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } } } - ResourceLocation centerBiome = biomeId(level.getBiome(pos.set(chunkMinX + 8, - findMarkedGround(chunk, pos, chunkMinX + 8, chunkMinZ + 8, - level.getMinBuildHeight(), level.getMaxBuildHeight()), chunkMinZ + 8))); - if (previousChunkBiome != null && !previousChunkBiome.equals(centerBiome)) edgeChanges++; - previousChunkBiome = centerBiome; + if (previousChunkBiome != null && !previousChunkBiome.equals(generationBiomeId)) edgeChanges++; + previousChunkBiome = generationBiomeId; sentinels += auditSentinels(level, chunk, pos, chunkMinX, chunkMinZ); if (!roofed) { NaturalSourceAudit natural = auditNaturalSources(level, chunk, pos, @@ -446,6 +529,7 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } } + long dynamicBiomeOre = roofed ? 0L : auditDynamicBiomeOre(level); if (top != EXPECTED_COLUMNS - 9 || underwater != 9 || filler != EXPECTED_FILLER || biomeA == 0 || biomeB == 0 || edgeChanges == 0 || sentinels != 9 * 4 || geology != (roofed ? 0 : EXPECTED_FILLER) @@ -454,7 +538,9 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { || structureNaturalSources != EXPECTED_NATURAL_SOURCES || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES || cavePockets != 54 || underwaterPockets != 63 - || rawBedrock != 9 || rawBlockEntities != 9))) { + || rawBedrock != 9 || rawBlockEntities != 9 + || dictionaryPrimary != EXPECTED_FILLER || dictionaryAlternative != 0 + || dynamicBiomeOre == 0))) { throw new IllegalStateException("Incomplete surface audit for " + level.dimension().location() + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edgeChanges @@ -466,13 +552,38 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { + ", cavePockets=" + cavePockets + ", underwaterPockets=" + underwaterPockets + ", rawBedrock=" + rawBedrock - + ", rawBlockEntities=" + rawBlockEntities); + + ", rawBlockEntities=" + rawBlockEntities + + ", dictionaryPrimary=" + dictionaryPrimary + + ", dictionaryAlternative=" + dictionaryAlternative + + ", dynamicBiomeOre=" + dynamicBiomeOre); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); return new AuditResult(top, underwater, filler, geology, ceiling, roofTop, biomeA, biomeB, edgeChanges, sentinels, aquiferFluid, rawNaturalSources, structureNaturalSources, vegetationNaturalSources, - cavePockets, underwaterPockets, rawBedrock, rawBlockEntities); + cavePockets, underwaterPockets, rawBedrock, rawBlockEntities, + dictionaryPrimary, dictionaryAlternative, dynamicBiomeOre); + } + + private static long auditDynamicBiomeOre(ServerLevel level) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + long count = 0L; + for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { + for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) { + LevelChunk chunk = level.getChunk(chunkX, chunkZ); + for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) { + for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) { + for (int y = 16; y <= 48; y++) { + if (chunk.getBlockState(pos.set(x, y, z)).is(Blocks.DIAMOND_BLOCK)) count++; + } + } + } + } + } + if (count == 0L) { + throw new IllegalStateException("Dynamic-registry biome filter produced no managed ore"); + } + return count; } private static NaturalSourceAudit auditNaturalSources(ServerLevel level, LevelChunk chunk, @@ -489,7 +600,8 @@ private static NaturalSourceAudit auditNaturalSources(ServerLevel level, LevelCh int z = naturalZ(minZ, index); int groundY = findMarkedGround(chunk, pos, x, z, level.getMinBuildHeight(), level.getMaxBuildHeight()); - if (chunk.getBlockState(pos.set(x, groundY - 12, z)).is(Blocks.CALCITE)) rawConverted++; + BlockState converted = chunk.getBlockState(pos.set(x, groundY - 12, z)); + if (converted.is(Blocks.CALCITE) || converted.is(Blocks.BASALT)) rawConverted++; Block pocket = chunk.getBlockState(pos.set(x, groundY - 11, z)).getBlock(); if (index < NATURAL_SOURCES.length / 2) { if (pocket == Blocks.AIR) cavePreserved++; @@ -663,6 +775,9 @@ private static Properties properties(long seed, Map results values.setProperty(prefix + "underwater_pockets", Long.toString(result.underwaterPockets())); values.setProperty(prefix + "raw_bedrock", Long.toString(result.rawBedrock())); values.setProperty(prefix + "raw_block_entities", Long.toString(result.rawBlockEntities())); + values.setProperty(prefix + "dictionary_primary", Long.toString(result.dictionaryPrimary())); + values.setProperty(prefix + "dictionary_alternative", Long.toString(result.dictionaryAlternative())); + values.setProperty(prefix + "dynamic_biome_ore", Long.toString(result.dynamicBiomeOre())); } return values; } @@ -867,5 +982,6 @@ private record AuditResult(long top, long underwater, long filler, long geology, int edgeChanges, int sentinels, long aquiferFluid, long rawNaturalSources, long structureNaturalSources, long vegetationNaturalSources, long cavePockets, long underwaterPockets, - long rawBedrock, long rawBlockEntities) { } + long rawBedrock, long rawBlockEntities, + long dictionaryPrimary, long dictionaryAlternative, long dynamicBiomeOre) { } } diff --git a/src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json b/src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json new file mode 100644 index 00000000..0c73c984 --- /dev/null +++ b/src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "surfaceprobe:surface_a" + ] +} diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java new file mode 100644 index 00000000..1ba1da92 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java @@ -0,0 +1,168 @@ +package zone.moddev.mc.orespawn.worldgen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.Bootstrap; +import net.minecraft.tags.BiomeTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraftforge.common.BiomeDictionary; +import net.minecraftforge.common.Tags; +import net.minecraftforge.registries.ForgeRegistries; + +/** + * Preserves OreSpawn's legacy biome-dictionary vocabulary while resolving + * world-owned biomes through the active dynamic registry. + */ +final class BiomeTypeCompatibility { + private static volatile Registry activeRegistry; + + private BiomeTypeCompatibility() { + } + + static void useRegistry(Registry registry) { + activeRegistry = registry; + } + + static void clearRegistry() { + activeRegistry = null; + } + + static Set> biomeKeys(String type) { + Registry registry = activeRegistry; + if (registry != null) { + Set> result = new LinkedHashSet<>(); + for (Map.Entry, Biome> entry : registry.entrySet()) { + if (registry.getHolder(entry.getKey()) + .map(holder -> matches(holder, tags(type))).orElse(false) + || legacyHasType(entry.getKey(), type)) { + result.add(entry.getKey()); + } + } + return result; + } + try { + return new LinkedHashSet<>(BiomeDictionary.getBiomes( + BiomeDictionary.Type.getType(type))); + } catch (RuntimeException ignored) { + return Collections.emptySet(); + } + } + + static Set biomeIds(String type) { + Set result = new LinkedHashSet<>(); + for (ResourceKey key : biomeKeys(type)) result.add(key.location()); + return result; + } + + static boolean hasType(ResourceKey key, String type) { + Registry registry = activeRegistry; + if (registry != null) { + return registry.getHolder(key) + .map(holder -> matches(holder, tags(type))).orElse(false) + || legacyHasType(key, type); + } + return legacyHasType(key, type); + } + + private static boolean legacyHasType(ResourceKey key, String type) { + try { + return BiomeDictionary.hasType(key, BiomeDictionary.Type.getType(type)); + } catch (RuntimeException ignored) { + return false; + } + } + + static Biome biome(ResourceLocation id) { + Registry registry = activeRegistry; + if (registry != null) return registry.get(id); + try { + Bootstrap.checkBootstrapCalled(() -> "OreSpawn biome lookup"); + } catch (IllegalArgumentException ignored) { + return null; + } + return ForgeRegistries.BIOMES.getValue(id); + } + + private static List> tags(String type) { + String normalized = type == null ? "" : type.trim().toUpperCase(Locale.ROOT); + List> known = TypeTags.TYPES.get(normalized); + if (known != null) return known; + if (normalized.isEmpty()) return Collections.emptyList(); + return Collections.singletonList(TagKey.create(Registry.BIOME_REGISTRY, + new ResourceLocation("forge", "is_" + normalized.toLowerCase(Locale.ROOT)))); + } + + private static boolean matches(Holder holder, + Collection> tags) { + for (TagKey tag : tags) if (holder.is(tag)) return true; + return false; + } + + private static Map>> types() { + Map>> result = new LinkedHashMap<>(); + add(result, "MOUNTAIN", BiomeTags.IS_MOUNTAIN); + add(result, "HILLS", BiomeTags.IS_HILL); + add(result, "OCEAN", BiomeTags.IS_OCEAN); + add(result, "RIVER", BiomeTags.IS_RIVER); + add(result, "BEACH", Tags.Biomes.IS_BEACH, BiomeTags.IS_BEACH); + add(result, "SANDY", Tags.Biomes.IS_SANDY); + add(result, "DRY", Tags.Biomes.IS_DRY); + add(result, "WET", Tags.Biomes.IS_WET); + add(result, "SWAMP", Tags.Biomes.IS_SWAMP); + add(result, "SNOWY", Tags.Biomes.IS_SNOWY); + add(result, "COLD", Tags.Biomes.IS_COLD); + add(result, "HOT", Tags.Biomes.IS_HOT); + add(result, "MESA", BiomeTags.IS_BADLANDS); + add(result, "FOREST", BiomeTags.IS_FOREST); + add(result, "PLAINS", Tags.Biomes.IS_PLAINS); + add(result, "SAVANNA", Tags.Biomes.IS_SAVANNA); + add(result, "CONIFEROUS", Tags.Biomes.IS_CONIFEROUS, BiomeTags.IS_TAIGA); + add(result, "JUNGLE", BiomeTags.IS_JUNGLE); + add(result, "LUSH", Tags.Biomes.IS_LUSH); + add(result, "MUSHROOM", Tags.Biomes.IS_MUSHROOM); + add(result, "PLATEAU", Tags.Biomes.IS_PLATEAU); + add(result, "PEAK", Tags.Biomes.IS_PEAK); + add(result, "SLOPE", Tags.Biomes.IS_SLOPE); + add(result, "UNDERGROUND", Tags.Biomes.IS_UNDERGROUND); + add(result, "WASTELAND", Tags.Biomes.IS_WASTELAND); + add(result, "WATER", Tags.Biomes.IS_WATER, BiomeTags.IS_OCEAN, + BiomeTags.IS_RIVER); + add(result, "DENSE", Tags.Biomes.IS_DENSE); + add(result, "SPARSE", Tags.Biomes.IS_SPARSE); + add(result, "DEAD", Tags.Biomes.IS_DEAD); + add(result, "MAGICAL", Tags.Biomes.IS_MAGICAL); + add(result, "SPOOKY", Tags.Biomes.IS_SPOOKY); + add(result, "NETHER", BiomeTags.IS_NETHER); + add(result, "END", Tags.Biomes.IS_END); + add(result, "VOID", Tags.Biomes.IS_VOID); + return Collections.unmodifiableMap(result); + } + + private static final class TypeTags { + private static final Map>> TYPES = types(); + + private TypeTags() { + } + } + + @SafeVarargs + private static void add(Map>> target, String name, + TagKey... tags) { + List> values = new ArrayList<>(tags.length); + Collections.addAll(values, tags); + target.put(name, Collections.unmodifiableList(values)); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java index 300dc9f0..bc59251e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java @@ -47,7 +47,6 @@ import net.minecraft.tags.TagKey; import net.minecraft.world.level.Level; import net.minecraft.world.level.biome.Biome; -import net.minecraftforge.common.BiomeDictionary; import net.minecraftforge.registries.ForgeRegistries; import org.apache.logging.log4j.LogManager; @@ -289,7 +288,8 @@ private static BakedGeomeConfig bake(JsonObject root, ResourceLocation dimension return null; } Map biomeWeights = bakeBiomeWeights(geomeIndexes, biomeRules, dictionaryRules); - Map biomeWeightsById = bakeBiomeIdentifierWeights(geomeIndexes, biomeRules); + Map biomeWeightsById = bakeBiomeIdentifierWeights( + geomeIndexes, biomeRules, dictionaryRules); LOGGER.info("Baked OreSpawn geome config for '{}' with {} geomes, {} rock entries, " + "{} resolved biome profiles, {} identifier profiles, and {} formations", @@ -1030,8 +1030,10 @@ private static Map bakeBiomeWeights(Map geomeI if (biomeId != null) { merge(weights, biomeRules.get(biomeId.toString())); ResourceKey biomeKey = ResourceKey.create(Registry.BIOME_REGISTRY, biomeId); - for (BiomeDictionary.Type type : BiomeDictionary.getTypes(biomeKey)) { - merge(weights, dictionaryRules.get(type.getName())); + for (Entry entry : dictionaryRules.entrySet()) { + if (BiomeTypeCompatibility.hasType(biomeKey, entry.getKey())) { + merge(weights, entry.getValue()); + } } } applyBiomeHeuristic(weights, geomeIndexes, biomeId, biome); @@ -1042,22 +1044,52 @@ private static Map bakeBiomeWeights(Map geomeI static Map bakeBiomeIdentifierWeights(Map geomeIndexes, Map biomeRules) { + return bakeBiomeIdentifierWeights(geomeIndexes, biomeRules, Collections.emptyMap()); + } + + static Map bakeBiomeIdentifierWeights(Map geomeIndexes, + Map biomeRules, Map dictionaryRules) { + return bakeBiomeIdentifierWeights(geomeIndexes, biomeRules, dictionaryRules, + BiomeTypeCompatibility::biomeIds); + } + + static Map bakeBiomeIdentifierWeights(Map geomeIndexes, + Map biomeRules, Map dictionaryRules, + java.util.function.Function> dictionaryResolver) { Map result = new LinkedHashMap<>(); for (Entry entry : biomeRules.entrySet()) { try { ResourceLocation biomeId = new ResourceLocation(entry.getKey()); - double[] weights = new double[geomeIndexes.size()]; - Arrays.fill(weights, 1.0D); - merge(weights, entry.getValue()); - applyBiomeHeuristic(weights, geomeIndexes, biomeId, Float.NaN, Float.NaN); - result.put(biomeId, weights); + merge(identifierWeights(result, biomeId, geomeIndexes.size()), entry.getValue()); } catch (RuntimeException e) { LOGGER.warn("Ignoring invalid OreSpawn biome rule ID '{}'", entry.getKey()); } } + for (Entry entry : dictionaryRules.entrySet()) { + for (ResourceLocation biomeId : dictionaryResolver.apply(entry.getKey())) { + merge(identifierWeights(result, biomeId, geomeIndexes.size()), entry.getValue()); + } + } + for (Entry entry : result.entrySet()) { + Biome biome = BiomeTypeCompatibility.biome(entry.getKey()); + if (biome == null) { + applyBiomeHeuristic(entry.getValue(), geomeIndexes, entry.getKey(), Float.NaN, Float.NaN); + } else { + applyBiomeHeuristic(entry.getValue(), geomeIndexes, entry.getKey(), biome); + } + } return result; } + private static double[] identifierWeights(Map result, + ResourceLocation biomeId, int geomeCount) { + return result.computeIfAbsent(biomeId, ignored -> { + double[] weights = new double[geomeCount]; + Arrays.fill(weights, 1.0D); + return weights; + }); + } + private static void applyBiomeHeuristic(double[] weights, Map geomeIndexes, ResourceLocation biomeId, Biome biome) { applyBiomeHeuristic(weights, geomeIndexes, biomeId, diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index a40aeca6..93235f8c 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.10.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.11.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index 56278d73..87519f03 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.10.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.11.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java index e60a4e65..7474e38a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -157,9 +157,10 @@ private static boolean generateChunk(WorldGenLevel world, ChunkAccess chunk, Hol ChunkPos chunkPos = chunk.getPos(); int centerX = chunkPos.getMinBlockX() + 8; int centerZ = chunkPos.getMinBlockZ() + 8; + ResourceKey biomeKey = biome.unwrapKey().orElse(null); int geome = -1; if (Level.OVERWORLD.equals(dimension)) { - ResourceLocation biomeId = biome.unwrapKey().map(ResourceKey::location).orElse(null); + ResourceLocation biomeId = biomeKey == null ? null : biomeKey.location(); geome = classifier(worldSeed).classifyColumn(biome.value(), biomeId, centerX, centerZ, scratch.geomeValues(geomeConfig.geomeCount())); } @@ -167,7 +168,7 @@ private static boolean generateChunk(WorldGenLevel world, ChunkAccess chunk, Hol boolean changed = false; for (BakedOre ore : ores) { if (retrogenOnly && !ore.retrogen) continue; - if (!ore.acceptsBiome(biome.value())) { + if (!ore.acceptsBiome(biomeKey)) { continue; } double frequency = ore.frequency; @@ -439,8 +440,9 @@ private static BakedOre bakeOre(BlockState output, BlockState deepOutput, int de } } } - Set includedBiomes = resolveBiomes(json, "biome_ids", "biome_dictionary"); - Set excludedBiomes = resolveBiomes(json, "excluded_biome_ids", "excluded_biome_dictionary"); + Set> includedBiomes = resolveBiomes(json, "biome_ids", "biome_dictionary"); + Set> excludedBiomes = resolveBiomes(json, + "excluded_biome_ids", "excluded_biome_dictionary"); return new BakedOre(output, deepOutput, deepOutputMaxY, outputs, minY, maxY, Math.min(64.0D, frequency), minQuantity, maxQuantity, pattern, heightDistribution, discardChanceOnAirExposure, @@ -504,23 +506,23 @@ private static void addTags(Map target, JsonElement element, } } - private static Set resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey) { - Set result = Collections.newSetFromMap(new IdentityHashMap()); + static Set> resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey) { + return resolveBiomes(rule, idsKey, dictionaryKey, BiomeTypeCompatibility::biomeKeys); + } + + static Set> resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey, + java.util.function.Function>> dictionaryResolver) { + Set> result = new HashSet<>(); if (rule.has(idsKey) && rule.get(idsKey).isJsonArray()) { for (JsonElement element : rule.getAsJsonArray(idsKey)) { ResourceLocation id = resource(element.getAsString()); - Biome biome = id == null ? null : ForgeRegistries.BIOMES.getValue(id); - if (biome != null) result.add(biome); + if (id != null) result.add(ResourceKey.create(Registry.BIOME_REGISTRY, id)); } } if (rule.has(dictionaryKey) && rule.get(dictionaryKey).isJsonArray()) { for (JsonElement element : rule.getAsJsonArray(dictionaryKey)) { try { - for (ResourceKey key : net.minecraftforge.common.BiomeDictionary.getBiomes( - net.minecraftforge.common.BiomeDictionary.Type.getType(element.getAsString()))) { - Biome biome = ForgeRegistries.BIOMES.getValue(key.location()); - if (biome != null) result.add(biome); - } + result.addAll(dictionaryResolver.apply(element.getAsString())); } catch (RuntimeException ignored) { } } @@ -528,6 +530,13 @@ private static Set resolveBiomes(JsonObject rule, String idsKey, String d return result; } + static boolean acceptsBiome(Set> includedBiomes, + Set> excludedBiomes, ResourceKey biome) { + if (biome == null) return includedBiomes.isEmpty() && excludedBiomes.isEmpty(); + return !excludedBiomes.contains(biome) + && (includedBiomes.isEmpty() || includedBiomes.contains(biome)); + } + private static Set resolveTag(TagKey tag) { Set result = Collections.newSetFromMap(new IdentityHashMap()); for (Block block : ForgeRegistries.BLOCKS.getValues()) { @@ -632,8 +641,8 @@ private static final class BakedOre { final Map hostBlocks; final int familyMask; final double[] geomeWeights; - final Set includedBiomes; - final Set excludedBiomes; + final Set> includedBiomes; + final Set> excludedBiomes; final boolean retrogen; BakedOre(BlockState output, BlockState deepOutput, int deepOutputMaxY, BakedOutput[] outputs, @@ -642,7 +651,8 @@ private static final class BakedOre { double discardChanceOnAirExposure, int spread, int verticalSpread, int nodeSize, Map hostBlocks, int familyMask, double[] geomeWeights, - Set includedBiomes, Set excludedBiomes, boolean retrogen) { + Set> includedBiomes, + Set> excludedBiomes, boolean retrogen) { this.output = output; this.deepOutput = deepOutput; this.deepOutputMaxY = deepOutputMaxY; @@ -690,9 +700,8 @@ boolean accepts(BlockState state, Random random, BakedGeomeConfig config) { && (familyMask & (1 << family.ordinal())) != 0; } - boolean acceptsBiome(Biome biome) { - return !excludedBiomes.contains(biome) - && (includedBiomes.isEmpty() || includedBiomes.contains(biome)); + boolean acceptsBiome(ResourceKey biome) { + return OreSpawnOreGeneration.acceptsBiome(includedBiomes, excludedBiomes, biome); } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java index 820e4b55..bc897ff9 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java @@ -21,6 +21,7 @@ import zone.moddev.mc.orespawn.api.OreSpawnOreIntegration; import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; +import net.minecraft.core.Registry; import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.storage.LevelResource; import net.minecraftforge.event.server.ServerAboutToStartEvent; @@ -117,6 +118,8 @@ public static synchronized boolean reloadActiveProfile() { public static void onServerAboutToStart(ServerAboutToStartEvent event) { activeServer = event.getServer(); + BiomeTypeCompatibility.useRegistry(event.getServer().registryAccess() + .registryOrThrow(Registry.BIOME_REGISTRY)); Path worldRoot = event.getServer().getWorldPath(LevelResource.ROOT).normalize(); Path profilePath = worldRoot.resolve("serverconfig").resolve(PROFILE_FILE_NAME); WorldGeologyProfile fallback = globalProfile(); @@ -172,6 +175,7 @@ public static void onServerAboutToStart(ServerAboutToStartEvent event) { public static void onServerStopped(ServerStoppedEvent event) { activeServer = null; activeProfile = null; + BiomeTypeCompatibility.clearRegistry(); GeomeConfig.applyWorldProfile(globalProfile()); BiomeWorldgenManager.clear(); StoneReplacer.refreshWorldConfig(); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java index 72fe1c23..485af828 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java @@ -6,10 +6,12 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Blocks; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; @@ -30,6 +32,21 @@ void configuredBiomeWeightsWorkWithoutAForgeBiomeRegistryEntry() { assertEquals(1, config.pickGeome(null, WINDSWEPT_HILLS, new double[2], 0.0D)); } + @Test + void identifierFallbackRetainsDictionaryWeightContributions() { + Map indexes = new LinkedHashMap<>(); + indexes.put("cakeworld:peppermint_fold", 0); + indexes.put("cakeworld:rock_candy_uplift", 1); + ResourceLocation marshmallowPeaks = new ResourceLocation("cakeworld", "marshmallow_peaks"); + Map weights = GeomeConfig.bakeBiomeIdentifierWeights(indexes, + Map.of(marshmallowPeaks.toString(), new double[] { 6.0D, 14.0D }), + Map.of("COLD", new double[] { 8.0D, 0.0D }), + type -> Set.of(marshmallowPeaks)); + + assertEquals(15.0D, weights.get(marshmallowPeaks)[0]); + assertEquals(15.0D, weights.get(marshmallowPeaks)[1]); + } + @Test void savedWorldBoundaryUsesItsConfiguredBiomeInsteadOfEqualFallbackWeights() { BakedGeomeConfig config = observedWorldConfig(); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java index 83815b24..81cd2e37 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java @@ -10,14 +10,54 @@ import java.util.Map; import java.util.Set; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + import org.junit.jupiter.api.Test; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.core.Registry; import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; class OreSpawnOreGenerationTest { + @Test + void biomeFiltersRetainUnknownDynamicRegistryKeys() { + ResourceKey sodaOcean = ResourceKey.create(Registry.BIOME_REGISTRY, + new ResourceLocation("cakeworld", "soda_ocean")); + JsonObject rule = new JsonObject(); + JsonArray ids = new JsonArray(); + ids.add("cakeworld:soda_ocean"); + rule.add("biome_ids", ids); + + Set resolved = OreSpawnOreGeneration.resolveBiomes( + rule, "biome_ids", "biome_dictionary"); + + assertEquals(Set.of(sodaOcean), resolved); + } + + @Test + void biomeFiltersMergeDictionaryKeys() { + ResourceKey sodaOcean = ResourceKey.create(Registry.BIOME_REGISTRY, + new ResourceLocation("cakeworld", "soda_ocean")); + JsonObject rule = new JsonObject(); + JsonArray dictionary = new JsonArray(); + dictionary.add("OCEAN"); + rule.add("biome_dictionary", dictionary); + + Set> resolved = OreSpawnOreGeneration.resolveBiomes( + rule, "biome_ids", "biome_dictionary", type -> Set.of(sodaOcean)); + + assertEquals(Set.of(sodaOcean), resolved); + assertTrue(OreSpawnOreGeneration.acceptsBiome(resolved, Set.of(), sodaOcean)); + assertFalse(OreSpawnOreGeneration.acceptsBiome(resolved, Set.of(), ResourceKey.create( + Registry.BIOME_REGISTRY, new ResourceLocation("cakeworld", "candy_plains")))); + assertFalse(OreSpawnOreGeneration.acceptsBiome(Set.of(), resolved, sodaOcean)); + assertTrue(OreSpawnOreGeneration.acceptsBiome(Set.of(), resolved, ResourceKey.create( + Registry.BIOME_REGISTRY, new ResourceLocation("cakeworld", "candy_plains")))); + } + @Test void fixedQuantityDoesNotConsumeRandomState() { CountingRandom random = new CountingRandom(0); From 7c82ecc844ae9d241a108f4417c6c3b5b0bc29a2 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sat, 29 Aug 2026 13:54:18 +0100 Subject: [PATCH 2/6] Align geology sampler with generated surface biomes --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.txt | 7 +++++++ README.md | 2 +- build.gradle | 10 +++++----- docs/API.md | 9 ++++++--- docs/VERSIONS.md | 9 +++++---- gradle.properties | 2 +- .../mc/orespawn/api/GeologySampler.java | 4 +++- .../orespawn/api/OreSpawnGeologySampler.java | 7 ++++++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../api/OreSpawnGeologySamplerTest.java | 19 +++++++++++++++++++ 12 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySamplerTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0e83ca8..03d79b80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.11.118021.jar - build/libs/OreSpawn-4.0.11.118021-sources.jar - build/libs/OreSpawn-4.0.11.118021-javadoc.jar + build/libs/OreSpawn-4.0.12.118021.jar + build/libs/OreSpawn-4.0.12.118021-sources.jar + build/libs/OreSpawn-4.0.12.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0c2ee7a4..7654e5e2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.12.118021 + +* Classify public geology samples at the same highest occupied block used by + chunk geology generation, rather than the first free block above it. +* Keep public sampler predictions consistent with generated rock at vertical + biome seams without changing existing chunks, profiles, or generation. + Version 4.0.11.118021 * Preserve biome-dictionary geome weights when a data-driven biome is reached diff --git a/README.md b/README.md index 8261f345..f519a623 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.11.118021`: the OreSpawn 4.0.11 +This branch builds target-qualified version `4.0.12.118021`: the OreSpawn 4.0.12 feature set for Minecraft 1.18.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index dee5ede8..c0a079b2 100644 --- a/build.gradle +++ b/build.gradle @@ -745,7 +745,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.11.118021' + if (project.mod_version != '4.0.12.118021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.18.2' || project.forge_version != '40.3.0' @@ -759,9 +759,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.11.118021.jar', - 'OreSpawn-4.0.11.118021-sources.jar', - 'OreSpawn-4.0.11.118021-javadoc.jar' + 'OreSpawn-4.0.12.118021.jar', + 'OreSpawn-4.0.12.118021-sources.jar', + 'OreSpawn-4.0.12.118021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -778,7 +778,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.11.118021')) { + if (!file(path).getText('UTF-8').contains('4.0.12.118021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index f12d6586..7ec66637 100644 --- a/docs/API.md +++ b/docs/API.md @@ -127,9 +127,12 @@ OreSpawnApi.createSampler(server.overworld()).ifPresent(sampler -> { ``` `sampleColumn` performs one biome/geome classification and reuses it for every -Y query. Sampling is read-only and is intended for gameplay decisions, -diagnostics, and compatible generation outside OreSpawn's block loops. -Callbacks inside OreSpawn generation loops are intentionally unsupported. +Y query. Pass the first-free surface height returned by `Level.getHeight`; +OreSpawn uses the highest occupied block immediately below it for biome/geome +classification, exactly as chunk geology generation does. Sampling is read-only +and is intended for gameplay decisions, diagnostics, and compatible generation +outside OreSpawn's block loops. Callbacks inside OreSpawn generation loops are +intentionally unsupported. Custom pattern mods create a Forge `DeferredRegister` using `OreSpawnPatternRegistry.REGISTRY_NAME`. An `OrePatternType` contains a codec diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index b1429e85..b43db89c 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -56,7 +56,7 @@ Examples: | 1.15.2 | Forge | `115021` | `4.0.9.115021` | | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | -| 1.18.2 | Forge | `118021` | `4.0.11.118021` | +| 1.18.2 | Forge | `118021` | `4.0.12.118021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -151,10 +151,11 @@ Bug number, such as Forge 1.14.4's `4.0.8.114041`, even though the 4.0.7 repair was not applicable there. Forge 1.15.2, 1.16.5, and 1.17.1 then advanced to their target-qualified 4.0.9 releases for the provider terrain-host ordering repair. Forge 1.18.2 then advanced to `4.0.10.118021` for the distinct Stable -Layers actual-height eligibility repair, and to `4.0.11.118021` to retain +Layers actual-height eligibility repair, to `4.0.11.118021` to retain biome-dictionary weights and ore biome filters for dynamic-registry biome -instances. A branch may therefore legitimately skip functional version -numbers. +instances, and to `4.0.12.118021` so public geology samples classify the same +highest occupied block as chunk generation at vertical biome seams. A branch +may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 22b22d53..fac3969e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20220404.173914 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.11.118021 +mod_version=4.0.12.118021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java index f029d69f..b242fa2f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java @@ -4,7 +4,9 @@ public interface GeologySampler { /** * Classifies one column. The returned column reuses that biome/geome - * classification for all subsequent Y queries. + * classification for all subsequent Y queries. {@code surfaceY} is the first + * free block returned by {@code Level.getHeight}; OreSpawn classifies the + * biome at the highest occupied block, matching chunk geology generation. */ GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY); } diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java index 40f0454b..a0c8f310 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java @@ -56,7 +56,8 @@ static GeologySampler create(ServerLevel level) { @Override public GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY) { - BlockPos position = new BlockPos(blockX, surfaceY, blockZ); + BlockPos position = new BlockPos(blockX, + generationBiomeY(surfaceY, level.getMinBuildHeight()), blockZ); Holder holder = level.getBiome(position); ResourceLocation biomeId = holder.unwrapKey().map(ResourceKey::location) .orElse(new ResourceLocation("orespawn", "unregistered_biome")); @@ -67,6 +68,10 @@ public GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY) { return new SkyColumn(biomeId, blockX, blockZ, surfaceY, sample); } + static int generationBiomeY(int firstFreeY, int minBuildHeight) { + return firstFreeY <= minBuildHeight ? minBuildHeight : firstFreeY - 1; + } + private abstract class BaseColumn implements GeologyColumn { private final ResourceLocation biome; private final int x; diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 93235f8c..31112b4f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.11.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.12.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index 87519f03..36b104cd 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.11.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.12.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/test/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySamplerTest.java b/src/test/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySamplerTest.java new file mode 100644 index 00000000..ec243a1a --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySamplerTest.java @@ -0,0 +1,19 @@ +package zone.moddev.mc.orespawn.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class OreSpawnGeologySamplerTest { + @Test + void convertsLevelHeightToTheGenerationBiomeHeight() { + assertEquals(96, OreSpawnGeologySampler.generationBiomeY(97, -64)); + assertEquals(-1, OreSpawnGeologySampler.generationBiomeY(0, -64)); + } + + @Test + void clampsAnEmptyColumnToTheLevelFloor() { + assertEquals(-64, OreSpawnGeologySampler.generationBiomeY(-64, -64)); + assertEquals(-64, OreSpawnGeologySampler.generationBiomeY(Integer.MIN_VALUE, -64)); + } +} From 9ef63213bd4093ba08ba3e3e6f41004d8fb4cc41 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sun, 30 Aug 2026 10:38:15 +0100 Subject: [PATCH 3/6] Fix provider biome filters and namespaced geomes --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 +++ README.md | 2 +- build.gradle | 10 +-- docs/API.md | 7 ++ docs/CONFIGURATION.md | 4 +- docs/DEVELOPER_GUIDE.md | 6 ++ docs/VERSIONS.md | 6 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 65 ++++++------------- .../mc/orespawn/api/WorldgenProvider.java | 27 ++++++++ .../orespawn/client/GeologyEditorSession.java | 11 +++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../mc/orespawn/api/WorldgenProviderTest.java | 62 ++++++++++++++++++ .../client/GeologyEditorSessionTest.java | 23 +++++++ 16 files changed, 183 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03d79b80..d17cb567 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.12.118021.jar - build/libs/OreSpawn-4.0.12.118021-sources.jar - build/libs/OreSpawn-4.0.12.118021-javadoc.jar + build/libs/OreSpawn-4.0.13.118021.jar + build/libs/OreSpawn-4.0.13.118021-sources.jar + build/libs/OreSpawn-4.0.13.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7654e5e2..4139177f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.13.118021 + +* Give the public ore-dimension builder the exact biome include/exclude and + biome-dictionary filter support already available in provider JSON. +* Accept valid namespaced geome IDs in both creation-editor validation paths + while preserving legacy unnamespaced geome keys. +* API major 1, schemas, existing profiles, generated chunks, and worldgen + behaviour are unchanged. + Version 4.0.12.118021 * Classify public geology samples at the same highest occupied block used by diff --git a/README.md b/README.md index f519a623..98b7b9f5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.12.118021`: the OreSpawn 4.0.12 +This branch builds target-qualified version `4.0.13.118021`: the OreSpawn 4.0.13 feature set for Minecraft 1.18.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index c0a079b2..0253b508 100644 --- a/build.gradle +++ b/build.gradle @@ -745,7 +745,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.12.118021' + if (project.mod_version != '4.0.13.118021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.18.2' || project.forge_version != '40.3.0' @@ -759,9 +759,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.12.118021.jar', - 'OreSpawn-4.0.12.118021-sources.jar', - 'OreSpawn-4.0.12.118021-javadoc.jar' + 'OreSpawn-4.0.13.118021.jar', + 'OreSpawn-4.0.13.118021-sources.jar', + 'OreSpawn-4.0.13.118021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -778,7 +778,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.12.118021')) { + if (!file(path).getText('UTF-8').contains('4.0.13.118021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index 7ec66637..bf376a86 100644 --- a/docs/API.md +++ b/docs/API.md @@ -77,6 +77,13 @@ WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) `OilDefinition` and template `.oil(...)` remain deprecated migration adapters for one legacy oil rule. New integrations should use `FluidDepositDefinition`. +Ore dimension builders expose the same biome filters as provider JSON and +fluid-deposit builders. Use `.biome(...)` and `.biomeDictionary(...)` for +inclusions, with `.excludeBiome(...)` and `.excludeBiomeDictionary(...)` for +exclusions. These methods work on both explicit `.dimension(...)` rules and +`.dimensionSelector(...)` fallbacks; built definitions and their returned +filter sets are immutable. + Register custom biomes with Forge as usual. `OreSpawnBiomes.copyAndRegister` provides a small optional convenience for cloning a known biome without adding TerraBlender: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6d901ac0..e6276439 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -117,7 +117,9 @@ is omitted. `dimensions` limits membership, and `geomes` multiplies selection weight by province. A weight of zero prevents selection in that context. Geomes contain a non-negative `base` weight and non-negative weights for each -rock family. Biome and biome-dictionary maps multiply those geome weights. +rock family. Keys may retain the legacy unnamespaced form or use a provider +resource ID such as `examplemod:crystal_basin`; the creation editor preserves +both forms. Biome and biome-dictionary maps multiply those geome weights. Missing optional-mod biome IDs are ignored during baking. Terrain dimensions require `enabled`, `host_blocks`, and `host_tags`. diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 5b8645e0..fe29da33 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -100,6 +100,10 @@ private void enqueueWorldgen(InterModEnqueueEvent event) { .quantityRange(4, 11) .pattern(OrePattern.VEIN) .heightDistribution(OreHeightDistribution.TRIANGLE) + .biome(new ResourceLocation("minecraft", "plains")) + .biomeDictionary("FOREST") + .excludeBiome(new ResourceLocation("minecraft", "dark_forest")) + .excludeBiomeDictionary("SPOOKY") .hostTag(new ResourceLocation("minecraft", "stone_ore_replaceables")))) .build(); @@ -114,6 +118,8 @@ Use `.quantity(8)` when every attempt should have a fixed budget. The selector above preserves old OS3 behavior in every ordinary dimension except Nether and End. Add an explicit `.dimension(overworld, ...)` as well when the Overworld needs different settings; the explicit rule overrides the selector there. +Ore dimension builders support the same exact-ID and biome-dictionary include +and exclude filters as provider JSON and fluid-deposit builders. ## Pack Override Quick Start diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index b43db89c..b44d82c9 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -56,7 +56,7 @@ Examples: | 1.15.2 | Forge | `115021` | `4.0.9.115021` | | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | -| 1.18.2 | Forge | `118021` | `4.0.12.118021` | +| 1.18.2 | Forge | `118021` | `4.0.13.118021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -154,7 +154,9 @@ repair. Forge 1.18.2 then advanced to `4.0.10.118021` for the distinct Stable Layers actual-height eligibility repair, to `4.0.11.118021` to retain biome-dictionary weights and ore biome filters for dynamic-registry biome instances, and to `4.0.12.118021` so public geology samples classify the same -highest occupied block as chunk generation at vertical biome seams. A branch +highest occupied block as chunk generation at vertical biome seams. It then +advanced to `4.0.13.118021` to restore API biome-filter parity and accept +provider-namespaced geomes in the creation editor. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index fac3969e..7202e5ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20220404.173914 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.12.118021 +mod_version=4.0.13.118021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java index a6b3fa1e..2d8fb28e 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -21,6 +21,8 @@ import zone.moddev.mc.orespawn.api.BiomeReplacementScope; import zone.moddev.mc.orespawn.api.GeologyFamily; import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.orespawn.api.OreHeightDistribution; +import zone.moddev.mc.orespawn.api.OrePattern; import zone.moddev.mc.orespawn.api.ProviderStatus; import zone.moddev.mc.orespawn.api.WorldgenProvider; import zone.moddev.mc.orespawn.api.WorldgenProvider.BiomeSurfaceDefinition; @@ -99,6 +101,8 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation PROBE_GEOME_ALTERNATIVE = new ResourceLocation(MODID + ":dynamic_biome_geome_alternative"); private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water"); + private static final ResourceLocation DYNAMIC_ORE = + new ResourceLocation(MODID + ":ore/dynamic_biome_filter"); private static final Block[] NATURAL_SOURCES = { Blocks.DIRT, Blocks.GRASS_BLOCK, Blocks.COARSE_DIRT, Blocks.PODZOL, Blocks.ROOTED_DIRT, Blocks.GRAVEL, Blocks.SAND, Blocks.RED_SAND, @@ -183,6 +187,23 @@ private static void addUnique(java.util.List> features, private void enqueueProvider(InterModEnqueueEvent event) { WorldgenProvider.Builder provider = WorldgenProvider.builder(MODID, 1); addDynamicBiomeGeology(provider); + provider.ore(DYNAMIC_ORE, blockId(Blocks.DIAMOND_BLOCK), ore -> ore + .retrogen(false) + .dimension(OPEN_ID, placement -> placement + .yRange(16, 48) + .attempts(16.0D) + .quantity(8) + .pattern(OrePattern.CLUSTER) + .heightDistribution(OreHeightDistribution.UNIFORM) + .discardChanceOnAirExposure(0.0D) + .spread(4, 3) + .nodeSize(3) + .hostBlock(blockId(Blocks.CALCITE)) + .hostBlock(blockId(Blocks.BASALT)) + .biome(BIOME_A) + .biomeDictionary("COLD") + .excludeBiome(BIOME_B) + .excludeBiomeDictionary("SPOOKY"))); provider.fluidDeposit(DYNAMIC_FLUID, blockId(Blocks.WATER), deposit -> deposit .dimension(OPEN_ID, placement -> placement .yRange(16, 24) @@ -260,7 +281,6 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) { dictionary.add("COLD", cold); } cold.addProperty(PROBE_GEOME.toString(), 8.0D); - addDynamicBiomeOre(root); JsonObject terrain = root.getAsJsonObject("terrain_dimensions"); if (terrain == null) { terrain = new JsonObject(); @@ -290,49 +310,6 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) { } } - private static void addDynamicBiomeOre(JsonObject root) { - JsonObject ores = root.getAsJsonObject("ores"); - if (ores == null) { - ores = new JsonObject(); - root.add("ores", ores); - } - JsonObject ore = new JsonObject(); - ore.addProperty("block", blockId(Blocks.DIAMOND_BLOCK).toString()); - ore.addProperty("enabled", true); - ore.addProperty("native_generation", false); - ore.addProperty("suppress_vanilla", false); - ore.addProperty("retrogen", false); - JsonObject dimensions = new JsonObject(); - JsonObject end = new JsonObject(); - end.addProperty("enabled", true); - end.addProperty("min_y", 16); - end.addProperty("max_y", 48); - end.addProperty("frequency", 16.0D); - end.addProperty("quantity", 8); - end.addProperty("pattern", "cluster"); - end.addProperty("height_distribution", "uniform"); - end.addProperty("discard_chance_on_air_exposure", 0.0D); - end.addProperty("spread", 4); - end.addProperty("vertical_spread", 3); - end.addProperty("node_size", 3); - end.add("host_families", new JsonArray()); - JsonArray hosts = new JsonArray(); - hosts.add(blockId(Blocks.CALCITE).toString()); - hosts.add(blockId(Blocks.BASALT).toString()); - end.add("host_blocks", hosts); - end.add("host_tags", new JsonArray()); - end.add("geomes", new JsonObject()); - JsonArray biomes = new JsonArray(); - biomes.add(BIOME_A.toString()); - end.add("biome_ids", biomes); - end.add("excluded_biome_ids", new JsonArray()); - end.add("biome_dictionary", new JsonArray()); - end.add("excluded_biome_dictionary", new JsonArray()); - dimensions.add(OPEN_ID.toString(), end); - ore.add("dimensions", dimensions); - ores.add(MODID + ":ore/dynamic_biome_filter", ore); - } - private static void addPalette(WorldgenProvider.Builder provider, String name, ResourceLocation dimension, boolean ceiling) { BiomeSurfaceDefinition surfaceA = surface(DyeColor.PINK, DyeColor.WHITE, diff --git a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java index d64133d6..a44c8b6a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java @@ -526,6 +526,10 @@ public static final class OreDimensionDefinition implements JsonDefinition { private final Map geomes; private final Set hostBlocks; private final Set hostTags; + private final Set biomeIds; + private final Set excludedBiomeIds; + private final Set biomeDictionary; + private final Set excludedBiomeDictionary; private final Map hostBlockWeights; private final Map hostTagWeights; @@ -549,6 +553,11 @@ private OreDimensionDefinition(Builder builder) { geomes = immutableMap(builder.geomes); hostBlocks = immutableSet(builder.hostBlocks); hostTags = immutableSet(builder.hostTags); + biomeIds = immutableSet(builder.biomeIds); + excludedBiomeIds = immutableSet(builder.excludedBiomeIds); + biomeDictionary = Collections.unmodifiableSet(new LinkedHashSet<>(builder.biomeDictionary)); + excludedBiomeDictionary = Collections.unmodifiableSet( + new LinkedHashSet<>(builder.excludedBiomeDictionary)); hostBlockWeights = immutableMap(builder.hostBlockWeights); hostTagWeights = immutableMap(builder.hostTagWeights); } @@ -575,6 +584,10 @@ private OreDimensionDefinition(Builder builder) { public Map geomes() { return geomes; } public Set hostBlocks() { return hostBlocks; } public Set hostTags() { return hostTags; } + public Set biomeIds() { return biomeIds; } + public Set excludedBiomeIds() { return excludedBiomeIds; } + public Set biomeDictionary() { return biomeDictionary; } + public Set excludedBiomeDictionary() { return excludedBiomeDictionary; } public Map hostBlockWeights() { return hostBlockWeights; } public Map hostTagWeights() { return hostTagWeights; } @@ -610,6 +623,10 @@ public JsonObject toJson() { json.add("geomes", weights(geomes)); json.add("host_blocks", weightedIds(hostBlocks, hostBlockWeights, "block")); json.add("host_tags", weightedIds(hostTags, hostTagWeights, "tag")); + json.add("biome_ids", ids(biomeIds)); + json.add("excluded_biome_ids", ids(excludedBiomeIds)); + json.add("biome_dictionary", strings(biomeDictionary)); + json.add("excluded_biome_dictionary", strings(excludedBiomeDictionary)); return json; } @@ -633,6 +650,10 @@ public static final class Builder { private final Map geomes = new LinkedHashMap<>(); private final Set hostBlocks = new LinkedHashSet<>(); private final Set hostTags = new LinkedHashSet<>(); + private final Set biomeIds = new LinkedHashSet<>(); + private final Set excludedBiomeIds = new LinkedHashSet<>(); + private final Set biomeDictionary = new LinkedHashSet<>(); + private final Set excludedBiomeDictionary = new LinkedHashSet<>(); private final Map hostBlockWeights = new LinkedHashMap<>(); private final Map hostTagWeights = new LinkedHashMap<>(); @@ -663,6 +684,12 @@ public Builder pattern(ResourceLocation type, JsonObject settings) { public Builder geomeWeight(ResourceLocation geome, double value) { geomes.put(geome, value); return this; } public Builder hostBlock(ResourceLocation value) { hostBlocks.add(value); return this; } public Builder hostTag(ResourceLocation value) { hostTags.add(value); return this; } + public Builder biome(ResourceLocation value) { biomeIds.add(value); return this; } + public Builder excludeBiome(ResourceLocation value) { excludedBiomeIds.add(value); return this; } + public Builder biomeDictionary(String value) { biomeDictionary.add(nonBlank(value)); return this; } + public Builder excludeBiomeDictionary(String value) { + excludedBiomeDictionary.add(nonBlank(value)); return this; + } public Builder hostBlock(ResourceLocation value, double weight) { hostBlocks.add(value); hostBlockWeights.put(value, replacementWeight(weight)); diff --git a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java index 12d65b1c..8cbd83c3 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -630,7 +630,7 @@ JsonObject weightMap(String section, String id) { void addGeome(String id) { String normalized = id.trim().toLowerCase(Locale.ROOT); - if (!normalized.matches("[a-z0-9_.-]+") || section("geomes").has(normalized)) { + if (!validGeomeId(normalized) || section("geomes").has(normalized)) { return; } JsonObject geome = new JsonObject(); @@ -690,7 +690,7 @@ List validate() { } for (Entry entry : terrainActive ? geomes.entrySet() : Collections.>emptySet()) { - if (!entry.getKey().matches("[a-z0-9_.-]+") || !entry.getValue().isJsonObject()) { + if (!validGeomeId(entry.getKey()) || !entry.getValue().isJsonObject()) { errors.add("Invalid geome: " + entry.getKey()); continue; } @@ -1170,6 +1170,13 @@ private static boolean validResource(String id) { } } + private static boolean validGeomeId(String id) { + if (id == null || id.isEmpty()) return false; + if (id.indexOf(':') < 0) return id.matches("[a-z0-9_.-]+"); + if (!validResource(id)) return false; + return id.equals(new ResourceLocation(id).toString()); + } + static String string(JsonObject json, String key, String fallback) { try { return json.has(key) ? json.get(key).getAsString() : fallback; } catch (RuntimeException e) { return fallback; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 31112b4f..9663f2b3 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.12.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.13.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index 36b104cd..f6f96530 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.12.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.13.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java index 96fbf8be..1159c2fe 100644 --- a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java @@ -269,6 +269,68 @@ void serializesRangedQuantityAndBroadDimensionSelector() { assertFalse(rule.has("quantity")); } + @Test + void oreBiomeFiltersMatchFluidBuilderForDimensionsAndSelectors() { + ResourceLocation overworld = id("minecraft:overworld"); + ResourceLocation plains = id("minecraft:plains"); + ResourceLocation darkForest = id("minecraft:dark_forest"); + WorldgenProvider.OreDimensionDefinition explicit = WorldgenProvider.OreDimensionDefinition + .builder(overworld) + .enabled(false) + .hostTag(id("minecraft:stone_ore_replaceables")) + .biome(plains) + .biomeDictionary("FOREST") + .excludeBiome(darkForest) + .excludeBiomeDictionary("SPOOKY") + .build(); + WorldgenProvider.OreDimensionDefinition selector = WorldgenProvider.OreDimensionDefinition + .builder(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id()) + .hostTag(id("minecraft:stone_ore_replaceables")) + .biome(plains) + .biomeDictionary("FOREST") + .excludeBiome(darkForest) + .excludeBiomeDictionary("SPOOKY") + .build(); + + assertEquals(Collections.singleton(plains), explicit.biomeIds()); + assertEquals(Collections.singleton(darkForest), explicit.excludedBiomeIds()); + assertEquals(Collections.singleton("FOREST"), explicit.biomeDictionary()); + assertEquals(Collections.singleton("SPOOKY"), explicit.excludedBiomeDictionary()); + assertThrows(UnsupportedOperationException.class, + () -> explicit.biomeIds().add(id("minecraft:forest"))); + + WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) + .ore(id("examplemod:filtered_ore"), ore -> ore + .dimension(explicit) + .dimensionSelector(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END, + selector)) + .build(); + JsonObject ore = provider.toJson().getAsJsonObject("ores") + .getAsJsonObject("examplemod:ore/examplemod/filtered_ore"); + assertFalse(ore.getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()) + .get("enabled").getAsBoolean()); + assertTrue(ore.getAsJsonObject("dimension_selectors").getAsJsonObject( + OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id().toString()) + .get("enabled").getAsBoolean()); + for (JsonObject rule : new JsonObject[] { + ore.getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()), + ore.getAsJsonObject("dimension_selectors").getAsJsonObject( + OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id().toString()) }) { + assertEquals("[\"minecraft:plains\"]", rule.getAsJsonArray("biome_ids").toString()); + assertEquals("[\"minecraft:dark_forest\"]", + rule.getAsJsonArray("excluded_biome_ids").toString()); + assertEquals("[\"FOREST\"]", rule.getAsJsonArray("biome_dictionary").toString()); + assertEquals("[\"SPOOKY\"]", + rule.getAsJsonArray("excluded_biome_dictionary").toString()); + } + ore.getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()) + .getAsJsonArray("biome_ids").add("minecraft:forest"); + assertEquals("[\"minecraft:plains\"]", provider.toJson().getAsJsonObject("ores") + .getAsJsonObject("examplemod:ore/examplemod/filtered_ore") + .getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()) + .getAsJsonArray("biome_ids").toString()); + } + @Test void rejectsInvalidQuantityRangesEarly() { assertThrows(IllegalStateException.class, () -> WorldgenProvider.OreDimensionDefinition diff --git a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java index 84fa6c32..e7226feb 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java @@ -147,4 +147,27 @@ void standaloneFluidPickerCreatesAUsableCoveredOverworldRule() { java.util.List errors = session.validate(); assertTrue(errors.isEmpty(), errors.toString()); } + + @Test + void namespacedGeomesCanBeAddedValidatedAndRoundTripped() { + String geomeId = "cakeworld:cocoa_basin"; + GeologyEditorSession session = new GeologyEditorSession(WorldGeologyProfile.recommended(false)); + session.configureDefaultVanillaStrata(); + session.addGeome(geomeId); + + assertTrue(session.section("geomes").has(geomeId)); + session.weightMap("biomes", "minecraft:plains").addProperty(geomeId, 2.0D); + session.rock("minecraft:stone").getAsJsonObject("geomes").addProperty(geomeId, 3.0D); + java.util.List errors = session.validate(); + assertTrue(errors.isEmpty(), errors.toString()); + + WorldGeologyProfile saved = session.profile(); + GeologyEditorSession reopened = new GeologyEditorSession(saved); + assertEquals(saved.rootCopy(), reopened.profile().rootCopy()); + assertTrue(reopened.validate().isEmpty(), reopened.validate().toString()); + assertEquals(2.0D, reopened.weightMap("biomes", "minecraft:plains") + .get(geomeId).getAsDouble()); + assertEquals(3.0D, reopened.rock("minecraft:stone").getAsJsonObject("geomes") + .get(geomeId).getAsDouble()); + } } From 668d3e0db2b2fac780a5bad35c1ab6fa9c2ef206 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sun, 30 Aug 2026 13:50:43 +0100 Subject: [PATCH 4/6] Fix exposed snow material conversion --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 8 ++ README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 6 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 115 +++++++++++++++++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/WorldMaterialWeather.java | 8 ++ 10 files changed, 144 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d17cb567..2cd30dbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.13.118021.jar - build/libs/OreSpawn-4.0.13.118021-sources.jar - build/libs/OreSpawn-4.0.13.118021-javadoc.jar + build/libs/OreSpawn-4.0.14.118021.jar + build/libs/OreSpawn-4.0.14.118021-sources.jar + build/libs/OreSpawn-4.0.14.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4139177f..c40de02d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.14.118021 + +* Convert exposed one-layer vanilla Snow from the first free cell immediately + above Minecraft's motion-blocking surface when a dimension supplies a custom + snow material. +* Retain the existing surface Ice conversion and leave buried or authored Snow + and Ice unchanged. + Version 4.0.13.118021 * Give the public ore-dimension builder the exact biome include/exclude and diff --git a/README.md b/README.md index 98b7b9f5..0e4f2d78 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.13.118021`: the OreSpawn 4.0.13 +This branch builds target-qualified version `4.0.14.118021`: the OreSpawn 4.0.14 feature set for Minecraft 1.18.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 0253b508..9877003e 100644 --- a/build.gradle +++ b/build.gradle @@ -745,7 +745,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.13.118021' + if (project.mod_version != '4.0.14.118021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.18.2' || project.forge_version != '40.3.0' @@ -759,9 +759,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.13.118021.jar', - 'OreSpawn-4.0.13.118021-sources.jar', - 'OreSpawn-4.0.13.118021-javadoc.jar' + 'OreSpawn-4.0.14.118021.jar', + 'OreSpawn-4.0.14.118021-sources.jar', + 'OreSpawn-4.0.14.118021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -778,7 +778,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.13.118021')) { + if (!file(path).getText('UTF-8').contains('4.0.14.118021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index b44d82c9..010b5d76 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -56,7 +56,7 @@ Examples: | 1.15.2 | Forge | `115021` | `4.0.9.115021` | | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | -| 1.18.2 | Forge | `118021` | `4.0.13.118021` | +| 1.18.2 | Forge | `118021` | `4.0.14.118021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -156,7 +156,9 @@ biome-dictionary weights and ore biome filters for dynamic-registry biome instances, and to `4.0.12.118021` so public geology samples classify the same highest occupied block as chunk generation at vertical biome seams. It then advanced to `4.0.13.118021` to restore API biome-filter parity and accept -provider-namespaced geomes in the creation editor. A branch +provider-namespaced geomes in the creation editor, then to `4.0.14.118021` so +one-layer Snow above the motion-blocking surface is included in configured +weather-material conversion. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 7202e5ba..6edcec13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20220404.173914 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.13.118021 +mod_version=4.0.14.118021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java index 2d8fb28e..61842e1c 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -134,6 +134,8 @@ public final class SurfaceProbeTestMod { private static final String MARKER_NAME = "surfaceprobe-integration.properties"; private static final String CHEST_ITEM_NAME = "surfaceprobe sentinel"; private static final String RAW_CHEST_ITEM_NAME = "surfaceprobe raw block entity sentinel"; + private static final BlockState WEATHER_SNOW_REPLACEMENT = Blocks.WHITE_WOOL.defaultBlockState(); + private static final BlockState WEATHER_ICE_REPLACEMENT = Blocks.BLUE_ICE.defaultBlockState(); public SurfaceProbeTestMod() { FMLJavaModLoadingContext context = FMLJavaModLoadingContext.get(); @@ -216,6 +218,9 @@ private void enqueueProvider(InterModEnqueueEvent event) { .hostBlock(blockId(Blocks.CALCITE)))); addPalette(provider, "open_palette", OPEN_ID, false); addPalette(provider, "roofed_palette", ROOFED_ID, true); + provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/end"), OPEN_ID, + materials -> materials.snowBlock(blockId(Blocks.WHITE_WOOL)) + .iceBlock(blockId(Blocks.BLUE_ICE))); provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/nether"), ROOFED_ID, materials -> materials.defaultFluid(blockId(Blocks.WATER))); if (!OreSpawnApi.enqueue(provider.build())) { @@ -420,6 +425,12 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { long rawBlockEntities = 0L; long dictionaryPrimary = 0L; long dictionaryAlternative = 0L; + long exposedSnowConverted = 0L; + long surfaceIceConverted = 0L; + long buriedSnowPreserved = 0L; + long buriedIcePreserved = 0L; + long unconfiguredSnowPreserved = 0L; + long unconfiguredIcePreserved = 0L; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -503,6 +514,14 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { rawBedrock += natural.bedrockPreserved(); rawBlockEntities += natural.blockEntityPreserved(); } + WeatherMaterialAudit weather = auditWeatherMaterials(chunk, pos, + chunkMinX, chunkMinZ, level.getMinBuildHeight(), level.getMaxBuildHeight(), roofed); + exposedSnowConverted += weather.exposedSnowConverted(); + surfaceIceConverted += weather.surfaceIceConverted(); + buriedSnowPreserved += weather.buriedSnowPreserved(); + buriedIcePreserved += weather.buriedIcePreserved(); + unconfiguredSnowPreserved += weather.unconfiguredSnowPreserved(); + unconfiguredIcePreserved += weather.unconfiguredIcePreserved(); } } @@ -510,13 +529,19 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { if (top != EXPECTED_COLUMNS - 9 || underwater != 9 || filler != EXPECTED_FILLER || biomeA == 0 || biomeB == 0 || edgeChanges == 0 || sentinels != 9 * 4 || geology != (roofed ? 0 : EXPECTED_FILLER) - || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS)) + || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS + || unconfiguredSnowPreserved != 9 || unconfiguredIcePreserved != 9 + || exposedSnowConverted != 0 || surfaceIceConverted != 0 + || buriedSnowPreserved != 0 || buriedIcePreserved != 0)) || (!roofed && (rawNaturalSources != EXPECTED_NATURAL_SOURCES || structureNaturalSources != EXPECTED_NATURAL_SOURCES || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES || cavePockets != 54 || underwaterPockets != 63 || rawBedrock != 9 || rawBlockEntities != 9 || dictionaryPrimary != EXPECTED_FILLER || dictionaryAlternative != 0 + || exposedSnowConverted != 9 || surfaceIceConverted != 9 + || buriedSnowPreserved != 9 || buriedIcePreserved != 9 + || unconfiguredSnowPreserved != 0 || unconfiguredIcePreserved != 0 || dynamicBiomeOre == 0))) { throw new IllegalStateException("Incomplete surface audit for " + level.dimension().location() + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler @@ -532,6 +557,12 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { + ", rawBlockEntities=" + rawBlockEntities + ", dictionaryPrimary=" + dictionaryPrimary + ", dictionaryAlternative=" + dictionaryAlternative + + ", exposedSnowConverted=" + exposedSnowConverted + + ", surfaceIceConverted=" + surfaceIceConverted + + ", buriedSnowPreserved=" + buriedSnowPreserved + + ", buriedIcePreserved=" + buriedIcePreserved + + ", unconfiguredSnowPreserved=" + unconfiguredSnowPreserved + + ", unconfiguredIcePreserved=" + unconfiguredIcePreserved + ", dynamicBiomeOre=" + dynamicBiomeOre); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); @@ -539,7 +570,46 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { biomeA, biomeB, edgeChanges, sentinels, aquiferFluid, rawNaturalSources, structureNaturalSources, vegetationNaturalSources, cavePockets, underwaterPockets, rawBedrock, rawBlockEntities, - dictionaryPrimary, dictionaryAlternative, dynamicBiomeOre); + dictionaryPrimary, dictionaryAlternative, dynamicBiomeOre, + exposedSnowConverted, surfaceIceConverted, + buriedSnowPreserved, buriedIcePreserved, + unconfiguredSnowPreserved, unconfiguredIcePreserved); + } + + private static WeatherMaterialAudit auditWeatherMaterials(ChunkAccess chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ, int minY, int maxY, + boolean roofed) { + int snowGroundY = findMarkedGround(chunk, pos, minX + 2, minZ + 2, minY, maxY); + int iceGroundY = findMarkedGround(chunk, pos, minX + 3, minZ + 2, minY, maxY); + if (roofed) { + return new WeatherMaterialAudit(0L, 0L, 0L, 0L, + assertState(chunk, pos.set(minX + 2, snowGroundY + 11, minZ + 2), + Blocks.SNOW.defaultBlockState(), "unconfigured exposed Snow preservation"), + assertState(chunk, pos.set(minX + 3, iceGroundY + 11, minZ + 2), + Blocks.ICE.defaultBlockState(), "unconfigured surface Ice preservation")); + } + int buriedSnowGroundY = findMarkedGround(chunk, pos, minX + 2, minZ + 3, minY, maxY); + int buriedIceGroundY = findMarkedGround(chunk, pos, minX + 3, minZ + 3, minY, maxY); + return new WeatherMaterialAudit( + assertState(chunk, pos.set(minX + 2, snowGroundY + 1, minZ + 2), + WEATHER_SNOW_REPLACEMENT, "exposed Snow weather replacement"), + assertState(chunk, pos.set(minX + 3, iceGroundY + 1, minZ + 2), + WEATHER_ICE_REPLACEMENT, "surface Ice weather replacement"), + assertState(chunk, pos.set(minX + 2, buriedSnowGroundY - 24, minZ + 3), + Blocks.SNOW.defaultBlockState(), "buried authored Snow preservation"), + assertState(chunk, pos.set(minX + 3, buriedIceGroundY - 24, minZ + 3), + Blocks.ICE.defaultBlockState(), "buried authored Ice preservation"), + 0L, 0L); + } + + private static long assertState(ChunkAccess chunk, BlockPos pos, + BlockState expected, String label) { + BlockState actual = chunk.getBlockState(pos); + if (!actual.equals(expected)) { + throw new IllegalStateException(label + " changed at " + pos + + ": expected " + expected + " but found " + actual); + } + return 1L; } private static long auditDynamicBiomeOre(ServerLevel level) { @@ -755,6 +825,12 @@ private static Properties properties(long seed, Map results values.setProperty(prefix + "dictionary_primary", Long.toString(result.dictionaryPrimary())); values.setProperty(prefix + "dictionary_alternative", Long.toString(result.dictionaryAlternative())); values.setProperty(prefix + "dynamic_biome_ore", Long.toString(result.dynamicBiomeOre())); + values.setProperty(prefix + "exposed_snow_converted", Long.toString(result.exposedSnowConverted())); + values.setProperty(prefix + "surface_ice_converted", Long.toString(result.surfaceIceConverted())); + values.setProperty(prefix + "buried_snow_preserved", Long.toString(result.buriedSnowPreserved())); + values.setProperty(prefix + "buried_ice_preserved", Long.toString(result.buriedIcePreserved())); + values.setProperty(prefix + "unconfigured_snow_preserved", Long.toString(result.unconfiguredSnowPreserved())); + values.setProperty(prefix + "unconfigured_ice_preserved", Long.toString(result.unconfiguredIcePreserved())); } return values; } @@ -918,9 +994,34 @@ private static boolean placeVegetationSentinels(WorldGenLevel world, ChunkAccess world.setBlock(pos.set(minX + 6, vegetationY + 1, minZ + 6), Blocks.DIRT.defaultBlockState(), 2); world.setBlock(pos.set(minX + 6, vegetationY + 2, minZ + 6), Blocks.OAK_SAPLING.defaultBlockState(), 2); placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 20); + placeWeatherMaterialSentinels(world, chunk, pos, minX, minZ); return true; } + private static void placeWeatherMaterialSentinels(WorldGenLevel world, ChunkAccess chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ) { + int snowGroundY = markedGround(chunk, pos, minX + 2, minZ + 2, world); + int iceGroundY = markedGround(chunk, pos, minX + 3, minZ + 2, world); + if (world.getLevel().dimension().equals(ROOFED)) { + world.setBlock(pos.set(minX + 2, snowGroundY + 11, minZ + 2), + Blocks.SNOW.defaultBlockState(), 2); + world.setBlock(pos.set(minX + 3, iceGroundY + 11, minZ + 2), + Blocks.ICE.defaultBlockState(), 2); + return; + } + if (!world.getLevel().dimension().equals(OPEN)) return; + world.setBlock(pos.set(minX + 2, snowGroundY + 1, minZ + 2), + Blocks.SNOW.defaultBlockState(), 2); + world.setBlock(pos.set(minX + 3, iceGroundY + 1, minZ + 2), + Blocks.ICE.defaultBlockState(), 2); + int buriedSnowGroundY = markedGround(chunk, pos, minX + 2, minZ + 3, world); + int buriedIceGroundY = markedGround(chunk, pos, minX + 3, minZ + 3, world); + world.setBlock(pos.set(minX + 2, buriedSnowGroundY - 24, minZ + 3), + Blocks.SNOW.defaultBlockState(), 2); + world.setBlock(pos.set(minX + 3, buriedIceGroundY - 24, minZ + 3), + Blocks.ICE.defaultBlockState(), 2); + } + private static void placeAuthoredNaturalSources(WorldGenLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, int minX, int minZ, int depth) { if (!world.getLevel().dimension().equals(OPEN)) return; @@ -954,11 +1055,19 @@ private record NaturalSourceAudit(long rawConverted, long structurePreserved, long vegetationPreserved, long cavePreserved, long underwaterPreserved, long bedrockPreserved, long blockEntityPreserved) { } + private record WeatherMaterialAudit(long exposedSnowConverted, + long surfaceIceConverted, long buriedSnowPreserved, + long buriedIcePreserved, long unconfiguredSnowPreserved, + long unconfiguredIcePreserved) { } + private record AuditResult(long top, long underwater, long filler, long geology, long ceiling, long roofTop, int biomeA, int biomeB, int edgeChanges, int sentinels, long aquiferFluid, long rawNaturalSources, long structureNaturalSources, long vegetationNaturalSources, long cavePockets, long underwaterPockets, long rawBedrock, long rawBlockEntities, - long dictionaryPrimary, long dictionaryAlternative, long dynamicBiomeOre) { } + long dictionaryPrimary, long dictionaryAlternative, long dynamicBiomeOre, + long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 9663f2b3..6bbaeece 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.13.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.14.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index f6f96530..6253b602 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.13.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.14.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java index d21a0332..86dfdc50 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java @@ -54,6 +54,14 @@ private static void convertChunk(ChunkAccess chunk, DimensionMaterials materials for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int top = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, localX, localZ); + // A one-layer Snow block is non-motion-blocking and therefore occupies + // the first free cell immediately above this heightmap's surface. + if (materials.snow != null && top + 1 < chunk.getMaxBuildHeight()) { + cursor.set(minX + localX, top + 1, minZ + localZ); + if (chunk.getBlockState(cursor).is(Blocks.SNOW)) { + chunk.setBlockState(cursor, materials.snow, false); + } + } for (int offset = 0; offset <= 2; offset++) { cursor.set(minX + localX, top - offset, minZ + localZ); BlockState state = chunk.getBlockState(cursor); From b7090d7386f76aa48aa29de9665de85e2c926b84 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sun, 30 Aug 2026 18:39:27 +0100 Subject: [PATCH 5/6] Fix geology sampler biome attribution --- .github/workflows/ci.yml | 6 +-- CHANGELOG.txt | 8 +++ README.md | 2 +- build.gradle | 10 ++-- docs/API.md | 8 +-- docs/VERSIONS.md | 6 ++- gradle.properties | 2 +- .../mc/orespawn/api/GeologySampler.java | 3 +- .../orespawn/api/OreSpawnGeologySampler.java | 7 ++- .../orespawn/worldgen/BakedGeomeConfig.java | 18 +++---- .../mc/orespawn/worldgen/GeomeGeology.java | 3 +- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../orespawn/worldgen/TerrainBiomeLookup.java | 21 ++++++++ .../worldgen/GeomeTransitionTest.java | 50 ++++++++++++++++++- .../worldgen/TerrainBiomeLookupTest.java | 27 ++++++++++ 16 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookup.java create mode 100644 src/test/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookupTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cd30dbe..fd58f6f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.14.118021.jar - build/libs/OreSpawn-4.0.14.118021-sources.jar - build/libs/OreSpawn-4.0.14.118021-javadoc.jar + build/libs/OreSpawn-4.0.15.118021.jar + build/libs/OreSpawn-4.0.15.118021-sources.jar + build/libs/OreSpawn-4.0.15.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c40de02d..a46a1494 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.15.118021 + +* Classify generated geology and public geology samples through the same + stable quart-biome cell at three-dimensional biome boundaries. +* Keep ore family-host filters and sampler predictions consistent when later + surface features alter the final heightmap by a small amount. +* Existing chunks, profiles, API signatures, and schemas are unchanged. + Version 4.0.14.118021 * Convert exposed one-layer vanilla Snow from the first free cell immediately diff --git a/README.md b/README.md index 0e4f2d78..926f0cca 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.14.118021`: the OreSpawn 4.0.14 +This branch builds target-qualified version `4.0.15.118021`: the OreSpawn 4.0.15 feature set for Minecraft 1.18.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 9877003e..d596f214 100644 --- a/build.gradle +++ b/build.gradle @@ -745,7 +745,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.14.118021' + if (project.mod_version != '4.0.15.118021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.18.2' || project.forge_version != '40.3.0' @@ -759,9 +759,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.14.118021.jar', - 'OreSpawn-4.0.14.118021-sources.jar', - 'OreSpawn-4.0.14.118021-javadoc.jar' + 'OreSpawn-4.0.15.118021.jar', + 'OreSpawn-4.0.15.118021-sources.jar', + 'OreSpawn-4.0.15.118021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -778,7 +778,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.14.118021')) { + if (!file(path).getText('UTF-8').contains('4.0.15.118021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index bf376a86..4690edb3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -136,9 +136,11 @@ OreSpawnApi.createSampler(server.overworld()).ifPresent(sampler -> { `sampleColumn` performs one biome/geome classification and reuses it for every Y query. Pass the first-free surface height returned by `Level.getHeight`; OreSpawn uses the highest occupied block immediately below it for biome/geome -classification, exactly as chunk geology generation does. Sampling is read-only -and is intended for gameplay decisions, diagnostics, and compatible generation -outside OreSpawn's block loops. Callbacks inside OreSpawn generation loops are +classification and resolves the same stable quart-biome cell used by chunk +geology. This avoids display-oriented fuzzy biome zoom changing the prediction +after later surface work alters a heightmap. Sampling is read-only and is +intended for gameplay decisions, diagnostics, and compatible generation outside +OreSpawn's block loops. Callbacks inside OreSpawn generation loops are intentionally unsupported. Custom pattern mods create a Forge `DeferredRegister` using diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 010b5d76..a09d5782 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -56,7 +56,7 @@ Examples: | 1.15.2 | Forge | `115021` | `4.0.9.115021` | | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | -| 1.18.2 | Forge | `118021` | `4.0.14.118021` | +| 1.18.2 | Forge | `118021` | `4.0.15.118021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -158,7 +158,9 @@ highest occupied block as chunk generation at vertical biome seams. It then advanced to `4.0.13.118021` to restore API biome-filter parity and accept provider-namespaced geomes in the creation editor, then to `4.0.14.118021` so one-layer Snow above the motion-blocking surface is included in configured -weather-material conversion. A branch +weather-material conversion, and then to `4.0.15.118021` so generated geology +and public samples use the same stable quart-biome cell at three-dimensional +biome boundaries. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 6edcec13..f3cd7b7a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20220404.173914 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.14.118021 +mod_version=4.0.15.118021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java index b242fa2f..8d5ad3db 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java @@ -6,7 +6,8 @@ public interface GeologySampler { * Classifies one column. The returned column reuses that biome/geome * classification for all subsequent Y queries. {@code surfaceY} is the first * free block returned by {@code Level.getHeight}; OreSpawn classifies the - * biome at the highest occupied block, matching chunk geology generation. + * stable quart biome at the highest occupied block, matching chunk geology + * generation without Minecraft's display-oriented fuzzy biome zoom. */ GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY); } diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java index a0c8f310..25ffbe89 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java @@ -9,10 +9,10 @@ import zone.moddev.mc.orespawn.worldgen.GeomeConfig; import zone.moddev.mc.orespawn.worldgen.GeomeGeology; import zone.moddev.mc.orespawn.worldgen.RockFamily; +import zone.moddev.mc.orespawn.worldgen.TerrainBiomeLookup; import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; -import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -56,9 +56,8 @@ static GeologySampler create(ServerLevel level) { @Override public GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY) { - BlockPos position = new BlockPos(blockX, - generationBiomeY(surfaceY, level.getMinBuildHeight()), blockZ); - Holder holder = level.getBiome(position); + int biomeY = generationBiomeY(surfaceY, level.getMinBuildHeight()); + Holder holder = TerrainBiomeLookup.atBlock(level, blockX, biomeY, blockZ); ResourceLocation biomeId = holder.unwrapKey().map(ResourceKey::location) .orElse(new ResourceLocation("orespawn", "unregistered_biome")); if (mode == GeologyMode.LEGACY) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java index a8790288..f1926b8b 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java @@ -65,7 +65,7 @@ public final class BakedGeomeConfig { for (Map.Entry entry : biomeWeights.entrySet()) { ResourceLocation biomeId = ForgeRegistries.BIOMES.getKey(entry.getKey()); if (biomeId != null) { - biomeWeightsById.put(biomeId, entry.getValue()); + this.biomeWeightsById.putIfAbsent(biomeId, entry.getValue()); } } this.fallbackWeights = defaultWeights(geomes.length); @@ -286,12 +286,12 @@ int familyDiversitySlots() { } String describeBiomeWeights(Biome biome) { - double[] weights = biomeWeights.get(biome); - String source = "identity"; + ResourceLocation biomeId = ForgeRegistries.BIOMES.getKey(biome); + double[] weights = biomeId == null ? null : biomeWeightsById.get(biomeId); + String source = "registry-id"; if (weights == null) { - ResourceLocation biomeId = ForgeRegistries.BIOMES.getKey(biome); - weights = biomeId == null ? null : biomeWeightsById.get(biomeId); - source = "registry-id"; + weights = biomeWeights.get(biome); + source = "identity"; } if (weights == null) { weights = fallbackWeights; @@ -330,10 +330,8 @@ boolean hasDistinctBiomeWeights(Biome biome) { } private double[] biomeWeightsFor(Biome biome, ResourceLocation biomeId) { - double[] weights = biomeWeights.get(biome); - if (weights == null && biomeId != null) { - weights = biomeWeightsById.get(biomeId); - } + double[] weights = biomeId == null ? null : biomeWeightsById.get(biomeId); + if (weights == null) weights = biomeWeights.get(biome); return weights == null ? fallbackWeights : weights; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java index 352fcd85..86b6e631 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -99,8 +99,7 @@ public void replaceStoneInChunk(LevelAccessor world, ChunkAccess chunk, BakedTer for (int dz = 0; dz < 16; dz++) { int z = zOffset + dz; int surfaceY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, dx, dz); - cursor.set(x, surfaceY, z); - Holder biomeHolder = world.getBiome(cursor); + Holder biomeHolder = TerrainBiomeLookup.atBlock(chunk, x, surfaceY, z); Biome biome = biomeHolder.value(); Optional> biomeKey = biomeHolder.unwrapKey(); ResourceLocation biomeId = biomeKey.isPresent() ? biomeKey.get().location() : null; diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 6bbaeece..613dfccc 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.14.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.15.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index 6253b602..30bf5221 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.14.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.15.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookup.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookup.java new file mode 100644 index 00000000..387beea9 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookup.java @@ -0,0 +1,21 @@ +package zone.moddev.mc.orespawn.worldgen; + +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; + +/** + * Internal generation-time biome lookup shared by geology and its public + * read-only sampler. + */ +public final class TerrainBiomeLookup { + private TerrainBiomeLookup() { + } + + public static Holder atBlock(BiomeManager.NoiseBiomeSource source, + int blockX, int blockY, int blockZ) { + return source.getNoiseBiome(QuartPos.fromBlock(blockX), + QuartPos.fromBlock(blockY), QuartPos.fromBlock(blockZ)); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java index 485af828..dd086dd9 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java @@ -8,10 +8,16 @@ import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import net.minecraft.SharedConstants; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.Bootstrap; import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeSpecialEffects; +import net.minecraft.world.level.biome.MobSpawnSettings; import net.minecraft.world.level.block.Blocks; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; @@ -20,6 +26,12 @@ class GeomeTransitionTest { private static final ResourceLocation WINDSWEPT_HILLS = new ResourceLocation("minecraft:windswept_hills"); + @BeforeAll + static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + @Test void configuredBiomeWeightsWorkWithoutAForgeBiomeRegistryEntry() { Map indexes = new LinkedHashMap<>(); @@ -32,6 +44,19 @@ void configuredBiomeWeightsWorkWithoutAForgeBiomeRegistryEntry() { assertEquals(1, config.pickGeome(null, WINDSWEPT_HILLS, new double[2], 0.0D)); } + @Test + void explicitBiomeIdentifierWinsOverAliasedBiomeObjectIdentity() { + Biome aliasedBiome = testBiome(); + ResourceLocation dynamicId = new ResourceLocation("cakeworld", "peppermint_pinewoods"); + double[] identityWeights = { 12.0D, 1.0D }; + double[] identifierWeights = { 1.0D, 12.0D }; + BakedGeomeConfig config = config(Map.of(aliasedBiome, identityWeights), + Map.of(dynamicId, identifierWeights)); + + assertEquals(1, config.pickGeome(aliasedBiome, dynamicId, new double[2], 0.0D), + "a stable dynamic biome key must override a conflicting object-identity alias"); + } + @Test void identifierFallbackRetainsDictionaryWeightContributions() { Map indexes = new LinkedHashMap<>(); @@ -94,6 +119,11 @@ void transitionBandUsesBothGeomesButKeepsClearDominanceOutsideIt() { } private static BakedGeomeConfig config(Map biomeWeightsById) { + return config(Collections.emptyMap(), biomeWeightsById); + } + + private static BakedGeomeConfig config(Map biomeWeights, + Map biomeWeightsById) { double[] familyWeights = { 1.0D, 1.0D, 1.0D, 1.0D }; GeomeDefinition[] geomes = { new GeomeDefinition("orespawn:first", 1.0D, familyWeights.clone()), @@ -106,7 +136,25 @@ private static BakedGeomeConfig config(Map biomeWeig FormationSettings formations = new FormationSettings(FormationSettings.Algorithm.STABLE_LAYERS, 256.0D, 100.0D, 8, 48.0D, 64.0D, 12.0D, 2, 0.85D); return new BakedGeomeConfig(geomes, 384.0D, 1.15D, 0.9D, 0.45D, - Collections.emptyMap(), biomeWeightsById, rocks, formations); + biomeWeights, biomeWeightsById, rocks, formations); + } + + private static Biome testBiome() { + BiomeSpecialEffects effects = new BiomeSpecialEffects.Builder() + .fogColor(0xC0D8FF) + .waterColor(0x3F76E4) + .waterFogColor(0x050533) + .skyColor(0x78A7FF) + .build(); + return new Biome.BiomeBuilder() + .precipitation(Biome.Precipitation.NONE) + .biomeCategory(Biome.BiomeCategory.NONE) + .temperature(0.5F) + .downfall(0.5F) + .specialEffects(effects) + .mobSpawnSettings(MobSpawnSettings.EMPTY) + .generationSettings(BiomeGenerationSettings.EMPTY) + .build(); } private static BakedGeomeConfig observedWorldConfig() { diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookupTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookupTest.java new file mode 100644 index 00000000..9979e613 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookupTest.java @@ -0,0 +1,27 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +class TerrainBiomeLookupTest { + @Test + void geologyAndSamplerHeightsResolveThroughTheSameQuartBiome() { + AtomicReference coordinates = new AtomicReference<>(); + assertNull(TerrainBiomeLookup.atBlock((x, y, z) -> { + coordinates.set(x + "," + y + "," + z); + return null; + }, 13, 62, -32)); + assertEquals("3,15,-8", coordinates.get()); + + assertNull(TerrainBiomeLookup.atBlock((x, y, z) -> { + coordinates.set(x + "," + y + "," + z); + return null; + }, 13, 63, -32)); + assertEquals("3,15,-8", coordinates.get(), + "later surface work must not move an adjacent height into a fuzzy biome cell"); + } +} From 4a9f9447293f4f6c088ef498a6f91216ba4c77f7 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 31 Aug 2026 09:25:34 +0100 Subject: [PATCH 6/6] Let GameTest harness own benchmark shutdown --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.txt | 7 +++++++ README.md | 2 +- build.gradle | 10 +++++----- docs/VERSIONS.md | 9 +++++---- gradle.properties | 2 +- .../orespawn/worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../mc/orespawn/worldgen/WorldgenBenchmark.java | 14 ++++++++++++-- .../worldgen/WorldgenBenchmarkTest.java | 17 +++++++++++++++++ 10 files changed, 53 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd58f6f5..1fad08cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.15.118021.jar - build/libs/OreSpawn-4.0.15.118021-sources.jar - build/libs/OreSpawn-4.0.15.118021-javadoc.jar + build/libs/OreSpawn-4.0.16.118021.jar + build/libs/OreSpawn-4.0.16.118021-sources.jar + build/libs/OreSpawn-4.0.16.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a46a1494..40222c33 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.16.118021 + +* Leave benchmark shutdown to the GameTest harness when a benchmark is run + through Forge's GameTest server, preventing a null test-tracker crash and + allowing the harness to report its real test result. +* Ordinary dedicated benchmark servers still stop automatically when requested. + Version 4.0.15.118021 * Classify generated geology and public geology samples through the same diff --git a/README.md b/README.md index 926f0cca..4cf000c1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.15.118021`: the OreSpawn 4.0.15 +This branch builds target-qualified version `4.0.16.118021`: the OreSpawn 4.0.16 feature set for Minecraft 1.18.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index d596f214..a1a5b7f0 100644 --- a/build.gradle +++ b/build.gradle @@ -745,7 +745,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.15.118021' + if (project.mod_version != '4.0.16.118021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.18.2' || project.forge_version != '40.3.0' @@ -759,9 +759,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.15.118021.jar', - 'OreSpawn-4.0.15.118021-sources.jar', - 'OreSpawn-4.0.15.118021-javadoc.jar' + 'OreSpawn-4.0.16.118021.jar', + 'OreSpawn-4.0.16.118021-sources.jar', + 'OreSpawn-4.0.16.118021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -778,7 +778,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.15.118021')) { + if (!file(path).getText('UTF-8').contains('4.0.16.118021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index a09d5782..57854b60 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -56,7 +56,7 @@ Examples: | 1.15.2 | Forge | `115021` | `4.0.9.115021` | | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | -| 1.18.2 | Forge | `118021` | `4.0.15.118021` | +| 1.18.2 | Forge | `118021` | `4.0.16.118021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -158,9 +158,10 @@ highest occupied block as chunk generation at vertical biome seams. It then advanced to `4.0.13.118021` to restore API biome-filter parity and accept provider-namespaced geomes in the creation editor, then to `4.0.14.118021` so one-layer Snow above the motion-blocking surface is included in configured -weather-material conversion, and then to `4.0.15.118021` so generated geology -and public samples use the same stable quart-biome cell at three-dimensional -biome boundaries. A branch +weather-material conversion, then to `4.0.15.118021` so generated geology and +public samples use the same stable quart-biome cell at three-dimensional biome +boundaries, and then to `4.0.16.118021` so GameTest benchmark runs leave +shutdown and result reporting to the test harness. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index f3cd7b7a..eb7a345a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20220404.173914 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.15.118021 +mod_version=4.0.16.118021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 613dfccc..6aa580fd 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.15.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index 30bf5221..58242430 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.15.118021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.118021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmark.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmark.java index de507af3..f561762e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmark.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmark.java @@ -12,8 +12,10 @@ import net.minecraft.core.Holder; import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; +import net.minecraft.gametest.framework.GameTestServer; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; @@ -118,11 +120,19 @@ private static void onServerStarted(ServerStartedEvent event) { MODE, chunks, repetitions, format(median), format(median / chunks), format(sorted[0]), format(sorted[sorted.length - 1])); if (Boolean.getBoolean("orespawn.worldgenBenchmarkStopServer")) { - LOGGER.info("ORESPAWN_BENCHMARK stopping server after completed benchmark"); - event.getServer().halt(false); + if (ownsServerShutdown(event.getServer().getClass())) { + LOGGER.info("ORESPAWN_BENCHMARK stopping server after completed benchmark"); + event.getServer().halt(false); + } else { + LOGGER.info("ORESPAWN_BENCHMARK leaving shutdown to the GameTest harness"); + } } } + static boolean ownsServerShutdown(Class serverType) { + return !GameTestServer.class.isAssignableFrom(serverType); + } + static ResourceKey benchmarkDimensionKey(String configured) { String dimensionName = configured.trim().toLowerCase(Locale.ROOT); return switch (dimensionName) { diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java index 11d03808..bfaf9bbb 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java @@ -3,14 +3,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import net.minecraft.SharedConstants; import net.minecraft.core.Registry; +import net.minecraft.gametest.framework.GameTestServer; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.Level; class WorldgenBenchmarkTest { + @BeforeAll + static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + @Test void resolvesVanillaAliasesAndCustomDimensionIds() { assertEquals(Level.OVERWORLD, WorldgenBenchmark.benchmarkDimensionKey("overworld")); @@ -26,4 +37,10 @@ void rejectsInvalidCustomDimensionIds() { assertThrows(IllegalArgumentException.class, () -> WorldgenBenchmark.benchmarkDimensionKey("not a dimension")); } + + @Test + void leavesGameTestHarnessInControlOfServerShutdown() { + assertEquals(false, WorldgenBenchmark.ownsServerShutdown(GameTestServer.class)); + assertEquals(true, WorldgenBenchmark.ownsServerShutdown(MinecraftServer.class)); + } }