From 10011c22b8137617ca836ecf123d2e9d94578f15 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 31 Aug 2026 15:28:39 +0100 Subject: [PATCH 1/5] 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 | 6 ++++-- 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, 57 insertions(+), 19 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 1f6674f9..9b7b1803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.11.119041.jar - build/libs/OreSpawn-4.0.11.119041-sources.jar - build/libs/OreSpawn-4.0.11.119041-javadoc.jar + build/libs/OreSpawn-4.0.12.119041.jar + build/libs/OreSpawn-4.0.12.119041-sources.jar + build/libs/OreSpawn-4.0.12.119041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3c13841f..10937ce6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.12.119041 + +* 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.119041 * Preserve biome-dictionary geome weights when a data-driven biome is reached diff --git a/README.md b/README.md index 0808d2f7..396f9f41 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.119041`: the OreSpawn 4.0.11 +This branch builds target-qualified version `4.0.12.119041`: the OreSpawn 4.0.12 feature set for Minecraft 1.19.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 2b6722e1..6d53bafe 100644 --- a/build.gradle +++ b/build.gradle @@ -845,7 +845,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.11.119041' + if (project.mod_version != '4.0.12.119041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.19.4' || project.forge_version != '45.4.0' @@ -859,9 +859,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.11.119041.jar', - 'OreSpawn-4.0.11.119041-sources.jar', - 'OreSpawn-4.0.11.119041-javadoc.jar' + 'OreSpawn-4.0.12.119041.jar', + 'OreSpawn-4.0.12.119041-sources.jar', + 'OreSpawn-4.0.12.119041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -878,7 +878,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.119041')) { + if (!file(path).getText('UTF-8').contains('4.0.12.119041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index 1835f8b0..5f05a9de 100644 --- a/docs/API.md +++ b/docs/API.md @@ -126,9 +126,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 614157cc..1bc2e065 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -57,7 +57,7 @@ Examples: | 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.19.4 | Forge | `119041` | `4.0.11.119041` | +| 1.19.4 | Forge | `119041` | `4.0.12.119041` | | 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` | @@ -157,7 +157,9 @@ therefore legitimately skip functional version numbers. Forge 1.19.4 then advanced to `4.0.11.119041` to retain biome-dictionary weights and ore biome filters when a data-driven biome is represented by a -different runtime object with the same stable registry key. +different runtime object with the same stable registry key, and to +`4.0.12.119041` so public geology samples classify the same highest occupied +block as chunk generation at vertical biome seams. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 45c230d3..fb4a18b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20230314.122934 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.11.119041 +mod_version=4.0.12.119041 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 6e87f991..853d587c 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.12.119041 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 5c9a3b51..1fd9b96f 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.12.119041 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 40d7e94d938137a0b63a14a3c822002ab5725aa2 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 31 Aug 2026 15:35:23 +0100 Subject: [PATCH 2/5] 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 | 8 ++- 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, 184 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b7b1803..b93b50ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.12.119041.jar - build/libs/OreSpawn-4.0.12.119041-sources.jar - build/libs/OreSpawn-4.0.12.119041-javadoc.jar + build/libs/OreSpawn-4.0.13.119041.jar + build/libs/OreSpawn-4.0.13.119041-sources.jar + build/libs/OreSpawn-4.0.13.119041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 10937ce6..d4c2697d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.13.119041 + +* 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.119041 * Classify public geology samples at the same highest occupied block used by diff --git a/README.md b/README.md index 396f9f41..afc065e8 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.119041`: the OreSpawn 4.0.12 +This branch builds target-qualified version `4.0.13.119041`: the OreSpawn 4.0.13 feature set for Minecraft 1.19.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 6d53bafe..0977bf3f 100644 --- a/build.gradle +++ b/build.gradle @@ -845,7 +845,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.12.119041' + if (project.mod_version != '4.0.13.119041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.19.4' || project.forge_version != '45.4.0' @@ -859,9 +859,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.12.119041.jar', - 'OreSpawn-4.0.12.119041-sources.jar', - 'OreSpawn-4.0.12.119041-javadoc.jar' + 'OreSpawn-4.0.13.119041.jar', + 'OreSpawn-4.0.13.119041-sources.jar', + 'OreSpawn-4.0.13.119041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -878,7 +878,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.119041')) { + if (!file(path).getText('UTF-8').contains('4.0.13.119041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index 5f05a9de..d53b71a1 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: 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 a15356b1..0431df91 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 1bc2e065..463e71ea 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -57,7 +57,7 @@ Examples: | 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.19.4 | Forge | `119041` | `4.0.12.119041` | +| 1.19.4 | Forge | `119041` | `4.0.13.119041` | | 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` | @@ -157,9 +157,11 @@ therefore legitimately skip functional version numbers. Forge 1.19.4 then advanced to `4.0.11.119041` to retain biome-dictionary weights and ore biome filters when a data-driven biome is represented by a -different runtime object with the same stable registry key, and to +different runtime object with the same stable registry key, to `4.0.12.119041` so public geology samples classify the same highest occupied -block as chunk generation at vertical biome seams. +block as chunk generation at vertical biome seams, and to `4.0.13.119041` to +restore API biome-filter parity and accept provider-namespaced geomes in the +creation editor. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index fb4a18b3..e88db376 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20230314.122934 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.12.119041 +mod_version=4.0.13.119041 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 7dc4f62b..53bb146b 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -19,6 +19,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; @@ -80,6 +82,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, @@ -123,6 +127,23 @@ public SurfaceProbeTestMod() { 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) @@ -200,7 +221,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(); @@ -230,49 +250,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 b5fdab5e..009939bb 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -637,7 +637,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(); @@ -697,7 +697,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; } @@ -1156,6 +1156,13 @@ private static boolean validBlock(String id) { return block != null && block != Blocks.AIR; } + 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()); + } + private static String safePath(String registryId) { return registryId.toLowerCase(Locale.ROOT).replace(':', '/') .replaceAll("[^a-z0-9_./-]", "_"); 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 853d587c..62cbe0ca 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.13.119041 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 1fd9b96f..0d2d4da6 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.13.119041 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..ff7aa819 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 2f7f9548..d3f0cd47 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java @@ -28,6 +28,29 @@ void emptyStandaloneProfileIsValidAndFirstRockActivatesOverworldTerrain() { assertTrue(overworld.getAsJsonArray("host_blocks").toString().contains("minecraft:deepslate")); } + @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()); + } + @Test void firstUseStrataStartsWithBalancedVanillaRocks() { GeologyEditorSession session = new GeologyEditorSession(WorldGeologyProfile.recommended(false)); From 8c23ef6fa1395047293a60c35f358fcba619b04b Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 31 Aug 2026 15:39:09 +0100 Subject: [PATCH 3/5] Fix exposed snow material conversion --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 7 ++ README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 5 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 115 +++++++++++++++++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/WorldMaterialWeather.java | 8 ++ 10 files changed, 142 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b93b50ba..ef2a70ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.13.119041.jar - build/libs/OreSpawn-4.0.13.119041-sources.jar - build/libs/OreSpawn-4.0.13.119041-javadoc.jar + build/libs/OreSpawn-4.0.14.119041.jar + build/libs/OreSpawn-4.0.14.119041-sources.jar + build/libs/OreSpawn-4.0.14.119041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d4c2697d..f0998d20 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.14.119041 + +* Convert naturally exposed one-layer Snow at the first free block above the + motion-blocking surface while retaining the existing Snow and Ice scan. +* Preserve buried or authored Snow and Ice, unconfigured dimensions, fluids, + bedrock, block entities, profiles, schemas, and existing chunks. + Version 4.0.13.119041 * Give the public ore-dimension builder the exact biome include/exclude and diff --git a/README.md b/README.md index afc065e8..ee35308b 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.119041`: the OreSpawn 4.0.13 +This branch builds target-qualified version `4.0.14.119041`: the OreSpawn 4.0.14 feature set for Minecraft 1.19.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 0977bf3f..eb247dd8 100644 --- a/build.gradle +++ b/build.gradle @@ -845,7 +845,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.13.119041' + if (project.mod_version != '4.0.14.119041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.19.4' || project.forge_version != '45.4.0' @@ -859,9 +859,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.13.119041.jar', - 'OreSpawn-4.0.13.119041-sources.jar', - 'OreSpawn-4.0.13.119041-javadoc.jar' + 'OreSpawn-4.0.14.119041.jar', + 'OreSpawn-4.0.14.119041-sources.jar', + 'OreSpawn-4.0.14.119041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -878,7 +878,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.119041')) { + if (!file(path).getText('UTF-8').contains('4.0.14.119041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 463e71ea..0bea2129 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -57,7 +57,7 @@ Examples: | 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.19.4 | Forge | `119041` | `4.0.13.119041` | +| 1.19.4 | Forge | `119041` | `4.0.14.119041` | | 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` | @@ -161,7 +161,8 @@ different runtime object with the same stable registry key, to `4.0.12.119041` so public geology samples classify the same highest occupied block as chunk generation at vertical biome seams, and to `4.0.13.119041` to restore API biome-filter parity and accept provider-namespaced geomes in the -creation editor. +creation editor, and to `4.0.14.119041` to convert exposed one-layer Snow +without touching buried or authored weather materials. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index e88db376..4644efd7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20230314.122934 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.13.119041 +mod_version=4.0.14.119041 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 53bb146b..1ef8f99b 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -108,6 +108,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(); static { FEATURES.register("terrain_setup", () -> new ProbeFeature(ProbeStage.TERRAIN)); @@ -156,6 +158,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())) { @@ -360,6 +365,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++) { @@ -441,6 +452,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(); } } @@ -448,13 +467,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 @@ -470,6 +495,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); @@ -477,7 +508,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) { @@ -693,6 +763,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; } @@ -856,9 +932,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; @@ -892,11 +993,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 62cbe0ca..c205160b 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.14.119041 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 0d2d4da6..c804266d 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.14.119041 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 14dcbf02..1366d03c 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 d471891d34b772773f9579df2f5e96b9d68d8f1e Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 31 Aug 2026 15:47:15 +0100 Subject: [PATCH 4/5] 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 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 101 ++++++++++++++++-- .../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 | 40 ++++++- .../worldgen/TerrainBiomeLookupTest.java | 27 +++++ 17 files changed, 221 insertions(+), 45 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 ef2a70ea..5b1d40d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.14.119041.jar - build/libs/OreSpawn-4.0.14.119041-sources.jar - build/libs/OreSpawn-4.0.14.119041-javadoc.jar + build/libs/OreSpawn-4.0.15.119041.jar + build/libs/OreSpawn-4.0.15.119041-sources.jar + build/libs/OreSpawn-4.0.15.119041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f0998d20..5d58356e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.15.119041 + +* 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.119041 * Convert naturally exposed one-layer Snow at the first free block above the diff --git a/README.md b/README.md index ee35308b..7e760572 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.119041`: the OreSpawn 4.0.14 +This branch builds target-qualified version `4.0.15.119041`: the OreSpawn 4.0.15 feature set for Minecraft 1.19.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index eb247dd8..ad74f362 100644 --- a/build.gradle +++ b/build.gradle @@ -845,7 +845,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.14.119041' + if (project.mod_version != '4.0.15.119041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.19.4' || project.forge_version != '45.4.0' @@ -859,9 +859,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.14.119041.jar', - 'OreSpawn-4.0.14.119041-sources.jar', - 'OreSpawn-4.0.14.119041-javadoc.jar' + 'OreSpawn-4.0.15.119041.jar', + 'OreSpawn-4.0.15.119041-sources.jar', + 'OreSpawn-4.0.15.119041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -878,7 +878,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.119041')) { + if (!file(path).getText('UTF-8').contains('4.0.15.119041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index d53b71a1..1f261000 100644 --- a/docs/API.md +++ b/docs/API.md @@ -135,9 +135,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 0bea2129..35dc357d 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -57,7 +57,7 @@ Examples: | 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.19.4 | Forge | `119041` | `4.0.14.119041` | +| 1.19.4 | Forge | `119041` | `4.0.15.119041` | | 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` | @@ -162,7 +162,9 @@ different runtime object with the same stable registry key, to block as chunk generation at vertical biome seams, and to `4.0.13.119041` to restore API biome-filter parity and accept provider-namespaced geomes in the creation editor, and to `4.0.14.119041` to convert exposed one-layer Snow -without touching buried or authored weather materials. +without touching buried or authored weather materials. It then advanced to +`4.0.15.119041` so generated geology and public samples use the same stable +quart-biome cell at three-dimensional biome boundaries. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 4644efd7..ae81977e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20230314.122934 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.14.119041 +mod_version=4.0.15.119041 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 1ef8f99b..3ff785a2 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -18,6 +18,7 @@ import zone.moddev.mc.orespawn.api.BiomeRegionSize; import zone.moddev.mc.orespawn.api.BiomeReplacementScope; import zone.moddev.mc.orespawn.api.GeologyFamily; +import zone.moddev.mc.orespawn.api.GeologySampler; import zone.moddev.mc.orespawn.api.OreSpawnApi; import zone.moddev.mc.orespawn.api.OreHeightDistribution; import zone.moddev.mc.orespawn.api.OrePattern; @@ -141,7 +142,6 @@ private void enqueueProvider(InterModEnqueueEvent event) { .spread(4, 3) .nodeSize(3) .hostBlock(blockId(Blocks.CALCITE)) - .hostBlock(blockId(Blocks.BASALT)) .biome(BIOME_A) .biomeDictionary("COLD") .excludeBiome(BIOME_B) @@ -155,7 +155,8 @@ private void enqueueProvider(InterModEnqueueEvent event) { .maxLobes(1) .minSolidCover(1) .minSolidShell(1) - .hostBlock(blockId(Blocks.CALCITE)))); + .hostBlock(blockId(Blocks.CALCITE)) + .hostBlock(blockId(Blocks.BASALT)))); addPalette(provider, "open_palette", OPEN_ID, false); addPalette(provider, "roofed_palette", ROOFED_ID, true); provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/end"), OPEN_ID, @@ -171,20 +172,23 @@ private void enqueueProvider(InterModEnqueueEvent event) { private static void addDynamicBiomeGeology(WorldgenProvider.Builder provider) { provider.geome(PROBE_GEOME, geome -> geome .baseWeight(0.0D) - .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); + .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D) + .familyWeight(GeologyFamily.IGNEOUS_INTRUSIVE, 1.0D)); provider.geome(PROBE_GEOME_ALTERNATIVE, geome -> geome .baseWeight(0.0D) - .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); + .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D) + .familyWeight(GeologyFamily.IGNEOUS_INTRUSIVE, 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); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 1.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 -> { + GeologyFamily.IGNEOUS_INTRUSIVE, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); + rock.yRange(16, 48); rock.geomeWeight(PROBE_GEOME, 0.0D); rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 1.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); @@ -200,7 +204,7 @@ private static void addDynamicBiomeGeology(WorldgenProvider.Builder provider) { 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)); + provider.biome(BIOME_B, java.util.Collections.singletonMap(PROBE_GEOME_ALTERNATIVE, 100.0D)); } private void enableGeologyProbe(ServerAboutToStartEvent event) { @@ -463,6 +467,12 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } } + AttributionAudit attribution = roofed ? AttributionAudit.EMPTY : auditStableBiomeAttribution(level); + if (!roofed) { + LOGGER.info("Surface probe stable attribution: sedimentary={}, intrusive={}, biomeA={}, biomeB={}, mismatches={}", + attribution.sedimentaryHosts(), attribution.intrusiveHosts(), + attribution.biomeAHosts(), attribution.biomeBHosts(), attribution.mismatches()); + } 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 @@ -480,6 +490,9 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { || exposedSnowConverted != 9 || surfaceIceConverted != 9 || buriedSnowPreserved != 9 || buriedIcePreserved != 9 || unconfiguredSnowPreserved != 0 || unconfiguredIcePreserved != 0 + || attribution.sedimentaryHosts() == 0 || attribution.intrusiveHosts() == 0 + || attribution.biomeAHosts() == 0 || attribution.biomeBHosts() == 0 + || attribution.mismatches() != 0 || dynamicBiomeOre == 0))) { throw new IllegalStateException("Incomplete surface audit for " + level.dimension().location() + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler @@ -501,6 +514,11 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { + ", buriedIcePreserved=" + buriedIcePreserved + ", unconfiguredSnowPreserved=" + unconfiguredSnowPreserved + ", unconfiguredIcePreserved=" + unconfiguredIcePreserved + + ", attributionSedimentary=" + attribution.sedimentaryHosts() + + ", attributionIntrusive=" + attribution.intrusiveHosts() + + ", attributionBiomeA=" + attribution.biomeAHosts() + + ", attributionBiomeB=" + attribution.biomeBHosts() + + ", attributionMismatches=" + attribution.mismatches() + ", dynamicBiomeOre=" + dynamicBiomeOre); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); @@ -511,7 +529,48 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { dictionaryPrimary, dictionaryAlternative, dynamicBiomeOre, exposedSnowConverted, surfaceIceConverted, buriedSnowPreserved, buriedIcePreserved, - unconfiguredSnowPreserved, unconfiguredIcePreserved); + unconfiguredSnowPreserved, unconfiguredIcePreserved, + attribution.sedimentaryHosts(), attribution.intrusiveHosts(), + attribution.biomeAHosts(), attribution.biomeBHosts(), attribution.mismatches()); + } + + private static AttributionAudit auditStableBiomeAttribution(ServerLevel level) { + GeologySampler sampler = OreSpawnApi.createSampler(level) + .orElseThrow(() -> new IllegalStateException("Surface probe geology sampler unavailable")); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + long sedimentary = 0L; + long intrusive = 0L; + long biomeA = 0L; + long biomeB = 0L; + long mismatches = 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++) { + var column = sampler.sampleColumn(x, z, + level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z)); + for (int y = 16; y <= 48; y++) { + BlockState state = chunk.getBlockState(pos.set(x, y, z)); + GeologyFamily expected; + if (state.is(Blocks.CALCITE)) { + expected = GeologyFamily.SEDIMENTARY; + sedimentary++; + } else if (state.is(Blocks.BASALT)) { + expected = GeologyFamily.IGNEOUS_INTRUSIVE; + intrusive++; + } else { + continue; + } + if (BIOME_A.equals(column.biome())) biomeA++; + if (BIOME_B.equals(column.biome())) biomeB++; + if (!column.familyAt(y).filter(expected::equals).isPresent()) mismatches++; + } + } + } + } + } + return new AttributionAudit(sedimentary, intrusive, biomeA, biomeB, mismatches); } private static WeatherMaterialAudit auditWeatherMaterials(ChunkAccess chunk, @@ -551,6 +610,8 @@ private static long assertState(ChunkAccess chunk, BlockPos pos, } private static long auditDynamicBiomeOre(ServerLevel level) { + GeologySampler sampler = OreSpawnApi.createSampler(level) + .orElseThrow(() -> new IllegalStateException("Dynamic ore geology sampler unavailable")); BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); long count = 0L; for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -559,7 +620,15 @@ private static long auditDynamicBiomeOre(ServerLevel level) { 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 (chunk.getBlockState(pos.set(x, y, z)).is(Blocks.DIAMOND_BLOCK)) { + var column = sampler.sampleColumn(x, z, + level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z)); + if (!column.familyAt(y).filter(GeologyFamily.SEDIMENTARY::equals).isPresent()) { + throw new IllegalStateException("Managed ore escaped its sedimentary biome host at " + pos + + ": biome=" + column.biome() + ", family=" + column.familyAt(y)); + } + count++; + } } } } @@ -769,6 +838,11 @@ private static Properties properties(long seed, Map results 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())); + values.setProperty(prefix + "attribution_sedimentary", Long.toString(result.attributionSedimentary())); + values.setProperty(prefix + "attribution_intrusive", Long.toString(result.attributionIntrusive())); + values.setProperty(prefix + "attribution_biome_a", Long.toString(result.attributionBiomeA())); + values.setProperty(prefix + "attribution_biome_b", Long.toString(result.attributionBiomeB())); + values.setProperty(prefix + "attribution_mismatches", Long.toString(result.attributionMismatches())); } return values; } @@ -998,6 +1072,11 @@ private record WeatherMaterialAudit(long exposedSnowConverted, long buriedIcePreserved, long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { } + private record AttributionAudit(long sedimentaryHosts, long intrusiveHosts, + long biomeAHosts, long biomeBHosts, long mismatches) { + private static final AttributionAudit EMPTY = new AttributionAudit(0L, 0L, 0L, 0L, 0L); + } + 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, @@ -1007,5 +1086,7 @@ private record AuditResult(long top, long underwater, long filler, long geology, long dictionaryPrimary, long dictionaryAlternative, long dynamicBiomeOre, long exposedSnowConverted, long surfaceIceConverted, long buriedSnowPreserved, long buriedIcePreserved, - long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { } + long unconfiguredSnowPreserved, long unconfiguredIcePreserved, + long attributionSedimentary, long attributionIntrusive, + long attributionBiomeA, long attributionBiomeB, long attributionMismatches) { } } 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 c205160b..9dadbf9b 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.15.119041 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 c804266d..83f6c6a3 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.15.119041 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 95aafc3b..88c19b42 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java @@ -14,6 +14,9 @@ import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; 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; @@ -34,6 +37,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<>(); @@ -96,6 +112,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()), @@ -108,7 +129,24 @@ 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() + .hasPrecipitation(false) + .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 695bf4ffd9cd35a69ec5cbd1b312452c41bc4d28 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 31 Aug 2026 15:49:19 +0100 Subject: [PATCH 5/5] Let GameTest harness own benchmark shutdown --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.txt | 7 +++++++ README.md | 2 +- build.gradle | 10 +++++----- docs/VERSIONS.md | 6 ++++-- gradle.properties | 2 +- .../mc/orespawn/worldgen/LegacyConfigMigrator.java | 2 +- .../worldgen/LegacyMineralogyProfileMigration.java | 2 +- .../mc/orespawn/worldgen/WorldgenBenchmark.java | 14 ++++++++++++-- .../orespawn/worldgen/WorldgenBenchmarkTest.java | 8 ++++++++ 10 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b1d40d1..66c9912d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.15.119041.jar - build/libs/OreSpawn-4.0.15.119041-sources.jar - build/libs/OreSpawn-4.0.15.119041-javadoc.jar + build/libs/OreSpawn-4.0.16.119041.jar + build/libs/OreSpawn-4.0.16.119041-sources.jar + build/libs/OreSpawn-4.0.16.119041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d58356e..70b97373 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.16.119041 + +* 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.119041 * Classify generated geology and public geology samples through the same diff --git a/README.md b/README.md index 7e760572..b4408cf4 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.119041`: the OreSpawn 4.0.15 +This branch builds target-qualified version `4.0.16.119041`: the OreSpawn 4.0.16 feature set for Minecraft 1.19.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index ad74f362..91beb33b 100644 --- a/build.gradle +++ b/build.gradle @@ -845,7 +845,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.15.119041' + if (project.mod_version != '4.0.16.119041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.19.4' || project.forge_version != '45.4.0' @@ -859,9 +859,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.15.119041.jar', - 'OreSpawn-4.0.15.119041-sources.jar', - 'OreSpawn-4.0.15.119041-javadoc.jar' + 'OreSpawn-4.0.16.119041.jar', + 'OreSpawn-4.0.16.119041-sources.jar', + 'OreSpawn-4.0.16.119041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -878,7 +878,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.119041')) { + if (!file(path).getText('UTF-8').contains('4.0.16.119041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 35dc357d..5630100d 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -57,7 +57,7 @@ Examples: | 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.19.4 | Forge | `119041` | `4.0.15.119041` | +| 1.19.4 | Forge | `119041` | `4.0.16.119041` | | 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` | @@ -164,7 +164,9 @@ restore API biome-filter parity and accept provider-namespaced geomes in the creation editor, and to `4.0.14.119041` to convert exposed one-layer Snow without touching buried or authored weather materials. It then advanced to `4.0.15.119041` so generated geology and public samples use the same stable -quart-biome cell at three-dimensional biome boundaries. +quart-biome cell at three-dimensional biome boundaries, and to +`4.0.16.119041` so GameTest benchmark runs leave shutdown and result reporting +to the test harness. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index ae81977e..4996aa57 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20230314.122934 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.15.119041 +mod_version=4.0.16.119041 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 9dadbf9b..297035ce 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.16.119041 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 83f6c6a3..8a5678e4 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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.16.119041 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 4eb4c23d..0762e100 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmark.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmark.java @@ -15,8 +15,10 @@ import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; +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; @@ -140,11 +142,19 @@ MODE, chunks, repetitions, format(median), format(median / chunks), throw new IllegalStateException("Benchmark fluid audit found no successful deposits"); } 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 da17219b..eb13ece9 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java @@ -5,9 +5,11 @@ import org.junit.jupiter.api.Test; +import net.minecraft.gametest.framework.GameTestServer; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.Level; class WorldgenBenchmarkTest { @@ -26,4 +28,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)); + } }