From e12d25ecd5325aa8cbd53b445baef62dc58dba9a Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:06:06 +0100 Subject: [PATCH 1/9] Fix Stable Layers rock height eligibility --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 ++ README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 10 +- gradle.properties | 2 +- .../orespawn/worldgen/BakedGeomeConfig.java | 115 +++++++++++++++--- .../mc/orespawn/worldgen/GeomeGeology.java | 14 +-- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../StableLayerHeightEligibilityTest.java | 47 +++++++ 11 files changed, 182 insertions(+), 37 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 772269cc..34bf50fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.9.116051.jar - build/libs/OreSpawn-4.0.9.116051-sources.jar - build/libs/OreSpawn-4.0.9.116051-javadoc.jar + build/libs/OreSpawn-4.0.10.116051.jar + build/libs/OreSpawn-4.0.10.116051-sources.jar + build/libs/OreSpawn-4.0.10.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9446c76c..ee0be188 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.10.116051 + +* Evaluate Stable Layers rock min_y and max_y bounds against actual world Y + instead of the vertically shifted formation coordinate. +* Preserve the shifted formation identity for layer, family, and rock choice + while preventing vanilla Stone fallback near dimension floors and ceilings. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + Version 4.0.9.116051 * Replace provider-declared natural terrain hosts during the existing geology diff --git a/README.md b/README.md index 468d3214..9189cfe9 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.9.116051`: the OreSpawn 4.0.9 +This branch builds target-qualified version `4.0.10.116051`: the OreSpawn 4.0.10 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 425a28c1..a9433d5b 100644 --- a/build.gradle +++ b/build.gradle @@ -733,7 +733,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.9.116051' + if (project.mod_version != '4.0.10.116051' || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' || project.mapping_channel != 'official' @@ -746,9 +746,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.9.116051.jar', - 'OreSpawn-4.0.9.116051-sources.jar', - 'OreSpawn-4.0.9.116051-javadoc.jar' + 'OreSpawn-4.0.10.116051.jar', + 'OreSpawn-4.0.10.116051-sources.jar', + 'OreSpawn-4.0.10.116051-javadoc.jar' ] if (base.archivesName.get() != 'OreSpawn' || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -765,7 +765,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.9.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.10.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 04099b8e..fbf190ab 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.9.116051` | +| 1.16.5 | Forge | `116051` | `4.0.10.116051` | | 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` | @@ -146,9 +146,11 @@ unaffected branches remained on their target-qualified 4.0.6 versions. If a different branch later receives a shared fix, it uses the next unused Bug number, such as Forge 1.14.4's `4.0.8.114041`, even though the 4.0.7 repair -was not applicable there. Forge 1.15.2 and 1.16.5 then advanced to their -target-qualified 4.0.9 releases for the provider terrain-host ordering repair. -A branch may therefore legitimately skip functional version numbers. +was not applicable there. Forge 1.15.2, 1.16.5, and 1.17.1 then advanced to +their target-qualified 4.0.9 releases for the provider terrain-host ordering +repair. Forge 1.16.5 and later targets then advanced to 4.0.10 for the distinct +Stable Layers actual-height eligibility repair. A branch may therefore +legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 15f53e9a..a1a4ee9e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.9.116051 +mod_version=4.0.10.116051 mod_group=zone.moddev.mc 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/BakedGeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java index 37d10317..d89760a5 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java @@ -35,6 +35,7 @@ public final class BakedGeomeConfig { private final Map biomeWeights; private final Map biomeWeightsById; private final double[] fallbackWeights; + private final RockEntry[] rocks; private final BlockState[] rockStates; private final Set sedimentaryBlocks; private final Set oreReplaceableBlocks; @@ -45,6 +46,9 @@ public final class BakedGeomeConfig { private WeightedBlockPicker[][][] legacyRockPickers; private byte[] stableFamilyChoices; private int[] stableRockChoices; + private int[][] familyRockIndexes; + private double[][][] stableRockLogWeights; + private double[][][] stableRockPriorities; BakedGeomeConfig(GeomeDefinition[] geomes, double geomeScale, double biomeInfluence, double regionalNoiseInfluence, double boundaryNoiseInfluence, Map biomeWeights, @@ -72,6 +76,7 @@ public final class BakedGeomeConfig { noiseOffsetZ[i] = -((i + 1) * 6151); } + this.rocks = rocks.clone(); rockStates = new BlockState[rocks.length]; for (int i = 0; i < rocks.length; i++) { rockStates[i] = rocks[i].state; @@ -152,15 +157,69 @@ RockFamily pickFamily(int geomeIndex, int y, int formationValue, int diversitySl return RockFamily.SEDIMENTARY; } + RockFamily pickStableFamilyAtWorldY(int geomeIndex, int worldY, int formationY, + int formationValue, int diversitySlot) { + RockFamily preferred = pickFamily(geomeIndex, formationY, formationValue, diversitySlot); + if (hasEligibleStableRock(geomeIndex, preferred, worldY, formationY)) { + return preferred; + } + + int bucket = formationValue & 0xFF; + int boundedFormationY = clampStableValue(formationY); + double bestScore = Double.NEGATIVE_INFINITY; + RockFamily bestFamily = preferred; + for (RockFamily family : RockFamily.values()) { + if (!hasEligibleStableRock(geomeIndex, family, worldY, formationY)) { + continue; + } + double weight = Math.pow(geomes[geomeIndex].familyWeights[family.ordinal()], 2.5D) + * familyDepthWeight(family, boundedFormationY); + if (weight <= 0.0D) { + continue; + } + double score = Math.log(weight) + gumbelPriority(bucket, geomeIndex, family.ordinal(), + isStableBucket(bucket, -1), 0x6A09E667F3BCC909L); + if (score > bestScore) { + bestScore = score; + bestFamily = family; + } + } + return bestFamily; + } + public BlockState pickRock(int geomeIndex, RockFamily family, int y, int formationValue) { if (formations.usesStableLayers()) { - int index = stableRockIndex(geomeIndex, family.ordinal(), clampStableY(y), formationValue & 0xFF); - int rockIndex = stableRockChoices[index]; - return rockIndex < 0 ? FALLBACK : rockStates[rockIndex]; + return pickStableRockAtWorldY(geomeIndex, family, y, y, formationValue); } return legacyRockPickers[geomeIndex][family.ordinal()][clampLegacyY(y)].pick(formationValue); } + BlockState pickStableRockAtWorldY(int geomeIndex, RockFamily family, int worldY, + int formationY, int formationValue) { + int yIndex = clampStableY(formationY); + int bucket = formationValue & 0xFF; + int choiceIndex = stableRockIndex(geomeIndex, family.ordinal(), yIndex, bucket); + int selectedRock = stableRockChoices[choiceIndex]; + if (isEligibleStableRock(geomeIndex, selectedRock, worldY, yIndex)) { + return rockStates[selectedRock]; + } + + double bestScore = Double.NEGATIVE_INFINITY; + int bestRock = -1; + for (int rockIndex : familyRockIndexes[family.ordinal()]) { + if (!isEligibleStableRock(geomeIndex, rockIndex, worldY, yIndex)) { + continue; + } + double score = stableRockLogWeights[geomeIndex][rockIndex][yIndex] + + stableRockPriorities[geomeIndex][rockIndex][bucket]; + if (score > bestScore) { + bestScore = score; + bestRock = rockIndex; + } + } + return bestRock < 0 ? FALLBACK : rockStates[bestRock]; + } + public String geomeName(int geomeIndex) { return geomes[geomeIndex].name; } @@ -283,9 +342,9 @@ private void buildStablePickers(RockEntry[] rocks) { stableRockChoices = new int[geomes.length * RockFamily.values().length * HEIGHT * FORMATION_BUCKETS]; Arrays.fill(stableRockChoices, -1); int familyCount = RockFamily.values().length; - int[][] familyRockIndexes = groupRockIndexes(rocks); - double[][][] rockLogWeights = new double[geomes.length][rocks.length][HEIGHT]; - double[][][] rockPriorities = new double[geomes.length][rocks.length][FORMATION_BUCKETS]; + familyRockIndexes = groupRockIndexes(rocks); + stableRockLogWeights = new double[geomes.length][rocks.length][HEIGHT]; + stableRockPriorities = new double[geomes.length][rocks.length][FORMATION_BUCKETS]; double[][][] familyLogWeights = new double[geomes.length][familyCount][HEIGHT]; double[][][] familyWeights = new double[geomes.length][familyCount][HEIGHT]; double[][][] familyPriorities = new double[geomes.length][familyCount][FORMATION_BUCKETS]; @@ -294,14 +353,13 @@ private void buildStablePickers(RockEntry[] rocks) { for (int rockIndex = 0; rockIndex < rocks.length; rockIndex++) { RockEntry rock = rocks[rockIndex]; for (int y = MIN_Y; y <= MAX_Y; y++) { - double rawWeight = y < rock.minY || y > rock.maxY ? 0.0D - : rock.weight * rock.geomeWeights[geome] - * depthWeight(y, rock.depthPeak, rock.depthSpread); - rockLogWeights[geome][rockIndex][y - MIN_Y] = rawWeight > 0.0D + double rawWeight = rock.weight * rock.geomeWeights[geome] + * depthWeight(y, rock.depthPeak, rock.depthSpread); + stableRockLogWeights[geome][rockIndex][y - MIN_Y] = rawWeight > 0.0D ? Math.log(rawWeight) : Double.NEGATIVE_INFINITY; } for (int bucket = 0; bucket < FORMATION_BUCKETS; bucket++) { - rockPriorities[geome][rockIndex][bucket] = gumbelPriority(bucket, geome, rockIndex, + stableRockPriorities[geome][rockIndex][bucket] = gumbelPriority(bucket, geome, rockIndex, isStableBucket(bucket, rock.family.ordinal()), 0xBB67AE8584CAA73BL ^ ((long) rock.family.ordinal() << 32)); } @@ -313,7 +371,9 @@ private void buildStablePickers(RockEntry[] rocks) { int yIndex = y - MIN_Y; boolean available = false; for (int rockIndex : familyRockIndexes[familyIndex]) { - if (rockLogWeights[geome][rockIndex][yIndex] != Double.NEGATIVE_INFINITY) { + RockEntry rock = rocks[rockIndex]; + if (y >= rock.minY && y <= rock.maxY + && stableRockLogWeights[geome][rockIndex][yIndex] != Double.NEGATIVE_INFINITY) { available = true; break; } @@ -363,8 +423,12 @@ private void buildStablePickers(RockEntry[] rocks) { double bestRockScore = Double.NEGATIVE_INFINITY; int bestRock = -1; for (int rockIndex : familyRockIndexes[familyIndex]) { - double rockScore = rockLogWeights[geome][rockIndex][yIndex] - + rockPriorities[geome][rockIndex][bucket]; + RockEntry rock = rocks[rockIndex]; + if (y < rock.minY || y > rock.maxY) { + continue; + } + double rockScore = stableRockLogWeights[geome][rockIndex][yIndex] + + stableRockPriorities[geome][rockIndex][bucket]; if (rockScore > bestRockScore) { bestRockScore = rockScore; bestRock = rockIndex; @@ -377,6 +441,25 @@ private void buildStablePickers(RockEntry[] rocks) { } } + private boolean hasEligibleStableRock(int geomeIndex, RockFamily family, int worldY, int formationY) { + int yIndex = clampStableY(formationY); + for (int rockIndex : familyRockIndexes[family.ordinal()]) { + if (isEligibleStableRock(geomeIndex, rockIndex, worldY, yIndex)) { + return true; + } + } + return false; + } + + private boolean isEligibleStableRock(int geomeIndex, int rockIndex, int worldY, int formationYIndex) { + if (rockIndex < 0) { + return false; + } + RockEntry rock = rocks[rockIndex]; + return worldY >= rock.minY && worldY <= rock.maxY + && stableRockLogWeights[geomeIndex][rockIndex][formationYIndex] != Double.NEGATIVE_INFINITY; + } + private void fillBalancedFamilyCycle(int geome, int yIndex, int bucket, double[][][] familyWeights, double[][][] familyPriorities, int[] quotas, int[] remaining, double[] remainders, boolean[] bonusAwarded) { @@ -631,6 +714,10 @@ private static int clampStableY(int y) { return Math.max(MIN_Y, Math.min(MAX_Y, y)) - MIN_Y; } + private static int clampStableValue(int y) { + return Math.max(MIN_Y, Math.min(MAX_Y, y)); + } + private static int clampLegacyY(int y) { return Math.max(0, Math.min(LEGACY_MAX_Y, y)); } 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 972cd26a..42b336ce 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -140,7 +140,6 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.Mutable cursor, int g int layerStart = layerIndex * layerThickness; int layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - BlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); boolean changed = false; cursor.set(x, surfaceY, z); @@ -151,12 +150,12 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.Mutable cursor, int g layerStart -= layerThickness; layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); } cursor.setY(y); if (terrain.isReplaceable(chunk.getBlockState(cursor)) && chunk.getBlockEntity(cursor) == null) { - chunk.setBlockState(cursor, replacement, false); + chunk.setBlockState(cursor, + pickStableReplacement(layerGeome, formationRegion, layerIndex, y), false); changed = true; } } @@ -249,7 +248,7 @@ private net.minecraft.block.BlockState pickReplacement(int geomeIndex, int baseR int stratum = baseRockValue + y; int layerIndex = Math.floorDiv(stratum, layerThickness); if (stableLayers) { - return pickStableReplacement(geomeIndex, formationRegion, layerIndex); + return pickStableReplacement(geomeIndex, formationRegion, layerIndex, y); } int layerY = y + (layerThickness / 2) - Math.floorMod(stratum, layerThickness); @@ -259,7 +258,7 @@ private net.minecraft.block.BlockState pickReplacement(int geomeIndex, int baseR return config.pickRock(geomeIndex, family, layerY, rockHash); } - private BlockState pickStableReplacement(int geomeIndex, long formationRegion, int layerIndex) { + private BlockState pickStableReplacement(int geomeIndex, long formationRegion, int layerIndex, int worldY) { // A dipping or uplifted layer keeps the depth identity it had in stratum space. int formationY = (layerIndex * layerThickness) + (layerThickness / 2); int layerBucket = layerIndex & 0xFF; @@ -285,8 +284,9 @@ private BlockState pickStableReplacement(int geomeIndex, long formationRegion, i // from collapsing onto one exact rock. rockBucket ^= LITHOLOGY_ROCK_SALTS[familySlot]; } - RockFamily family = config.pickFamily(geomeIndex, formationY, familyBucket, familySlot); - return config.pickRock(geomeIndex, family, formationY, rockBucket); + RockFamily family = config.pickStableFamilyAtWorldY(geomeIndex, worldY, formationY, + familyBucket, familySlot); + return config.pickStableRockAtWorldY(geomeIndex, family, worldY, formationY, rockBucket); } int stratumOffsetAt(int x, int z) { 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 552920fe..3f65db2b 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,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.9.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.10.116051 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 9d7fa570..e7ef43f4 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,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.9.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.10.116051 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/worldgen/StableLayerHeightEligibilityTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java new file mode 100644 index 00000000..0d27abf2 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java @@ -0,0 +1,47 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import net.minecraft.util.registry.Bootstrap; +import net.minecraft.block.Blocks; + +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.RockEntry; + +class StableLayerHeightEligibilityTest { + static { + Bootstrap.bootStrap(); + } + + @Test + void rockBoundsUseActualWorldYWhileFormationIdentityRemainsShifted() { + BakedGeomeConfig config = netherFloorConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.BASALT, + geology.getStoneAt(0, geomeScores, -64, 0L, 0, 1, 0), + "a legal Nether Y must not fall back to Stone when waviness shifts its formation below min_y"); + assertEquals(Blocks.STONE, + geology.getStoneAt(0, geomeScores, 64, 0L, 0, -1, 0), + "a shifted formation inside the range must not make an illegal actual Y eligible"); + } + + private static BakedGeomeConfig netherFloorConfig() { + GeomeDefinition[] geomes = { + new GeomeDefinition("test:nether", 1.0D, new double[] { 0.0D, 0.0D, 0.0D, 1.0D }) + }; + RockEntry[] rocks = { + new RockEntry(Blocks.BASALT.defaultBlockState(), RockFamily.IGNEOUS_VOLCANIC, + 24, 68, 0, 127, 1.0D, true, new double[] { 1.0D }) + }; + FormationSettings formations = new FormationSettings(FormationSettings.Algorithm.STABLE_LAYERS, + 32.0D, 8192.0D, 8, 512.0D, 96.0D, 24.0D, 3, 0.85D); + return new BakedGeomeConfig(geomes, 384.0D, 1.15D, 0.9D, 0.45D, + Collections.emptyMap(), Collections.emptyMap(), rocks, formations); + } +} From a6137b4f81a5c8cf920b29ef122bcad0382dcd97 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:11:32 +0100 Subject: [PATCH 2/9] Qualify cold Forge bootstrap --- .github/workflows/ci.yml | 66 +++++++++++++++++-- .github/workflows/codeql-analysis.yml | 17 +++-- build.gradle | 36 ++++++++-- gradle.properties | 2 +- .../orespawn/ReleaseWorkflowContractTest.java | 66 +++++++++++++++++++ 5 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34bf50fb..2a2c6225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,61 @@ concurrency: cancel-in-progress: true jobs: + cold-forge-bootstrap: + name: Cold Forge bootstrap + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Bootstrap Forge from an empty cache + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle + run: | + set -euo pipefail + test ! -e .gradle + test ! -e "$GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + chmod +x ./gradlew + gradle_args=( + classes verifyLegacyFixtures + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Verify same-cache bootstrap offline + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle + run: | + set -euo pipefail + gradle_args=( + classes verifyLegacyFixtures + --rerun-tasks --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + build: name: Build, test, and audit runs-on: ubuntu-latest @@ -26,17 +81,17 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle + - name: Install pinned Java 17 Gradle runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: '17.0.1+12' - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -48,6 +103,9 @@ jobs: run: >- ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath --no-daemon --stacktrace + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 23d19749..749be529 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,17 +26,17 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle + - name: Install pinned Java 17 Gradle runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: '17.0.1+12' - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -49,7 +49,14 @@ jobs: - name: Compile production code run: | chmod +x ./gradlew - ./gradlew clean classes --no-daemon --stacktrace + gradle_args=( + clean classes + --no-daemon --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" - name: Analyze uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/build.gradle b/build.gradle index a9433d5b..f747f5e6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.jar.Manifest @@ -35,6 +36,9 @@ if (versionParts[3] != expectedTargetVersion) { ext.functional_version = versionParts[0..2].join('.') ext.display_version = project.mod_version ext.release_tag = project.mod_version +def expectedMavenGroup = 'zone.moddev.mc.orespawn' +def expectedMavenArtifact = 'OreSpawn' +def expectedMavenCoordinate = "${expectedMavenGroup}:${expectedMavenArtifact}:${project.version}" java { toolchain { @@ -677,7 +681,7 @@ tasks.named('jar', Jar) { 'Implementation-Vendor' : 'SkyBlade1978', 'OreSpawn-API-Version' : '1', 'FMLAT' : 'accesstransformer.cfg', - 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}", + 'Maven-Artifact' : expectedMavenCoordinate, 'Built-On-Java' : '8', 'Built-On' : "${project.minecraft_version}-${project.forge_version}" ]) @@ -734,6 +738,7 @@ tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { if (project.mod_version != '4.0.10.116051' + || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' || project.mapping_channel != 'official' @@ -750,7 +755,7 @@ tasks.register('verifyReleaseConfiguration') { 'OreSpawn-4.0.10.116051-sources.jar', 'OreSpawn-4.0.10.116051-javadoc.jar' ] - if (base.archivesName.get() != 'OreSpawn' + if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { throw new GradleException('Public artifacts must use the version-only OreSpawn filename contract') } @@ -932,6 +937,7 @@ tasks.register('verifyReleaseArtifacts') { || manifest.getValue('Implementation-Version') != project.mod_version || manifest.getValue('OreSpawn-API-Version') != '1' || manifest.getValue('FMLAT') != 'accesstransformer.cfg' + || manifest.getValue('Maven-Artifact') != expectedMavenCoordinate || manifest.getValue('Implementation-Timestamp') != null) { throw new GradleException('Release manifest is incorrect or volatile') } @@ -1031,8 +1037,8 @@ def mavenUploadPassword = providers.environmentVariable('MAVEN_UPLOAD_PASSWORD') publishing { publications { mavenJava(MavenPublication) { - groupId = project.group.toString() - artifactId = base.archivesName.get() + groupId = expectedMavenGroup + artifactId = expectedMavenArtifact version = project.version.toString() if (preparedReleaseDir.isPresent()) { File prepared = file(preparedReleaseDir.get()) @@ -1068,6 +1074,26 @@ publishing { } } } +tasks.register('verifyMavenCoordinates') { + group = 'verification' + description = 'Verifies the generated POM uses OreSpawn\'s mod-specific Maven namespace.' + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + doLast { + File pomFile = layout.buildDirectory.file( + 'publications/mavenJava/pom-default.xml').get().asFile + if (!pomFile.isFile()) { + throw new GradleException("Generated Maven POM does not exist: ${pomFile}") + } + def pom = new XmlSlurper(false, false).parse(pomFile) + def actual = [pom.groupId.text(), pom.artifactId.text(), pom.version.text()] + def expected = [expectedMavenGroup, expectedMavenArtifact, + project.version.toString()] + if (project.group.toString() != expectedMavenGroup || actual != expected) { + throw new GradleException("Expected Maven coordinate ${expected.join(':')}, " + + "found ${actual.join(':')}") + } + } +} tasks.register('validateMavenReleaseCredentials') { group = 'publishing' doLast { @@ -1083,6 +1109,7 @@ tasks.register('validateMavenReleaseCredentials') { } tasks.withType(PublishToMavenRepository).configureEach { dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn tasks.named('verifyMavenCoordinates') dependsOn preparedReleaseDir.isPresent() ? tasks.named('verifyPreparedReleaseArtifacts') : tasks.named('verifyReleaseArtifacts') @@ -1238,6 +1265,7 @@ tasks.register('verifyCommandPortability') { } tasks.named('check') { dependsOn tasks.named('verifyCommandPortability') } +tasks.named('check') { dependsOn tasks.named('verifyMavenCoordinates') } def packagedForgeServerRuntime = providers.gradleProperty('packagedForgeServerRuntime') def packagedForgeClientRuntime = providers.gradleProperty('packagedForgeClientRuntime') diff --git a/gradle.properties b/gradle.properties index a1a4ee9e..55f8ea43 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 mod_version=4.0.10.116051 -mod_group=zone.moddev.mc +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/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java new file mode 100644 index 00000000..07c2dd15 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java @@ -0,0 +1,66 @@ +package zone.moddev.mc.orespawn; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +class ReleaseWorkflowContractTest { + @Test + void usesOreSpawnSpecificMavenNamespace() throws Exception { + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(Paths.get("gradle.properties"))) { + properties.load(input); + } + assertEquals("zone.moddev.mc.orespawn", properties.getProperty("mod_group")); + } + + @Test + void verifiesGeneratedMavenCoordinatesBeforeCheckAndPublication() throws Exception { + Path buildFile = Paths.get("build.gradle"); + String build = new String(Files.readAllBytes(buildFile), StandardCharsets.UTF_8); + assertTrue(build.contains("tasks.register('verifyMavenCoordinates')")); + assertTrue(build.contains("generatePomFileForMavenJavaPublication")); + assertTrue(build.contains("dependsOn tasks.named('verifyMavenCoordinates')")); + assertTrue(build.contains("expectedMavenCoordinate")); + } + + @Test + void hostedWorkflowsUsePinnedJdksAndExerciseAColdForgeBootstrap() throws Exception { + String ci = readWorkflow("ci.yml"); + String codeql = readWorkflow("codeql-analysis.yml"); + + for (String workflow : new String[] { ci, codeql }) { + assertFalse(workflow.contains("distribution: microsoft")); + assertTrue(workflow.contains("java-version: '8.0.502+7'")); + assertTrue(workflow.contains("java-version: '17.0.1+12'")); + assertTrue(workflow.lastIndexOf("java-version: '17.0.1+12'") + > workflow.lastIndexOf("java-version: '8.0.502+7'")); + assertTrue(workflow.contains("$JAVA_HOME,$JAVA_HOME_8_X64")); + assertTrue(workflow.contains("-Dorg.gradle.java.installations.auto-detect=false")); + assertTrue(workflow.contains("-Dorg.gradle.java.installations.auto-download=false")); + } + + assertTrue(ci.contains("name: Cold Forge bootstrap")); + assertTrue(ci.contains("GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle")); + assertTrue(ci.contains("test ! -e .gradle")); + assertTrue(ci.contains("test ! -e \"$GRADLE_USER_HOME\"")); + assertTrue(ci.contains("classes verifyLegacyFixtures")); + assertTrue(ci.contains("--rerun-tasks --offline --no-daemon --no-build-cache")); + assertFalse(ci.contains("Mavenizer compatibility")); + assertFalse(ci.contains("25.0.3")); + } + + private static String readWorkflow(String name) throws Exception { + return new String(Files.readAllBytes(Paths.get(".github", "workflows", name)), + StandardCharsets.UTF_8); + } +} From 1394b4ec2454397d6cac037f8ef170f3eb944753 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:20:27 +0100 Subject: [PATCH 3/9] Fix dynamic registry biome rule matching --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 ++ README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 8 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 147 ++++++++++++++++-- .../worldgen/BiomeTypeCompatibility.java | 78 ++++++++++ .../mc/orespawn/worldgen/GeomeConfig.java | 50 ++++-- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/OreSpawnOreGeneration.java | 69 ++++---- .../worldgen/WorldGeologyProfileManager.java | 4 + .../worldgen/GeomeTransitionTest.java | 23 +++ .../worldgen/OreSpawnOreGenerationTest.java | 46 ++++++ 15 files changed, 385 insertions(+), 73 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a2c6225..346eb24c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.10.116051.jar - build/libs/OreSpawn-4.0.10.116051-sources.jar - build/libs/OreSpawn-4.0.10.116051-javadoc.jar + build/libs/OreSpawn-4.0.11.116051.jar + build/libs/OreSpawn-4.0.11.116051-sources.jar + build/libs/OreSpawn-4.0.11.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ee0be188..52543040 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.11.116051 + +* Preserve biome-dictionary geome weights when a data-driven biome is reached + through its stable registry key rather than the object baked at startup. +* Apply ore biome include and exclude filters by stable registry key so + dynamic-registry biome instances with the same ID are treated consistently. +* Existing chunks and profile formats are unchanged; the corrections apply to + generation in affected provider biomes. + Version 4.0.10.116051 * Evaluate Stable Layers rock min_y and max_y bounds against actual world Y diff --git a/README.md b/README.md index 9189cfe9..b29917f2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.10.116051`: the OreSpawn 4.0.10 +This branch builds target-qualified version `4.0.11.116051`: the OreSpawn 4.0.11 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index f747f5e6..eac1ca26 100644 --- a/build.gradle +++ b/build.gradle @@ -737,7 +737,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.10.116051' + if (project.mod_version != '4.0.11.116051' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' @@ -751,9 +751,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.10.116051.jar', - 'OreSpawn-4.0.10.116051-sources.jar', - 'OreSpawn-4.0.10.116051-javadoc.jar' + 'OreSpawn-4.0.11.116051.jar', + 'OreSpawn-4.0.11.116051-sources.jar', + 'OreSpawn-4.0.11.116051-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -770,7 +770,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.10.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.11.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index fbf190ab..96ca98e9 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.10.116051` | +| 1.16.5 | Forge | `116051` | `4.0.11.116051` | | 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` | @@ -149,8 +149,10 @@ Bug number, such as Forge 1.14.4's `4.0.8.114041`, even though the 4.0.7 repair was not applicable there. Forge 1.15.2, 1.16.5, and 1.17.1 then advanced to their target-qualified 4.0.9 releases for the provider terrain-host ordering repair. Forge 1.16.5 and later targets then advanced to 4.0.10 for the distinct -Stable Layers actual-height eligibility repair. A branch may therefore -legitimately skip functional version numbers. +Stable Layers actual-height eligibility repair, and to 4.0.11 to retain +biome-dictionary weights and ore biome filters for dynamic-registry biome +instances. A branch may therefore legitimately skip functional version +numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 55f8ea43..a83cd6f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.10.116051 +mod_version=4.0.11.116051 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 390c667d..b6279086 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -63,6 +63,7 @@ import net.minecraft.world.storage.FolderName; import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent; import net.minecraftforge.fml.event.server.FMLServerStartedEvent; +import net.minecraftforge.common.BiomeDictionary; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.world.BiomeLoadingEvent; import net.minecraftforge.eventbus.api.EventPriority; @@ -102,6 +103,8 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation BIOME_A = new ResourceLocation(MODID + ":surface_a"); private static final ResourceLocation BIOME_B = new ResourceLocation(MODID + ":surface_b"); private static final ResourceLocation PROBE_GEOME = new ResourceLocation(MODID + ":dynamic_biome_geome"); + private static final ResourceLocation PROBE_GEOME_ALTERNATIVE = + new ResourceLocation(MODID + ":dynamic_biome_geome_alternative"); private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water"); private static final Block[] NATURAL_SOURCES = { Blocks.DIRT, Blocks.GRASS_BLOCK, Blocks.COARSE_DIRT, Blocks.PODZOL, @@ -151,6 +154,8 @@ public SurfaceProbeTestMod() { private void setup(FMLCommonSetupEvent event) { event.enqueueWork(() -> { + BiomeDictionary.addTypes(RegistryKey.create(Registry.BIOME_REGISTRY, BIOME_A), + BiomeDictionary.Type.COLD); terrainConfigured = registerConfigured("terrain_setup", TERRAIN_FEATURE.get()); structureConfigured = registerConfigured("structure_sentinels", STRUCTURE_FEATURE.get()); vegetationConfigured = registerConfigured("vegetation_sentinels", VEGETATION_FEATURE.get()); @@ -211,19 +216,34 @@ private static void addDynamicBiomeGeology(WorldgenProvider.Builder provider) { provider.geome(PROBE_GEOME, geome -> geome .baseWeight(0.0D) .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); + provider.geome(PROBE_GEOME_ALTERNATIVE, geome -> geome + .baseWeight(0.0D) + .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); provider.rock(new ResourceLocation(MODID + ":rock/dynamic_biome"), blockId(Blocks.DIORITE), GeologyFamily.SEDIMENTARY, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); rock.geomeWeight(PROBE_GEOME, 1.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 0.0D); + for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); + }); + provider.rock(new ResourceLocation(MODID + ":rock/dynamic_biome_alternative"), blockId(Blocks.GRANITE), + GeologyFamily.SEDIMENTARY, rock -> { + rock.dimensions(java.util.Collections.singleton(OPEN_ID)); + rock.geomeWeight(PROBE_GEOME, 0.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 1.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); }); - provider.rock(new ResourceLocation(MODID + ":rock/fallback"), blockId(Blocks.GRANITE), + provider.rock(new ResourceLocation(MODID + ":rock/fallback"), blockId(Blocks.ANDESITE), GeologyFamily.SEDIMENTARY, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); rock.geomeWeight(PROBE_GEOME, 0.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 0.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 1.0D); }); - provider.biome(BIOME_A, java.util.Collections.singletonMap(PROBE_GEOME, 100.0D)); + Map biomeAWeights = new LinkedHashMap<>(); + biomeAWeights.put(PROBE_GEOME, 6.0D); + biomeAWeights.put(PROBE_GEOME_ALTERNATIVE, 14.0D); + provider.biome(BIOME_A, biomeAWeights); provider.biome(BIOME_B, java.util.Collections.singletonMap(PROBE_GEOME, 100.0D)); } @@ -238,6 +258,19 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent event) { } try { root.addProperty("place_fluid_deposits", true); + root.addProperty("place_ores", true); + JsonObject dictionary = root.getAsJsonObject("biome_dictionary"); + if (dictionary == null) { + dictionary = new JsonObject(); + root.add("biome_dictionary", dictionary); + } + JsonObject cold = dictionary.getAsJsonObject("COLD"); + if (cold == null) { + cold = new JsonObject(); + dictionary.add("COLD", cold); + } + cold.addProperty(PROBE_GEOME.toString(), 8.0D); + addDynamicBiomeOre(root); JsonObject terrain = root.getAsJsonObject("terrain_dimensions"); if (terrain == null) { terrain = new JsonObject(); @@ -267,6 +300,49 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent 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.DIORITE).toString()); + hosts.add(blockId(Blocks.GRANITE).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, @@ -379,6 +455,8 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { long underwaterPockets = 0L; long rawBedrock = 0L; long rawBlockEntities = 0L; + long dictionaryPrimary = 0L; + long dictionaryAlternative = 0L; BlockPos.Mutable pos = new BlockPos.Mutable(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -428,8 +506,15 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { } if (!roofed) { for (int depth = 6; depth <= 8; depth++) { - assertBlock(chunk, pos, x, groundY - depth, z, - Blocks.DIORITE.defaultBlockState(), "dynamic-biome geome rock"); + BlockState geologyState = chunk.getBlockState(pos.set(x, groundY - depth, z)); + if (geologyState.is(Blocks.GRANITE)) { + dictionaryAlternative++; + } else if (geologyState.is(Blocks.DIORITE)) { + dictionaryPrimary++; + } else { + throw new IllegalStateException("Unexpected dynamic-biome geome rock at " + + pos + " in " + biomeId + ": " + geologyState); + } geology++; } } @@ -461,6 +546,7 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { } } + long dynamicBiomeOre = roofed ? 0L : auditDynamicBiomeOre(level); if (top != EXPECTED_COLUMNS - 9 || underwater != 9 || filler != EXPECTED_FILLER || biomeA == 0 || biomeB == 0 || edgeChanges == 0 || sentinels != 9 * 4 || geology != (roofed ? 0 : EXPECTED_FILLER) @@ -470,7 +556,9 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES || cavePockets != EXPECTED_NATURAL_SOURCES / 2 || underwaterPockets != EXPECTED_NATURAL_SOURCES / 2 - || rawBedrock != 9 || rawBlockEntities != 9))) { + || rawBedrock != 9 || rawBlockEntities != 9 + || dictionaryPrimary != EXPECTED_FILLER || dictionaryAlternative != 0 + || dynamicBiomeOre == 0))) { throw new IllegalStateException("Incomplete surface audit for " + level.dimension().location() + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edgeChanges @@ -482,13 +570,38 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { + ", cavePockets=" + cavePockets + ", underwaterPockets=" + underwaterPockets + ", rawBedrock=" + rawBedrock - + ", rawBlockEntities=" + rawBlockEntities); + + ", rawBlockEntities=" + rawBlockEntities + + ", dictionaryPrimary=" + dictionaryPrimary + + ", dictionaryAlternative=" + dictionaryAlternative + + ", dynamicBiomeOre=" + dynamicBiomeOre); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); return new AuditResult(top, underwater, filler, geology, ceiling, roofTop, biomeA, biomeB, edgeChanges, sentinels, aquiferFluid, rawNaturalSources, structureNaturalSources, vegetationNaturalSources, - cavePockets, underwaterPockets, rawBedrock, rawBlockEntities); + cavePockets, underwaterPockets, rawBedrock, rawBlockEntities, + dictionaryPrimary, dictionaryAlternative, dynamicBiomeOre); + } + + private static long auditDynamicBiomeOre(ServerWorld level) { + BlockPos.Mutable pos = new BlockPos.Mutable(); + long count = 0L; + for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { + for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) { + Chunk chunk = level.getChunk(chunkX, chunkZ); + for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) { + for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) { + for (int y = 16; y <= 48; y++) { + if (chunk.getBlockState(pos.set(x, y, z)).is(Blocks.DIAMOND_BLOCK)) count++; + } + } + } + } + } + if (count == 0L) { + throw new IllegalStateException("Dynamic-registry biome filter produced no managed ore"); + } + return count; } private static NaturalSourceAudit auditNaturalSources(ServerWorld level, IChunk chunk, @@ -504,9 +617,8 @@ private static NaturalSourceAudit auditNaturalSources(ServerWorld level, IChunk int x = naturalX(minX, index); int z = naturalZ(minZ, index); int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); - if (chunk.getBlockState(pos.set(x, groundY - 12, z)).is(Blocks.DIORITE)) { - rawConverted++; - } + BlockState converted = chunk.getBlockState(pos.set(x, groundY - 12, z)); + if (converted.is(Blocks.DIORITE) || converted.is(Blocks.GRANITE)) rawConverted++; Block pocket = chunk.getBlockState(pos.set(x, groundY - 11, z)).getBlock(); if (index < NATURAL_SOURCES.length / 2) { if (pocket == Blocks.AIR) cavePreserved++; @@ -683,6 +795,9 @@ private static Properties properties(long seed, Map results values.setProperty(prefix + "underwater_pockets", Long.toString(result.underwaterPockets())); values.setProperty(prefix + "raw_bedrock", Long.toString(result.rawBedrock())); values.setProperty(prefix + "raw_block_entities", Long.toString(result.rawBlockEntities())); + values.setProperty(prefix + "dictionary_primary", Long.toString(result.dictionaryPrimary())); + values.setProperty(prefix + "dictionary_alternative", Long.toString(result.dictionaryAlternative())); + values.setProperty(prefix + "dynamic_biome_ore", Long.toString(result.dynamicBiomeOre())); } return values; } @@ -944,12 +1059,16 @@ private static final class AuditResult { private final long underwaterPockets; private final long rawBedrock; private final long rawBlockEntities; + private final long dictionaryPrimary; + private final long dictionaryAlternative; + private final long dynamicBiomeOre; 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 rawBedrock, long rawBlockEntities, long dictionaryPrimary, + long dictionaryAlternative, long dynamicBiomeOre) { this.top = top; this.underwater = underwater; this.filler = filler; @@ -968,6 +1087,9 @@ private static final class AuditResult { this.underwaterPockets = underwaterPockets; this.rawBedrock = rawBedrock; this.rawBlockEntities = rawBlockEntities; + this.dictionaryPrimary = dictionaryPrimary; + this.dictionaryAlternative = dictionaryAlternative; + this.dynamicBiomeOre = dynamicBiomeOre; } long top() { return top; } @@ -988,5 +1110,8 @@ private static final class AuditResult { long underwaterPockets() { return underwaterPockets; } long rawBedrock() { return rawBedrock; } long rawBlockEntities() { return rawBlockEntities; } + long dictionaryPrimary() { return dictionaryPrimary; } + long dictionaryAlternative() { return dictionaryAlternative; } + long dynamicBiomeOre() { return dynamicBiomeOre; } } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java new file mode 100644 index 00000000..5f8b8c40 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java @@ -0,0 +1,78 @@ +package zone.moddev.mc.orespawn.worldgen; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import net.minecraft.util.RegistryKey; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.biome.Biome; +import net.minecraftforge.common.BiomeDictionary; +import net.minecraftforge.registries.ForgeRegistries; + +/** + * Resolves Forge 1.16 biome-dictionary entries through stable keys while a + * server-owned dynamic biome registry is active. + */ +final class BiomeTypeCompatibility { + private static volatile Registry activeRegistry; + + private BiomeTypeCompatibility() { + } + + static void useRegistry(Registry registry) { + activeRegistry = registry; + } + + static void clearRegistry() { + activeRegistry = null; + } + + static Set> biomeKeys(String type) { + BiomeDictionary.Type dictionaryType; + try { + dictionaryType = BiomeDictionary.Type.getType(type); + } catch (RuntimeException ignored) { + return Collections.emptySet(); + } + + Registry registry = activeRegistry; + if (registry == null) { + return new LinkedHashSet<>(BiomeDictionary.getBiomes(dictionaryType)); + } + + Set> result = new LinkedHashSet<>(); + for (Map.Entry, Biome> entry : registry.entrySet()) { + if (BiomeDictionary.hasType(entry.getKey(), dictionaryType)) { + result.add(entry.getKey()); + } + } + return result; + } + + static Set biomeIds(String type) { + Set result = new LinkedHashSet<>(); + for (RegistryKey key : biomeKeys(type)) { + result.add(key.location()); + } + return result; + } + + static boolean hasType(RegistryKey key, String type) { + try { + return BiomeDictionary.hasType(key, BiomeDictionary.Type.getType(type)); + } catch (RuntimeException ignored) { + return false; + } + } + + static Biome biome(ResourceLocation id) { + Registry registry = activeRegistry; + if (registry != null) { + return registry.get(id); + } + return ForgeRegistries.BIOMES.getValue(id); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java index 075ffc09..f50ad73a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java @@ -49,7 +49,6 @@ import net.minecraft.tags.BlockTags; import net.minecraft.world.World; import net.minecraft.world.biome.Biome; -import net.minecraftforge.common.BiomeDictionary; import net.minecraftforge.registries.ForgeRegistries; import org.apache.logging.log4j.LogManager; @@ -291,7 +290,8 @@ private static BakedGeomeConfig bake(JsonObject root, ResourceLocation dimension return null; } Map biomeWeights = bakeBiomeWeights(geomeIndexes, biomeRules, dictionaryRules); - Map biomeWeightsById = bakeBiomeIdentifierWeights(geomeIndexes, biomeRules); + Map biomeWeightsById = bakeBiomeIdentifierWeights( + geomeIndexes, biomeRules, dictionaryRules); LOGGER.info("Baked OreSpawn geome config for '{}' with {} geomes, {} rock entries, " + "{} resolved biome profiles, {} identifier profiles, and {} formations", @@ -1117,8 +1117,10 @@ private static Map bakeBiomeWeights(Map geomeI if (biomeId != null) { merge(weights, biomeRules.get(biomeId.toString())); RegistryKey biomeKey = RegistryKey.create(Registry.BIOME_REGISTRY, biomeId); - for (BiomeDictionary.Type type : BiomeDictionary.getTypes(biomeKey)) { - merge(weights, dictionaryRules.get(type.getName())); + for (Entry entry : dictionaryRules.entrySet()) { + if (BiomeTypeCompatibility.hasType(biomeKey, entry.getKey())) { + merge(weights, entry.getValue()); + } } } applyBiomeHeuristic(weights, geomeIndexes, biomeId, biome); @@ -1129,22 +1131,52 @@ private static Map bakeBiomeWeights(Map geomeI static Map bakeBiomeIdentifierWeights(Map geomeIndexes, Map biomeRules) { + return bakeBiomeIdentifierWeights(geomeIndexes, biomeRules, Collections.emptyMap()); + } + + static Map bakeBiomeIdentifierWeights(Map geomeIndexes, + Map biomeRules, Map dictionaryRules) { + return bakeBiomeIdentifierWeights(geomeIndexes, biomeRules, dictionaryRules, + BiomeTypeCompatibility::biomeIds); + } + + static Map bakeBiomeIdentifierWeights(Map geomeIndexes, + Map biomeRules, Map dictionaryRules, + java.util.function.Function> dictionaryResolver) { Map result = new LinkedHashMap<>(); for (Entry entry : biomeRules.entrySet()) { try { ResourceLocation biomeId = new ResourceLocation(entry.getKey()); - double[] weights = new double[geomeIndexes.size()]; - Arrays.fill(weights, 1.0D); - merge(weights, entry.getValue()); - applyBiomeHeuristic(weights, geomeIndexes, biomeId, Float.NaN, Float.NaN); - result.put(biomeId, weights); + merge(identifierWeights(result, biomeId, geomeIndexes.size()), entry.getValue()); } catch (RuntimeException e) { LOGGER.warn("Ignoring invalid OreSpawn biome rule ID '{}'", entry.getKey()); } } + for (Entry entry : dictionaryRules.entrySet()) { + for (ResourceLocation biomeId : dictionaryResolver.apply(entry.getKey())) { + merge(identifierWeights(result, biomeId, geomeIndexes.size()), entry.getValue()); + } + } + for (Entry entry : result.entrySet()) { + Biome biome = BiomeTypeCompatibility.biome(entry.getKey()); + if (biome == null) { + applyBiomeHeuristic(entry.getValue(), geomeIndexes, entry.getKey(), Float.NaN, Float.NaN); + } else { + applyBiomeHeuristic(entry.getValue(), geomeIndexes, entry.getKey(), biome); + } + } return result; } + private static double[] identifierWeights(Map result, + ResourceLocation biomeId, int geomeCount) { + return result.computeIfAbsent(biomeId, ignored -> { + double[] weights = new double[geomeCount]; + Arrays.fill(weights, 1.0D); + return weights; + }); + } + private static void applyBiomeHeuristic(double[] weights, Map geomeIndexes, ResourceLocation biomeId, Biome biome) { applyBiomeHeuristic(weights, geomeIndexes, biomeId, diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 3f65db2b..2779b246 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.10.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.11.116051 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 e7ef43f4..d8ffe613 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.10.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.11.116051 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java index 127101fd..aaed1753 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -164,6 +164,8 @@ private static boolean generateChunk(ISeedReader world, IChunk chunk, Biome biom ChunkPos chunkPos = chunk.getPos(); int centerX = chunkPos.getMinBlockX() + 8; int centerZ = chunkPos.getMinBlockZ() + 8; + RegistryKey biomeKey = biomeId == null ? null + : RegistryKey.create(Registry.BIOME_REGISTRY, biomeId); int geome = -1; if (World.OVERWORLD.equals(dimension)) { geome = classifier(worldSeed).classifyColumn(biome, biomeId, centerX, centerZ, @@ -173,7 +175,7 @@ private static boolean generateChunk(ISeedReader world, IChunk chunk, Biome biom boolean changed = false; for (BakedOre ore : ores) { if (retrogenOnly && !ore.retrogen) continue; - if (!ore.acceptsBiome(biome, biomeId)) { + if (!ore.acceptsBiome(biomeKey)) { continue; } double frequency = ore.frequency; @@ -445,17 +447,14 @@ private static BakedOre bakeOre(BlockState output, BlockState deepOutput, int de } } } - Set includedBiomeIds = resolveBiomeIds(json, "biome_ids"); - Set excludedBiomeIds = resolveBiomeIds(json, "excluded_biome_ids"); - Set includedDictionaryBiomes = resolveBiomeDictionary(json, "biome_dictionary"); - Set excludedDictionaryBiomes = resolveBiomeDictionary(json, - "excluded_biome_dictionary"); + Set> includedBiomes = resolveBiomes(json, "biome_ids", "biome_dictionary"); + Set> excludedBiomes = resolveBiomes(json, + "excluded_biome_ids", "excluded_biome_dictionary"); return new BakedOre(output, deepOutput, deepOutputMaxY, outputs, minY, maxY, Math.min(64.0D, frequency), minQuantity, maxQuantity, pattern, heightDistribution, discardChanceOnAirExposure, spread, verticalSpread, nodeSize, - hostBlocks, familyMask, geomeWeights, includedBiomeIds, excludedBiomeIds, - includedDictionaryBiomes, excludedDictionaryBiomes, retrogen); + hostBlocks, familyMask, geomeWeights, includedBiomes, excludedBiomes, retrogen); } private static BakedOutput[] bakeOutputs(JsonObject ore, BlockState fallback) { @@ -513,27 +512,23 @@ private static void addTags(Map target, JsonElement element, } } - private static Set resolveBiomeIds(JsonObject rule, String idsKey) { - Set result = new HashSet<>(); + static Set> resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey) { + return resolveBiomes(rule, idsKey, dictionaryKey, BiomeTypeCompatibility::biomeKeys); + } + + static Set> resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey, + java.util.function.Function>> dictionaryResolver) { + Set> result = new HashSet<>(); if (rule.has(idsKey) && rule.get(idsKey).isJsonArray()) { for (JsonElement element : rule.getAsJsonArray(idsKey)) { ResourceLocation id = resource(element.getAsString()); - if (id != null) result.add(id); + if (id != null) result.add(RegistryKey.create(Registry.BIOME_REGISTRY, id)); } } - return result; - } - - private static Set resolveBiomeDictionary(JsonObject rule, String dictionaryKey) { - Set result = Collections.newSetFromMap(new IdentityHashMap()); if (rule.has(dictionaryKey) && rule.get(dictionaryKey).isJsonArray()) { for (JsonElement element : rule.getAsJsonArray(dictionaryKey)) { try { - for (RegistryKey key : net.minecraftforge.common.BiomeDictionary.getBiomes( - net.minecraftforge.common.BiomeDictionary.Type.getType(element.getAsString()))) { - Biome biome = ForgeRegistries.BIOMES.getValue(key.location()); - if (biome != null) result.add(biome); - } + result.addAll(dictionaryResolver.apply(element.getAsString())); } catch (RuntimeException ignored) { } } @@ -541,6 +536,13 @@ private static Set resolveBiomeDictionary(JsonObject rule, String diction return result; } + static boolean acceptsBiome(Set> includedBiomes, + Set> excludedBiomes, RegistryKey biome) { + if (biome == null) return includedBiomes.isEmpty() && excludedBiomes.isEmpty(); + return !excludedBiomes.contains(biome) + && (includedBiomes.isEmpty() || includedBiomes.contains(biome)); + } + private static Set resolveTag(ResourceLocation tag) { Set result = Collections.newSetFromMap(new IdentityHashMap()); result.addAll(BlockTags.getAllTags().getTagOrEmpty(tag).getValues()); @@ -641,10 +643,8 @@ private static final class BakedOre { final Map hostBlocks; final int familyMask; final double[] geomeWeights; - final Set includedBiomeIds; - final Set excludedBiomeIds; - final Set includedDictionaryBiomes; - final Set excludedDictionaryBiomes; + final Set> includedBiomes; + final Set> excludedBiomes; final boolean retrogen; BakedOre(BlockState output, BlockState deepOutput, int deepOutputMaxY, BakedOutput[] outputs, @@ -653,9 +653,8 @@ private static final class BakedOre { double discardChanceOnAirExposure, int spread, int verticalSpread, int nodeSize, Map hostBlocks, int familyMask, double[] geomeWeights, - Set includedBiomeIds, Set excludedBiomeIds, - Set includedDictionaryBiomes, Set excludedDictionaryBiomes, - boolean retrogen) { + Set> includedBiomes, + Set> excludedBiomes, boolean retrogen) { this.output = output; this.deepOutput = deepOutput; this.deepOutputMaxY = deepOutputMaxY; @@ -674,10 +673,8 @@ private static final class BakedOre { this.hostBlocks = hostBlocks; this.familyMask = familyMask; this.geomeWeights = geomeWeights; - this.includedBiomeIds = includedBiomeIds; - this.excludedBiomeIds = excludedBiomeIds; - this.includedDictionaryBiomes = includedDictionaryBiomes; - this.excludedDictionaryBiomes = excludedDictionaryBiomes; + this.includedBiomes = includedBiomes; + this.excludedBiomes = excludedBiomes; this.retrogen = retrogen; } @@ -705,12 +702,8 @@ boolean accepts(BlockState state, Random random, BakedGeomeConfig config) { && (familyMask & (1 << family.ordinal())) != 0; } - boolean acceptsBiome(Biome biome, ResourceLocation biomeId) { - if (excludedBiomeIds.contains(biomeId) || excludedDictionaryBiomes.contains(biome)) { - return false; - } - return (includedBiomeIds.isEmpty() && includedDictionaryBiomes.isEmpty()) - || includedBiomeIds.contains(biomeId) || includedDictionaryBiomes.contains(biome); + boolean acceptsBiome(RegistryKey biome) { + return OreSpawnOreGeneration.acceptsBiome(includedBiomes, excludedBiomes, biome); } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java index a638c135..054dd38f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java @@ -21,6 +21,7 @@ import zone.moddev.mc.orespawn.api.OreSpawnOreIntegration; import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; +import net.minecraft.util.registry.Registry; import net.minecraft.server.MinecraftServer; import net.minecraft.world.storage.FolderName; import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent; @@ -117,6 +118,8 @@ public static synchronized boolean reloadActiveProfile() { public static void onServerAboutToStart(FMLServerAboutToStartEvent event) { activeServer = event.getServer(); + BiomeTypeCompatibility.useRegistry(event.getServer().registryAccess() + .registryOrThrow(Registry.BIOME_REGISTRY)); Path worldRoot = event.getServer().getWorldPath(FolderName.ROOT).normalize(); Path profilePath = worldRoot.resolve("serverconfig").resolve(PROFILE_FILE_NAME); WorldGeologyProfile fallback = globalProfile(); @@ -172,6 +175,7 @@ public static void onServerAboutToStart(FMLServerAboutToStartEvent event) { public static void onServerStopped(FMLServerStoppedEvent event) { activeServer = null; activeProfile = null; + BiomeTypeCompatibility.clearRegistry(); GeomeConfig.applyWorldProfile(globalProfile()); BiomeWorldgenManager.clear(); StoneReplacer.refreshWorldConfig(); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java index 55b4cb89..f8a49a0d 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java @@ -6,16 +6,22 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.registry.Bootstrap; import net.minecraft.block.Blocks; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.RockEntry; class GeomeTransitionTest { + static { + Bootstrap.bootStrap(); + } + private static final ResourceLocation MOUNTAINS = new ResourceLocation("minecraft:mountains"); @Test @@ -30,6 +36,23 @@ void configuredBiomeWeightsWorkWithoutAForgeBiomeRegistryEntry() { assertEquals(1, config.pickGeome(null, MOUNTAINS, new double[2], 0.0D)); } + @Test + void identifierFallbackRetainsDictionaryWeightContributions() { + Map indexes = new LinkedHashMap<>(); + indexes.put("cakeworld:peppermint_fold", 0); + indexes.put("cakeworld:rock_candy_uplift", 1); + ResourceLocation marshmallowPeaks = new ResourceLocation("cakeworld", "marshmallow_peaks"); + Map exact = new LinkedHashMap<>(); + exact.put(marshmallowPeaks.toString(), new double[] { 6.0D, 14.0D }); + Map dictionary = new LinkedHashMap<>(); + dictionary.put("COLD", new double[] { 8.0D, 0.0D }); + Map weights = GeomeConfig.bakeBiomeIdentifierWeights(indexes, + exact, dictionary, type -> Collections.singleton(marshmallowPeaks)); + + assertEquals(15.0D, weights.get(marshmallowPeaks)[0]); + assertEquals(15.0D, weights.get(marshmallowPeaks)[1]); + } + @Test void savedWorldBoundaryUsesItsConfiguredBiomeInsteadOfEqualFallbackWeights() { BakedGeomeConfig config = observedWorldConfig(); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java index 1cfac449..f77c03e9 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java @@ -5,19 +5,65 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Random; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + import org.junit.jupiter.api.Test; import net.minecraft.util.RegistryKey; import net.minecraft.util.ResourceLocation; import net.minecraft.util.registry.Registry; +import net.minecraft.util.registry.Bootstrap; import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; class OreSpawnOreGenerationTest { + static { + Bootstrap.bootStrap(); + } + + @Test + void biomeFiltersRetainUnknownDynamicRegistryKeys() { + RegistryKey sodaOcean = RegistryKey.create(Registry.BIOME_REGISTRY, + new ResourceLocation("cakeworld", "soda_ocean")); + JsonObject rule = new JsonObject(); + JsonArray ids = new JsonArray(); + ids.add("cakeworld:soda_ocean"); + rule.add("biome_ids", ids); + + Set resolved = OreSpawnOreGeneration.resolveBiomes( + rule, "biome_ids", "biome_dictionary"); + + assertEquals(Collections.singleton(sodaOcean), resolved); + } + + @Test + void biomeFiltersMergeDictionaryKeys() { + RegistryKey sodaOcean = RegistryKey.create(Registry.BIOME_REGISTRY, + new ResourceLocation("cakeworld", "soda_ocean")); + JsonObject rule = new JsonObject(); + JsonArray dictionary = new JsonArray(); + dictionary.add("OCEAN"); + rule.add("biome_dictionary", dictionary); + + Set> resolved = OreSpawnOreGeneration.resolveBiomes( + rule, "biome_ids", "biome_dictionary", type -> Collections.singleton(sodaOcean)); + + assertEquals(Collections.singleton(sodaOcean), resolved); + assertTrue(OreSpawnOreGeneration.acceptsBiome(resolved, Collections.emptySet(), sodaOcean)); + assertFalse(OreSpawnOreGeneration.acceptsBiome(resolved, Collections.emptySet(), RegistryKey.create( + Registry.BIOME_REGISTRY, new ResourceLocation("cakeworld", "candy_plains")))); + assertFalse(OreSpawnOreGeneration.acceptsBiome(Collections.emptySet(), resolved, sodaOcean)); + assertTrue(OreSpawnOreGeneration.acceptsBiome(Collections.emptySet(), resolved, RegistryKey.create( + Registry.BIOME_REGISTRY, new ResourceLocation("cakeworld", "candy_plains")))); + } + @Test void fixedQuantityDoesNotConsumeRandomState() { CountingRandom random = new CountingRandom(0); From bd969474f2d5ea1561452be2524e466fd9520427 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:24:00 +0100 Subject: [PATCH 4/9] 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 | 7 ++++--- gradle.properties | 2 +- .../mc/orespawn/api/GeologySampler.java | 4 +++- .../orespawn/api/OreSpawnGeologySampler.java | 6 +++++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../api/OreSpawnGeologySamplerTest.java | 19 +++++++++++++++++++ 12 files changed, 56 insertions(+), 20 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 346eb24c..05875a80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.11.116051.jar - build/libs/OreSpawn-4.0.11.116051-sources.jar - build/libs/OreSpawn-4.0.11.116051-javadoc.jar + build/libs/OreSpawn-4.0.12.116051.jar + build/libs/OreSpawn-4.0.12.116051-sources.jar + build/libs/OreSpawn-4.0.12.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 52543040..fd027dec 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.12.116051 + +* 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.116051 * Preserve biome-dictionary geome weights when a data-driven biome is reached diff --git a/README.md b/README.md index b29917f2..af881ca0 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.116051`: the OreSpawn 4.0.11 +This branch builds target-qualified version `4.0.12.116051`: the OreSpawn 4.0.12 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index eac1ca26..3ee0b1c3 100644 --- a/build.gradle +++ b/build.gradle @@ -737,7 +737,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.11.116051' + if (project.mod_version != '4.0.12.116051' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' @@ -751,9 +751,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.11.116051.jar', - 'OreSpawn-4.0.11.116051-sources.jar', - 'OreSpawn-4.0.11.116051-javadoc.jar' + 'OreSpawn-4.0.12.116051.jar', + 'OreSpawn-4.0.12.116051-sources.jar', + 'OreSpawn-4.0.12.116051-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -770,7 +770,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.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.12.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index 8e2325e9..eeffe9d4 100644 --- a/docs/API.md +++ b/docs/API.md @@ -127,9 +127,12 @@ OreSpawnApi.createSampler(server.overworld()).ifPresent(sampler -> { ``` `sampleColumn` performs one biome/geome classification and reuses it for every -Y query. Sampling is read-only and is intended for gameplay decisions, -diagnostics, and compatible generation outside OreSpawn's block loops. -Callbacks inside OreSpawn generation loops are intentionally unsupported. +Y query. Pass the first-free surface height returned by `Level.getHeight`; +OreSpawn uses the highest occupied block immediately below it for biome/geome +classification, exactly as chunk geology generation does. Sampling is read-only +and is intended for gameplay decisions, diagnostics, and compatible generation +outside OreSpawn's block loops. Callbacks inside OreSpawn generation loops are +intentionally unsupported. Custom pattern mods create a Forge `DeferredRegister` using `OreSpawnPatternRegistry.REGISTRY_NAME`. An `OrePatternType` contains a codec diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 96ca98e9..924fe0c8 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.11.116051` | +| 1.16.5 | Forge | `116051` | `4.0.12.116051` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -151,8 +151,9 @@ their target-qualified 4.0.9 releases for the provider terrain-host ordering repair. Forge 1.16.5 and later targets then advanced to 4.0.10 for the distinct Stable Layers actual-height eligibility repair, and to 4.0.11 to retain biome-dictionary weights and ore biome filters for dynamic-registry biome -instances. A branch may therefore legitimately skip functional version -numbers. +instances, and to 4.0.12 so public geology samples classify the same +highest occupied block as chunk generation at vertical biome seams. A branch +may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index a83cd6f9..ffa389ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.11.116051 +mod_version=4.0.12.116051 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 0e1bd1c0..b3cc5743 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java @@ -55,7 +55,7 @@ static GeologySampler create(ServerWorld 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, 0), blockZ); Biome biome = level.getBiome(position); ResourceLocation biomeId = level.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY) .getKey(biome); @@ -67,6 +67,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 2779b246..1212fdf3 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.12.116051 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 d8ffe613..69c12b8f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.12.116051 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..cb8f76c3 --- /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, 0)); + assertEquals(0, OreSpawnGeologySampler.generationBiomeY(1, 0)); + } + + @Test + void clampsAnEmptyColumnToTheLevelFloor() { + assertEquals(0, OreSpawnGeologySampler.generationBiomeY(0, 0)); + assertEquals(0, OreSpawnGeologySampler.generationBiomeY(Integer.MIN_VALUE, 0)); + } +} From d378db2ec3cbf68fd460fe9093a4997c1f9673be Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:27:21 +0100 Subject: [PATCH 5/9] 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 | 8 ++- docs/VERSIONS.md | 6 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 65 ++++++------------- .../mc/orespawn/api/WorldgenProvider.java | 27 ++++++++ .../orespawn/client/GeologyEditorSession.java | 11 +++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../mc/orespawn/api/WorldgenProviderTest.java | 62 ++++++++++++++++++ .../client/GeologyEditorSessionTest.java | 23 +++++++ 16 files changed, 184 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05875a80..f3c9ae52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.12.116051.jar - build/libs/OreSpawn-4.0.12.116051-sources.jar - build/libs/OreSpawn-4.0.12.116051-javadoc.jar + build/libs/OreSpawn-4.0.13.116051.jar + build/libs/OreSpawn-4.0.13.116051-sources.jar + build/libs/OreSpawn-4.0.13.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fd027dec..facc831e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.13.116051 + +* 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.116051 * Classify public geology samples at the same highest occupied block used by diff --git a/README.md b/README.md index af881ca0..10d4b809 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.116051`: the OreSpawn 4.0.12 +This branch builds target-qualified version `4.0.13.116051`: the OreSpawn 4.0.13 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 3ee0b1c3..1971b6d2 100644 --- a/build.gradle +++ b/build.gradle @@ -737,7 +737,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.12.116051' + if (project.mod_version != '4.0.13.116051' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' @@ -751,9 +751,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.12.116051.jar', - 'OreSpawn-4.0.12.116051-sources.jar', - 'OreSpawn-4.0.12.116051-javadoc.jar' + 'OreSpawn-4.0.13.116051.jar', + 'OreSpawn-4.0.13.116051-sources.jar', + 'OreSpawn-4.0.13.116051-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -770,7 +770,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.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.13.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index eeffe9d4..999f61eb 100644 --- a/docs/API.md +++ b/docs/API.md @@ -77,6 +77,13 @@ WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) `OilDefinition` and template `.oil(...)` remain deprecated migration adapters for one legacy oil rule. New integrations should use `FluidDepositDefinition`. +Ore dimension builders expose the same biome filters as provider JSON and +fluid-deposit builders. Use `.biome(...)` and `.biomeDictionary(...)` for +inclusions, with `.excludeBiome(...)` and `.excludeBiomeDictionary(...)` for +exclusions. These methods work on both explicit `.dimension(...)` rules and +`.dimensionSelector(...)` fallbacks; built definitions and their returned +filter sets are immutable. + Register custom biomes with Forge as usual. `OreSpawnBiomes.copyAndRegister` provides a small optional convenience for cloning a known biome without adding TerraBlender: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index b2a8a6f1..e4c90d23 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 abb1a190..1a51ca3b 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -85,7 +85,7 @@ import zone.moddev.mc.orespawn.api.OreDimensionSelector; import zone.moddev.mc.orespawn.api.OrePattern; import zone.moddev.mc.orespawn.api.OreSpawnApi; import zone.moddev.mc.orespawn.api.WorldgenProvider; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.event.lifecycle.InterModEnqueueEvent; private void enqueueWorldgen(InterModEnqueueEvent event) { @@ -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("forge", "stone")))) .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 924fe0c8..553d5744 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.12.116051` | +| 1.16.5 | Forge | `116051` | `4.0.13.116051` | | 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` | @@ -152,7 +152,9 @@ repair. Forge 1.16.5 and later targets then advanced to 4.0.10 for the distinct Stable Layers actual-height eligibility repair, and to 4.0.11 to retain biome-dictionary weights and ore biome filters for dynamic-registry biome instances, and to 4.0.12 so public geology samples classify the same -highest occupied block as chunk generation at vertical biome seams. A branch +highest occupied block as chunk generation at vertical biome seams. It then +advanced to 4.0.13 to restore API biome-filter parity and accept +provider-namespaced geomes in the creation editor. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index ffa389ae..535322c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.12.116051 +mod_version=4.0.13.116051 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 b6279086..59f39717 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -26,6 +26,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; @@ -106,6 +108,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.GRAVEL, Blocks.SAND, Blocks.RED_SAND, Blocks.CLAY, @@ -193,6 +197,23 @@ private static void addUnique(java.util.List>> 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.DIORITE)) + .hostBlock(blockId(Blocks.GRANITE)) + .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) @@ -270,7 +291,6 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent 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(); @@ -300,49 +320,6 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent 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.DIORITE).toString()); - hosts.add(blockId(Blocks.GRANITE).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 25e9a0ad..388eb395 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java @@ -528,6 +528,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; @@ -551,6 +555,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); } @@ -577,6 +586,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; } @@ -612,6 +625,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; } @@ -635,6 +652,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<>(); @@ -665,6 +686,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 dd217d55..bed860b6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -625,7 +625,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(); @@ -685,7 +685,7 @@ List validate() { } for (Entry entry : terrainActive ? geomes.entrySet() : Collections.>emptySet()) { - if (!entry.getKey().matches("[a-z0-9_.-]+") || !entry.getValue().isJsonObject()) { + if (!validGeomeId(entry.getKey()) || !entry.getValue().isJsonObject()) { errors.add("Invalid geome: " + entry.getKey()); continue; } @@ -1170,6 +1170,13 @@ private static boolean validResource(String id) { } } + private static boolean validGeomeId(String id) { + if (id == null || id.isEmpty()) return false; + if (id.indexOf(':') < 0) return id.matches("[a-z0-9_.-]+"); + if (!validResource(id)) return false; + return id.equals(new ResourceLocation(id).toString()); + } + static String string(JsonObject json, String key, String fallback) { try { return json.has(key) ? json.get(key).getAsString() : fallback; } catch (RuntimeException e) { return fallback; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 1212fdf3..937ea93f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.13.116051 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 69c12b8f..901fa7de 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.13.116051 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 d32b0ca3..7993377d 100644 --- a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java @@ -268,6 +268,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 9e3ce48e..168943ef 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java @@ -144,4 +144,27 @@ void standaloneFluidPickerCreatesAUsableCoveredOverworldRule() { java.util.List errors = session.validate(); assertTrue(errors.isEmpty(), errors.toString()); } + + @Test + void namespacedGeomesCanBeAddedValidatedAndRoundTripped() { + String geomeId = "cakeworld:cocoa_basin"; + GeologyEditorSession session = new GeologyEditorSession(WorldGeologyProfile.recommended(false)); + session.configureDefaultVanillaStrata(); + session.addGeome(geomeId); + + assertTrue(session.section("geomes").has(geomeId)); + session.weightMap("biomes", "minecraft:plains").addProperty(geomeId, 2.0D); + session.rock("minecraft:stone").getAsJsonObject("geomes").addProperty(geomeId, 3.0D); + java.util.List errors = session.validate(); + assertTrue(errors.isEmpty(), errors.toString()); + + WorldGeologyProfile saved = session.profile(); + GeologyEditorSession reopened = new GeologyEditorSession(saved); + assertEquals(saved.rootCopy(), reopened.profile().rootCopy()); + assertTrue(reopened.validate().isEmpty(), reopened.validate().toString()); + assertEquals(2.0D, reopened.weightMap("biomes", "minecraft:plains") + .get(geomeId).getAsDouble()); + assertEquals(3.0D, reopened.rock("minecraft:stone").getAsJsonObject("geomes") + .get(geomeId).getAsDouble()); + } } From b763b2e79df0deef5581b05d1b6762ec565cd817 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:31:50 +0100 Subject: [PATCH 6/9] Fix exposed snow material conversion --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 8 + README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 6 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 155 +++++++++++++++++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/WorldMaterialWeather.java | 8 + 10 files changed, 184 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3c9ae52..690f340a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.13.116051.jar - build/libs/OreSpawn-4.0.13.116051-sources.jar - build/libs/OreSpawn-4.0.13.116051-javadoc.jar + build/libs/OreSpawn-4.0.14.116051.jar + build/libs/OreSpawn-4.0.14.116051-sources.jar + build/libs/OreSpawn-4.0.14.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index facc831e..81aa7db8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.14.116051 + +* Convert exposed one-layer vanilla Snow from the first free cell immediately + above Minecraft's motion-blocking surface when a dimension supplies a custom + snow material. +* Retain the existing surface Ice conversion and leave buried or authored Snow + and Ice unchanged. + Version 4.0.13.116051 * Give the public ore-dimension builder the exact biome include/exclude and diff --git a/README.md b/README.md index 10d4b809..372befbd 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.116051`: the OreSpawn 4.0.13 +This branch builds target-qualified version `4.0.14.116051`: the OreSpawn 4.0.14 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 1971b6d2..339a7cae 100644 --- a/build.gradle +++ b/build.gradle @@ -737,7 +737,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.13.116051' + if (project.mod_version != '4.0.14.116051' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' @@ -751,9 +751,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.13.116051.jar', - 'OreSpawn-4.0.13.116051-sources.jar', - 'OreSpawn-4.0.13.116051-javadoc.jar' + 'OreSpawn-4.0.14.116051.jar', + 'OreSpawn-4.0.14.116051-sources.jar', + 'OreSpawn-4.0.14.116051-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -770,7 +770,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.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.14.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 553d5744..4d6164a7 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.13.116051` | +| 1.16.5 | Forge | `116051` | `4.0.14.116051` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -154,7 +154,9 @@ biome-dictionary weights and ore biome filters for dynamic-registry biome instances, and to 4.0.12 so public geology samples classify the same highest occupied block as chunk generation at vertical biome seams. It then advanced to 4.0.13 to restore API biome-filter parity and accept -provider-namespaced geomes in the creation editor. A branch +provider-namespaced geomes in the creation editor, then to 4.0.14 so one-layer +Snow above the motion-blocking surface is included in configured +weather-material conversion. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 535322c1..3006e417 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.13.116051 +mod_version=4.0.14.116051 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 59f39717..7254d620 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -144,6 +144,8 @@ public final class SurfaceProbeTestMod { private static final String MARKER_NAME = "surfaceprobe-integration.properties"; private static final String CHEST_ITEM_NAME = "surfaceprobe sentinel"; private static final String RAW_CHEST_ITEM_NAME = "surfaceprobe raw block entity sentinel"; + private static final BlockState WEATHER_SNOW_REPLACEMENT = Blocks.WHITE_WOOL.defaultBlockState(); + private static final BlockState WEATHER_ICE_REPLACEMENT = Blocks.BLUE_ICE.defaultBlockState(); public SurfaceProbeTestMod() { FMLJavaModLoadingContext context = FMLJavaModLoadingContext.get(); @@ -226,6 +228,9 @@ private void enqueueProvider(InterModEnqueueEvent event) { .hostBlock(blockId(Blocks.DIORITE)))); addPalette(provider, "open_palette_0", OPEN_ID, false); addPalette(provider, "roofed_palette_0", 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())) { @@ -434,6 +439,12 @@ private static AuditResult auditDimension(ServerWorld 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.Mutable pos = new BlockPos.Mutable(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -520,6 +531,14 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { rawBedrock += natural.bedrockPreserved; rawBlockEntities += natural.blockEntityPreserved; } + WeatherMaterialAudit weather = auditWeatherMaterials(chunk, pos, + chunkMinX, chunkMinZ, 0, 256, roofed); + exposedSnowConverted += weather.exposedSnowConverted(); + surfaceIceConverted += weather.surfaceIceConverted(); + buriedSnowPreserved += weather.buriedSnowPreserved(); + buriedIcePreserved += weather.buriedIcePreserved(); + unconfiguredSnowPreserved += weather.unconfiguredSnowPreserved(); + unconfiguredIcePreserved += weather.unconfiguredIcePreserved(); } } @@ -527,7 +546,10 @@ private static AuditResult auditDimension(ServerWorld 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 @@ -535,6 +557,9 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { || underwaterPockets != EXPECTED_NATURAL_SOURCES / 2 || 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 @@ -550,6 +575,12 @@ private static AuditResult auditDimension(ServerWorld 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); @@ -557,7 +588,46 @@ private static AuditResult auditDimension(ServerWorld 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(IChunk chunk, + BlockPos.Mutable 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(IChunk 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(ServerWorld level) { @@ -775,6 +845,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; } @@ -940,9 +1016,34 @@ private static boolean placeVegetationSentinels(ISeedReader world, IChunk chunk) 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(ISeedReader world, IChunk chunk, + BlockPos.Mutable 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(ISeedReader world, IChunk chunk, BlockPos.Mutable pos, int minX, int minZ, int depth) { if (!world.getLevel().dimension().equals(OPEN)) return; @@ -1017,6 +1118,33 @@ private static final class NaturalSourceAudit { } } + private static final class WeatherMaterialAudit { + private final long exposedSnowConverted; + private final long surfaceIceConverted; + private final long buriedSnowPreserved; + private final long buriedIcePreserved; + private final long unconfiguredSnowPreserved; + private final long unconfiguredIcePreserved; + + WeatherMaterialAudit(long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { + this.exposedSnowConverted = exposedSnowConverted; + this.surfaceIceConverted = surfaceIceConverted; + this.buriedSnowPreserved = buriedSnowPreserved; + this.buriedIcePreserved = buriedIcePreserved; + this.unconfiguredSnowPreserved = unconfiguredSnowPreserved; + this.unconfiguredIcePreserved = unconfiguredIcePreserved; + } + + long exposedSnowConverted() { return exposedSnowConverted; } + long surfaceIceConverted() { return surfaceIceConverted; } + long buriedSnowPreserved() { return buriedSnowPreserved; } + long buriedIcePreserved() { return buriedIcePreserved; } + long unconfiguredSnowPreserved() { return unconfiguredSnowPreserved; } + long unconfiguredIcePreserved() { return unconfiguredIcePreserved; } + } + private static final class AuditResult { private final long top; private final long underwater; @@ -1039,13 +1167,22 @@ private static final class AuditResult { private final long dictionaryPrimary; private final long dictionaryAlternative; private final long dynamicBiomeOre; + private final long exposedSnowConverted; + private final long surfaceIceConverted; + private final long buriedSnowPreserved; + private final long buriedIcePreserved; + private final long unconfiguredSnowPreserved; + private final long unconfiguredIcePreserved; 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 dictionaryAlternative, long dynamicBiomeOre, + long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { this.top = top; this.underwater = underwater; this.filler = filler; @@ -1067,6 +1204,12 @@ private static final class AuditResult { this.dictionaryPrimary = dictionaryPrimary; this.dictionaryAlternative = dictionaryAlternative; this.dynamicBiomeOre = dynamicBiomeOre; + this.exposedSnowConverted = exposedSnowConverted; + this.surfaceIceConverted = surfaceIceConverted; + this.buriedSnowPreserved = buriedSnowPreserved; + this.buriedIcePreserved = buriedIcePreserved; + this.unconfiguredSnowPreserved = unconfiguredSnowPreserved; + this.unconfiguredIcePreserved = unconfiguredIcePreserved; } long top() { return top; } @@ -1090,5 +1233,11 @@ private static final class AuditResult { long dictionaryPrimary() { return dictionaryPrimary; } long dictionaryAlternative() { return dictionaryAlternative; } long dynamicBiomeOre() { return dynamicBiomeOre; } + long exposedSnowConverted() { return exposedSnowConverted; } + long surfaceIceConverted() { return surfaceIceConverted; } + long buriedSnowPreserved() { return buriedSnowPreserved; } + long buriedIcePreserved() { return buriedIcePreserved; } + long unconfiguredSnowPreserved() { return unconfiguredSnowPreserved; } + long unconfiguredIcePreserved() { return 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 937ea93f..325663d6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.14.116051 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 901fa7de..836e5aef 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.14.116051 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 ba173ae5..bcf80999 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(IChunk chunk, DimensionMaterials materials) { for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int top = chunk.getHeight(Heightmap.Type.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 < 256) { + 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 6ac683ac965d0722cc81bcc771eb3231c7997c1b Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:41:24 +0100 Subject: [PATCH 7/9] Fix geology sampler biome attribution --- .github/workflows/ci.yml | 6 +-- CHANGELOG.txt | 8 ++++ README.md | 2 +- build.gradle | 10 ++-- docs/API.md | 8 ++-- docs/VERSIONS.md | 6 ++- gradle.properties | 2 +- .../mc/orespawn/api/GeologySampler.java | 3 +- .../orespawn/api/OreSpawnGeologySampler.java | 6 +-- .../orespawn/worldgen/BakedGeomeConfig.java | 18 ++++--- .../mc/orespawn/worldgen/GeomeGeology.java | 3 +- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../orespawn/worldgen/TerrainBiomeLookup.java | 25 ++++++++++ .../worldgen/GeomeTransitionTest.java | 47 ++++++++++++++++++- .../worldgen/TerrainBiomeLookupTest.java | 34 ++++++++++++++ 16 files changed, 148 insertions(+), 34 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 690f340a..6bc3c56a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.14.116051.jar - build/libs/OreSpawn-4.0.14.116051-sources.jar - build/libs/OreSpawn-4.0.14.116051-javadoc.jar + build/libs/OreSpawn-4.0.15.116051.jar + build/libs/OreSpawn-4.0.15.116051-sources.jar + build/libs/OreSpawn-4.0.15.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 81aa7db8..9c847cf3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.15.116051 + +* 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.116051 * Convert exposed one-layer vanilla Snow from the first free cell immediately diff --git a/README.md b/README.md index 372befbd..9b2f2913 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.116051`: the OreSpawn 4.0.14 +This branch builds target-qualified version `4.0.15.116051`: the OreSpawn 4.0.15 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 339a7cae..1dcb57a7 100644 --- a/build.gradle +++ b/build.gradle @@ -737,7 +737,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.14.116051' + if (project.mod_version != '4.0.15.116051' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' @@ -751,9 +751,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.14.116051.jar', - 'OreSpawn-4.0.14.116051-sources.jar', - 'OreSpawn-4.0.14.116051-javadoc.jar' + 'OreSpawn-4.0.15.116051.jar', + 'OreSpawn-4.0.15.116051-sources.jar', + 'OreSpawn-4.0.15.116051-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -770,7 +770,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.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.15.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index 999f61eb..5fcad3f6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -136,9 +136,11 @@ OreSpawnApi.createSampler(server.overworld()).ifPresent(sampler -> { `sampleColumn` performs one biome/geome classification and reuses it for every Y query. Pass the first-free surface height returned by `Level.getHeight`; OreSpawn uses the highest occupied block immediately below it for biome/geome -classification, exactly as chunk geology generation does. Sampling is read-only -and is intended for gameplay decisions, diagnostics, and compatible generation -outside OreSpawn's block loops. Callbacks inside OreSpawn generation loops are +classification and resolves the same stable quart-biome cell used by chunk +geology. This avoids display-oriented fuzzy biome zoom changing the prediction +after later surface work alters a heightmap. Sampling is read-only and is +intended for gameplay decisions, diagnostics, and compatible generation outside +OreSpawn's block loops. Callbacks inside OreSpawn generation loops are intentionally unsupported. Custom pattern mods create a Forge `DeferredRegister` using diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 4d6164a7..c51b2acd 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.14.116051` | +| 1.16.5 | Forge | `116051` | `4.0.15.116051` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -156,7 +156,9 @@ highest occupied block as chunk generation at vertical biome seams. It then advanced to 4.0.13 to restore API biome-filter parity and accept provider-namespaced geomes in the creation editor, then to 4.0.14 so one-layer Snow above the motion-blocking surface is included in configured -weather-material conversion. A branch +weather-material conversion, and then to 4.0.15 so generated geology and +public samples use the same stable quart-biome cell at three-dimensional biome +boundaries. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 3006e417..0ba5d5f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.14.116051 +mod_version=4.0.15.116051 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java index b242fa2f..8d5ad3db 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java @@ -6,7 +6,8 @@ public interface GeologySampler { * Classifies one column. The returned column reuses that biome/geome * classification for all subsequent Y queries. {@code surfaceY} is the first * free block returned by {@code Level.getHeight}; OreSpawn classifies the - * biome at the highest occupied block, matching chunk geology generation. + * stable quart biome at the highest occupied block, matching chunk geology + * generation without Minecraft's display-oriented fuzzy biome zoom. */ GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY); } diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java index b3cc5743..505fdbac 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.util.math.BlockPos; import net.minecraft.util.registry.Registry; import net.minecraft.util.ResourceLocation; import net.minecraft.world.biome.Biome; @@ -55,8 +55,8 @@ static GeologySampler create(ServerWorld level) { @Override public GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY) { - BlockPos position = new BlockPos(blockX, generationBiomeY(surfaceY, 0), blockZ); - Biome biome = level.getBiome(position); + int biomeY = generationBiomeY(surfaceY, 0); + Biome biome = TerrainBiomeLookup.atBlock(level.getBiomeManager(), blockX, biomeY, blockZ); ResourceLocation biomeId = level.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY) .getKey(biome); if (biomeId == null) biomeId = new ResourceLocation("orespawn", "unregistered_biome"); 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 d89760a5..4199f0eb 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 42b336ce..af711723 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -97,8 +97,7 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (int dz = 0; dz < 16; dz++) { int z = zOffset + dz; int surfaceY = chunk.getHeight(Heightmap.Type.WORLD_SURFACE_WG, dx, dz); - cursor.set(x, surfaceY, z); - Biome biome = world.getBiome(cursor); + Biome biome = TerrainBiomeLookup.atBlock(chunk.getBiomes(), x, surfaceY, z); ResourceLocation biomeId = world.registryAccess() .registryOrThrow(Registry.BIOME_REGISTRY).getKey(biome); if (!terrain.acceptsBiome(biomeId)) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 325663d6..7682325f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.15.116051 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 836e5aef..bb457987 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.15.116051 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..1a2bf57b --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookup.java @@ -0,0 +1,25 @@ +package zone.moddev.mc.orespawn.worldgen; + +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeManager; + +/** + * Internal generation-time biome lookup shared by geology and its public + * read-only sampler. + */ +public final class TerrainBiomeLookup { + private TerrainBiomeLookup() { + } + + public static Biome atBlock(BiomeManager.IBiomeReader source, + int blockX, int blockY, int blockZ) { + return source.getNoiseBiome(Math.floorDiv(blockX, 4), + Math.floorDiv(blockY, 4), Math.floorDiv(blockZ, 4)); + } + + public static Biome atBlock(BiomeManager manager, + int blockX, int blockY, int blockZ) { + return manager.getNoiseBiomeAtQuart(Math.floorDiv(blockX, 4), + Math.floorDiv(blockY, 4), Math.floorDiv(blockZ, 4)); + } +} 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 f8a49a0d..21bd3f80 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java @@ -13,6 +13,10 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.util.registry.Bootstrap; import net.minecraft.block.Blocks; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeAmbience; +import net.minecraft.world.biome.BiomeGenerationSettings; +import net.minecraft.world.biome.MobSpawnInfo; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.RockEntry; @@ -36,6 +40,22 @@ void configuredBiomeWeightsWorkWithoutAForgeBiomeRegistryEntry() { assertEquals(1, config.pickGeome(null, MOUNTAINS, 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 }; + Map identity = new java.util.IdentityHashMap<>(); + identity.put(aliasedBiome, identityWeights); + Map identifiers = new LinkedHashMap<>(); + identifiers.put(dynamicId, identifierWeights); + BakedGeomeConfig config = config(identity, identifiers); + + 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<>(); @@ -100,6 +120,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()), @@ -112,7 +137,27 @@ 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() { + BiomeAmbience effects = new BiomeAmbience.Builder() + .fogColor(0xC0D8FF) + .waterColor(0x3F76E4) + .waterFogColor(0x050533) + .skyColor(0x78A7FF) + .build(); + return new Biome.Builder() + .precipitation(Biome.RainType.NONE) + .biomeCategory(Biome.Category.NONE) + .depth(0.0F) + .scale(0.0F) + .temperature(0.5F) + .downfall(0.5F) + .specialEffects(effects) + .mobSpawnSettings(MobSpawnInfo.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..5a78bb09 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/TerrainBiomeLookupTest.java @@ -0,0 +1,34 @@ +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"); + + assertNull(TerrainBiomeLookup.atBlock((x, y, z) -> { + coordinates.set(x + "," + y + "," + z); + return null; + }, -1, -1, -1)); + assertEquals("-1,-1,-1", coordinates.get(), + "negative block coordinates must use floor-by-four quart coordinates"); + } +} From a6bc7ee3f621b71b46025ee91b976d4d51e9b07a Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 10:42:42 +0100 Subject: [PATCH 8/9] Record shared 4.0.16 release identity --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.txt | 8 ++++++++ README.md | 2 +- build.gradle | 10 +++++----- docs/VERSIONS.md | 6 ++++-- gradle.properties | 2 +- .../zone/moddev/mc/orespawn/api/WorldgenProvider.java | 2 +- .../mc/orespawn/worldgen/LegacyConfigMigrator.java | 2 +- .../worldgen/LegacyMineralogyProfileMigration.java | 2 +- 9 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bc3c56a..52da150f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.15.116051.jar - build/libs/OreSpawn-4.0.15.116051-sources.jar - build/libs/OreSpawn-4.0.15.116051-javadoc.jar + build/libs/OreSpawn-4.0.16.116051.jar + build/libs/OreSpawn-4.0.16.116051-sources.jar + build/libs/OreSpawn-4.0.16.116051-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9c847cf3..a4616d66 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.16.116051 + +* Adopt the shared 4.0.16 release identity. Forge 1.16 has no server-side + GameTest harness, so the later GameTest lifecycle repair is not applicable + on this target. +* Ordinary dedicated benchmark servers continue to stop automatically when + requested. + Version 4.0.15.116051 * Classify generated geology and public geology samples through the same diff --git a/README.md b/README.md index 9b2f2913..aa880abb 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.116051`: the OreSpawn 4.0.15 +This branch builds target-qualified version `4.0.16.116051`: the OreSpawn 4.0.16 feature set for Minecraft 1.16.5 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 1dcb57a7..4adc0b50 100644 --- a/build.gradle +++ b/build.gradle @@ -737,7 +737,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.15.116051' + if (project.mod_version != '4.0.16.116051' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.16.5' || project.forge_version != '36.2.34' @@ -751,9 +751,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.15.116051.jar', - 'OreSpawn-4.0.15.116051-sources.jar', - 'OreSpawn-4.0.15.116051-javadoc.jar' + 'OreSpawn-4.0.16.116051.jar', + 'OreSpawn-4.0.16.116051-sources.jar', + 'OreSpawn-4.0.16.116051-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -770,7 +770,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.116051')) { + if (!file(path).getText('UTF-8').contains('4.0.16.116051')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index c51b2acd..04be2f2d 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -54,7 +54,7 @@ Examples: | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.14.4 | Forge | `114041` | `4.0.8.114041` | | 1.15.2 | Forge | `115021` | `4.0.9.115021` | -| 1.16.5 | Forge | `116051` | `4.0.15.116051` | +| 1.16.5 | Forge | `116051` | `4.0.16.116051` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -158,7 +158,9 @@ provider-namespaced geomes in the creation editor, then to 4.0.14 so one-layer Snow above the motion-blocking surface is included in configured weather-material conversion, and then to 4.0.15 so generated geology and public samples use the same stable quart-biome cell at three-dimensional biome -boundaries. A branch +boundaries. The shared 4.0.16 identity fixes GameTest benchmark shutdown where +that harness exists; Forge 1.16 has no server-side GameTest harness, so it keeps +its ordinary dedicated benchmark auto-stop without a compatibility shim. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 0ba5d5f3..d705bcb7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20210115.111550 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.15.116051 +mod_version=4.0.16.116051 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/WorldgenProvider.java b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java index 388eb395..749ee980 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java @@ -1299,7 +1299,7 @@ public Builder fluidDeposit(FluidDepositDefinition value) { profile.addProperty("place_fluid_deposits", true); return this; } - /** @deprecated Use {@link #fluidDeposit(FluidDepositDefinition)}. */ + /** @deprecated Use {@link WorldgenProvider.GeologyTemplate.Builder#fluidDeposit(WorldgenProvider.FluidDepositDefinition)}. */ @Deprecated public Builder oil(OilDefinition value) { profile.add("oil", value.toJson()); 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 7682325f..8b853be9 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.16.116051 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 bb457987..e9afa7d1 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,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.116051 Upgrade Report"); + lines.add("OreSpawn 4.0.16.116051 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); From e5e75b032158bafe1695ff2b4c618a650f0fb9a4 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 12:06:35 +0100 Subject: [PATCH 9/9] Install Java 25 for ForgeGradle Mavenizer --- .github/workflows/ci.yml | 18 +++++++-- .github/workflows/codeql-analysis.yml | 8 +++- README.md | 8 ++-- .../orespawn/ReleaseWorkflowContractTest.java | 40 +++++++++++++++++-- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52da150f..6a2fe95e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,12 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: @@ -51,7 +57,7 @@ jobs: gradle_args=( classes verifyLegacyFixtures --no-daemon --no-build-cache --stacktrace --max-workers=2 - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false ) @@ -66,7 +72,7 @@ jobs: gradle_args=( classes verifyLegacyFixtures --rerun-tasks --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false ) @@ -81,6 +87,12 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: @@ -103,7 +115,7 @@ jobs: run: >- ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath --no-daemon --stacktrace - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 749be529..de2b0963 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,6 +26,12 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: @@ -52,7 +58,7 @@ jobs: gradle_args=( clean classes --no-daemon --stacktrace --max-workers=2 - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64" + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false ) diff --git a/README.md b/README.md index aa880abb..e6fc4e79 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,11 @@ exported to `config/orespawn-guide/` without overwriting existing files. ## Building -Run Gradle with Java 17 from the repository root. Install the exact Temurin -`8.0.502+7` toolchain used to compile production code and test fixtures for -Minecraft 1.16.5; the build rejects a different Java 8 toolchain: +Run Gradle with Java 17 from the repository root. Install exact Temurin +`25.0.3+9` for ForgeGradle's Mavenizer and exact Temurin `8.0.502+7` for the +Minecraft 1.16.5 compilation and fixture toolchain. Java 17 remains the Gradle +runtime, while production bytecode remains Java 8; the build rejects a +different Java 8 toolchain: ```powershell .\gradlew.bat clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums --no-daemon diff --git a/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java index 07c2dd15..77e7365c 100644 --- a/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java @@ -40,14 +40,14 @@ void hostedWorkflowsUsePinnedJdksAndExerciseAColdForgeBootstrap() throws Excepti for (String workflow : new String[] { ci, codeql }) { assertFalse(workflow.contains("distribution: microsoft")); + assertTrue(workflow.contains("java-version: '25.0.3+9.0.LTS'")); assertTrue(workflow.contains("java-version: '8.0.502+7'")); assertTrue(workflow.contains("java-version: '17.0.1+12'")); - assertTrue(workflow.lastIndexOf("java-version: '17.0.1+12'") - > workflow.lastIndexOf("java-version: '8.0.502+7'")); - assertTrue(workflow.contains("$JAVA_HOME,$JAVA_HOME_8_X64")); assertTrue(workflow.contains("-Dorg.gradle.java.installations.auto-detect=false")); assertTrue(workflow.contains("-Dorg.gradle.java.installations.auto-download=false")); } + assertPinnedToolchains(ci, 2, 3); + assertPinnedToolchains(codeql, 1, 1); assertTrue(ci.contains("name: Cold Forge bootstrap")); assertTrue(ci.contains("GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle")); @@ -56,11 +56,43 @@ void hostedWorkflowsUsePinnedJdksAndExerciseAColdForgeBootstrap() throws Excepti assertTrue(ci.contains("classes verifyLegacyFixtures")); assertTrue(ci.contains("--rerun-tasks --offline --no-daemon --no-build-cache")); assertFalse(ci.contains("Mavenizer compatibility")); - assertFalse(ci.contains("25.0.3")); } private static String readWorkflow(String name) throws Exception { return new String(Files.readAllBytes(Paths.get(".github", "workflows", name)), StandardCharsets.UTF_8); } + + private static void assertPinnedToolchains(String workflow, int expectedJobs, int expectedPathUses) { + String java25 = "java-version: '25.0.3+9.0.LTS'"; + String java8 = "java-version: '8.0.502+7'"; + String java17 = "java-version: '17.0.1+12'"; + String paths = "$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64"; + + assertEquals(expectedJobs, occurrences(workflow, java25)); + assertEquals(expectedJobs, occurrences(workflow, java8)); + assertEquals(expectedJobs, occurrences(workflow, java17)); + assertEquals(expectedPathUses, occurrences(workflow, paths)); + + int cursor = 0; + for (int job = 0; job < expectedJobs; job++) { + int java25Index = workflow.indexOf(java25, cursor); + int java8Index = workflow.indexOf(java8, java25Index + java25.length()); + int java17Index = workflow.indexOf(java17, java8Index + java8.length()); + assertTrue(java25Index >= cursor); + assertTrue(java8Index > java25Index); + assertTrue(java17Index > java8Index); + cursor = java17Index + java17.length(); + } + } + + private static int occurrences(String value, String needle) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(needle, offset)) >= 0) { + count++; + offset += needle.length(); + } + return count; + } }