diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 287c1c8a..ad20ee60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,67 @@ 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 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: + 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,$JAVA_HOME_25_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,$JAVA_HOME_25_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 +87,23 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - 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: 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 +115,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,$JAVA_HOME_25_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 @@ -56,9 +126,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.8.114041.jar - build/libs/OreSpawn-4.0.8.114041-sources.jar - build/libs/OreSpawn-4.0.8.114041-javadoc.jar + build/libs/OreSpawn-4.0.16.114041.jar + build/libs/OreSpawn-4.0.16.114041-sources.jar + build/libs/OreSpawn-4.0.16.114041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f3dff15d..bfbe6223 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,17 +26,23 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - 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: 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 +55,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,$JAVA_HOME_25_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/CHANGELOG.txt b/CHANGELOG.txt index f7099589..09f2106d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,47 @@ +Version 4.0.16.114041 + +* Adopt the shared 4.0.16 release identity. Forge 1.14 has neither + three-dimensional biome-cell attribution nor a server-side GameTest harness, + so the 4.0.15 attribution and 4.0.16 GameTest lifecycle repairs are not + applicable on this target. +* Ordinary dedicated benchmark servers continue to stop automatically when + requested. + +Version 4.0.14.114041 + +* 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.114041 + +* 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.10.114041 + +* 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.114041 + +* Replace provider-declared natural terrain hosts during the existing geology + scan before structure and vegetation features can author matching blocks. +* Keep air, fluids, bedrock, and block entities protected even when their block + IDs are mistakenly declared as terrain hosts. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + Version 4.0.8.114041 * Preserve long host, tag, and biome-list values when OreSpawn editors load diff --git a/README.md b/README.md index 282a6da9..8232afee 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.8.114041`: the OreSpawn 4.0.8 +This branch builds target-qualified version `4.0.16.114041`: the OreSpawn 4.0.16 feature set for Minecraft 1.14.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. @@ -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.14.4; 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.14.4 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/build.gradle b/build.gradle index 96deebec..cb00e8b5 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 { @@ -464,6 +468,7 @@ level-name=surface-integration-world level-seed=zsjpxah level-type=default online-mode=false +server-port=0 allow-nether=true generate-structures=false spawn-protection=0 @@ -676,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}" ]) @@ -732,7 +737,8 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.8.114041' + if (project.mod_version != '4.0.16.114041' + || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.14.4' || project.forge_version != '28.2.26' || project.mapping_channel != 'snapshot' @@ -745,11 +751,11 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.8.114041.jar', - 'OreSpawn-4.0.8.114041-sources.jar', - 'OreSpawn-4.0.8.114041-javadoc.jar' + 'OreSpawn-4.0.16.114041.jar', + 'OreSpawn-4.0.16.114041-sources.jar', + 'OreSpawn-4.0.16.114041-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') } @@ -764,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.8.114041')) { + if (!file(path).getText('UTF-8').contains('4.0.16.114041')) { throw new GradleException("Release identity missing from ${path}") } } @@ -928,6 +934,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') } @@ -1027,8 +1034,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()) @@ -1064,6 +1071,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 { @@ -1079,6 +1106,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') @@ -1234,6 +1262,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') @@ -1266,6 +1295,7 @@ level-name=surface-integration-world level-seed=zsjpxah level-type=default online-mode=false +server-port=0 allow-nether=true generate-structures=false spawn-protection=0 diff --git a/docs/API.md b/docs/API.md index 30f4b92c..c8c42856 100644 --- a/docs/API.md +++ b/docs/API.md @@ -82,6 +82,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/BIOMES.md b/docs/BIOMES.md index d5a7d78a..d1d6232b 100644 --- a/docs/BIOMES.md +++ b/docs/BIOMES.md @@ -127,6 +127,13 @@ lets OreSpawn replace the actual exposed ground while preserving later trees, plants, authored structures, and block entities. In ceiling dimensions, `ceiling_block` applies to the roof underside and does not replace the roof top. +Provider-declared `terrain_dimensions.host_blocks` are resolved by one terrain +scan at the start of `LOCAL_MODIFICATIONS`, immediately before provider +surfaces. Matching natural blocks already present in base terrain are eligible +for geology; matching blocks authored by later structure or vegetation stages +are not. Air, fluids, bedrock, and block-entity states remain protected even if +a provider mistakenly lists their block IDs as terrain hosts. + Surface correction is generation-only. Installing or updating OreSpawn does not rewrite already generated chunks; travel into new terrain to see a changed provider surface definition. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 42badd92..534c41f9 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 5107f313..c07642aa 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -100,6 +100,10 @@ private void enqueueWorldgen(InterModEnqueueEvent event) { .quantityRange(4, 11) .pattern(OrePattern.VEIN) .heightDistribution(OreHeightDistribution.TRIANGLE) + .biome(new ResourceLocation("minecraft", "plains")) + .biomeDictionary("FOREST") + .excludeBiome(new ResourceLocation("minecraft", "dark_forest")) + .excludeBiomeDictionary("SPOOKY") .hostTag(new ResourceLocation("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 30572efb..8ce7bd9c 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -52,7 +52,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | -| 1.14.4 | Forge | `114041` | `4.0.8.114041` | +| 1.14.4 | Forge | `114041` | `4.0.16.114041` | | 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` | @@ -144,8 +144,18 @@ 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 to it. -A branch may therefore legitimately skip functional version numbers. +was not applicable there. Forge 1.14.4 then advanced to its target-qualified +4.0.9 release for the provider terrain-host ordering repair and to 4.0.10 for +the distinct Stable Layers actual-height eligibility repair. Forge 1.14 uses a +static, two-dimensional biome provider, so the dynamic-registry and vertical +sampler repairs released as 4.0.11 and 4.0.12 are not applicable; it advances +directly to 4.0.13 for provider biome-filter parity and namespaced geome support, +then to 4.0.14 so one-layer Snow above the motion-blocking surface is included +in configured weather-material conversion. Forge 1.14 has no Y-sensitive biome +cell attribution or server-side GameTest harness, so 4.0.15 and the GameTest +lifecycle portion of 4.0.16 are not applicable; it adopts the shared 4.0.16 +identity while retaining ordinary benchmark auto-stop. A branch may therefore +legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index a5e8ee69..3e7b3975 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,8 +19,8 @@ mcp_version=20190829.143755 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.8.114041 -mod_group=zone.moddev.mc +mod_version=4.0.16.114041 +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 3f3541b7..8219c474 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -120,6 +120,15 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation SPRING_ROCK = new ResourceLocation(MODID + ":rock/spring_host"); private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water"); private static final BlockPos SPRING_POS = new BlockPos(1128, 32, 1128); + 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, + Blocks.TERRACOTTA, Blocks.WHITE_TERRACOTTA, + Blocks.ORANGE_TERRACOTTA, Blocks.RED_TERRACOTTA + }; + private static final Block[] INVALID_TERRAIN_HOSTS = { + Blocks.AIR, Blocks.WATER, Blocks.BEDROCK, Blocks.CHEST + }; private static final ResourceLocation[] BUILT_IN_GEOMES = { new ResourceLocation("orespawn:stable_craton"), new ResourceLocation("orespawn:mountain_belt"), new ResourceLocation("orespawn:volcanic_arc"), new ResourceLocation("orespawn:sedimentary_basin"), @@ -140,9 +149,13 @@ public final class SurfaceProbeTestMod { private static final int FLUID_PROBE_MAX_CHUNK_X = 62; private static final int EXPECTED_COLUMNS = 9 * 16 * 16; private static final int EXPECTED_FILLER = EXPECTED_COLUMNS * 3; + private static final int EXPECTED_NATURAL_SOURCES = 9 * NATURAL_SOURCES.length; private static final String PHASE_PROPERTY = "surfaceprobe.integrationPhase"; 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.getDefaultState(); + private static final BlockState WEATHER_ICE_REPLACEMENT = Blocks.BLUE_ICE.getDefaultState(); public SurfaceProbeTestMod() { FMLJavaModLoadingContext context = FMLJavaModLoadingContext.get(); @@ -205,6 +218,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())) { @@ -262,6 +278,8 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent event) { end.add("biome_namespaces", namespaces); JsonArray hosts = new JsonArray(); hosts.add(blockId(Blocks.END_STONE).toString()); + for (Block source : NATURAL_SOURCES) hosts.add(blockId(source).toString()); + for (Block source : INVALID_TERRAIN_HOSTS) hosts.add(blockId(source).toString()); end.add("host_blocks", hosts); end.add("host_tags", new JsonArray()); terrain.add(OPEN_ID.toString(), end); @@ -433,6 +451,19 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { int biomeB = 0; int edgeChanges = 0; int sentinels = 0; + long rawNaturalSources = 0L; + long structureNaturalSources = 0L; + long vegetationNaturalSources = 0L; + long cavePockets = 0L; + long underwaterPockets = 0L; + long rawBedrock = 0L; + long rawBlockEntities = 0L; + long exposedSnowConverted = 0L; + long surfaceIceConverted = 0L; + long buriedSnowPreserved = 0L; + long buriedIcePreserved = 0L; + long unconfiguredSnowPreserved = 0L; + long unconfiguredIcePreserved = 0L; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -501,22 +532,159 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) { } } sentinels += auditSentinels(level, chunk, pos, chunkMinX, chunkMinZ); + if (!roofed) { + NaturalSourceAudit natural = auditNaturalSources(level, chunk, pos, chunkMinX, chunkMinZ); + rawNaturalSources += natural.rawConverted; + structureNaturalSources += natural.structurePreserved; + vegetationNaturalSources += natural.vegetationPreserved; + cavePockets += natural.cavePreserved; + underwaterPockets += natural.underwaterPreserved; + 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(); } } 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))) { - throw new IllegalStateException("Incomplete surface audit for " + DimensionType.getKey(level.dimension.getType()) + || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS + || unconfiguredSnowPreserved != 9 || unconfiguredIcePreserved != 9 + || exposedSnowConverted != 0 || surfaceIceConverted != 0 + || buriedSnowPreserved != 0 || buriedIcePreserved != 0)) + || (!roofed && (rawNaturalSources != EXPECTED_NATURAL_SOURCES + || structureNaturalSources != EXPECTED_NATURAL_SOURCES + || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES + || cavePockets != EXPECTED_NATURAL_SOURCES / 2 + || underwaterPockets != EXPECTED_NATURAL_SOURCES / 2 + || rawBedrock != 9 || rawBlockEntities != 9 + || exposedSnowConverted != 9 || surfaceIceConverted != 9 + || buriedSnowPreserved != 9 || buriedIcePreserved != 9 + || unconfiguredSnowPreserved != 0 || unconfiguredIcePreserved != 0))) { + throw new IllegalStateException("Incomplete surface audit for " + + DimensionType.getKey(level.dimension.getType()) + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edgeChanges + ", sentinels=" + sentinels + ", geology=" + geology - + ", ceiling=" + ceiling + ", roofTop=" + roofTop); + + ", ceiling=" + ceiling + ", roofTop=" + roofTop + + ", rawNatural=" + rawNaturalSources + + ", structureNatural=" + structureNaturalSources + + ", vegetationNatural=" + vegetationNaturalSources + + ", cavePockets=" + cavePockets + + ", underwaterPockets=" + underwaterPockets + + ", rawBedrock=" + rawBedrock + + ", rawBlockEntities=" + rawBlockEntities + + ", exposedSnowConverted=" + exposedSnowConverted + + ", surfaceIceConverted=" + surfaceIceConverted + + ", buriedSnowPreserved=" + buriedSnowPreserved + + ", buriedIcePreserved=" + buriedIcePreserved + + ", unconfiguredSnowPreserved=" + unconfiguredSnowPreserved + + ", unconfiguredIcePreserved=" + unconfiguredIcePreserved); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); return new AuditResult(top, underwater, filler, geology, ceiling, roofTop, - biomeA, biomeB, edgeChanges, sentinels, aquiferFluid); + biomeA, biomeB, edgeChanges, sentinels, aquiferFluid, + rawNaturalSources, structureNaturalSources, vegetationNaturalSources, + cavePockets, underwaterPockets, rawBedrock, rawBlockEntities, + exposedSnowConverted, surfaceIceConverted, + buriedSnowPreserved, buriedIcePreserved, + unconfiguredSnowPreserved, unconfiguredIcePreserved); + } + + private static WeatherMaterialAudit auditWeatherMaterials(IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ, int minY, int maxY, + boolean roofed) { + int snowGroundY = findMarkedGround(chunk, pos, minX + 2, minZ + 2, minY, maxY); + int iceGroundY = findMarkedGround(chunk, pos, minX + 3, minZ + 2, minY, maxY); + if (roofed) { + return new WeatherMaterialAudit(0L, 0L, 0L, 0L, + assertState(chunk, pos.setPos(minX + 2, snowGroundY + 11, minZ + 2), + Blocks.SNOW.getDefaultState(), "unconfigured exposed Snow preservation"), + assertState(chunk, pos.setPos(minX + 3, iceGroundY + 11, minZ + 2), + Blocks.ICE.getDefaultState(), "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.setPos(minX + 2, snowGroundY + 1, minZ + 2), + WEATHER_SNOW_REPLACEMENT, "exposed Snow weather replacement"), + assertState(chunk, pos.setPos(minX + 3, iceGroundY + 1, minZ + 2), + WEATHER_ICE_REPLACEMENT, "surface Ice weather replacement"), + assertState(chunk, pos.setPos(minX + 2, buriedSnowGroundY - 24, minZ + 3), + Blocks.SNOW.getDefaultState(), "buried authored Snow preservation"), + assertState(chunk, pos.setPos(minX + 3, buriedIceGroundY - 24, minZ + 3), + Blocks.ICE.getDefaultState(), "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 NaturalSourceAudit auditNaturalSources(ServerWorld level, IChunk chunk, BlockPos.MutableBlockPos pos, + int minX, int minZ) { + long rawConverted = 0L; + long structurePreserved = 0L; + long vegetationPreserved = 0L; + long cavePreserved = 0L; + long underwaterPreserved = 0L; + long bedrockPreserved = 0L; + long blockEntityPreserved = 0L; + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index); + int z = naturalZ(minZ, index); + int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); + if (chunk.getBlockState(pos.setPos(x, groundY - 12, z)).getBlock() == Blocks.DIORITE) { + rawConverted++; + } + Block pocket = chunk.getBlockState(pos.setPos(x, groundY - 11, z)).getBlock(); + if (index < NATURAL_SOURCES.length / 2) { + if (pocket == Blocks.AIR) cavePreserved++; + } else if (pocket == Blocks.WATER) { + underwaterPreserved++; + } + if (chunk.getBlockState(pos.setPos(x, groundY - 16, z)).getBlock() + == NATURAL_SOURCES[index]) { + structurePreserved++; + } + if (chunk.getBlockState(pos.setPos(x, groundY - 20, z)).getBlock() + == NATURAL_SOURCES[index]) { + vegetationPreserved++; + } + } + int bedrockGroundY = findMarkedGround(chunk, pos, minX + 11, minZ + 12, 0, 256); + if (chunk.getBlockState(pos.setPos(minX + 11, bedrockGroundY - 24, minZ + 12)).getBlock() + == Blocks.BEDROCK) { + bedrockPreserved++; + } + int chestGroundY = findMarkedGround(chunk, pos, minX + 12, minZ + 12, 0, 256); + pos.setPos(minX + 12, chestGroundY - 24, minZ + 12); + if (chunk.getBlockState(pos).getBlock() == Blocks.CHEST + && level.getTileEntity(pos) instanceof ChestTileEntity) { + ChestTileEntity chest = (ChestTileEntity) level.getTileEntity(pos); + if (chest != null && chest.getStackInSlot(0).getItem() == Items.EMERALD + && RAW_CHEST_ITEM_NAME.equals( + chest.getStackInSlot(0).getDisplayName().getString())) { + blockEntityPreserved++; + } + } + return new NaturalSourceAudit(rawConverted, structurePreserved, + vegetationPreserved, cavePreserved, underwaterPreserved, + bedrockPreserved, blockEntityPreserved); } private static long auditDynamicFluid(ServerWorld level) { @@ -660,6 +828,19 @@ private static Properties properties(long seed, Map results values.setProperty(prefix + "edge_changes", Integer.toString(result.edgeChanges())); values.setProperty(prefix + "sentinels", Integer.toString(result.sentinels())); values.setProperty(prefix + "aquifer_fluid", Long.toString(result.aquiferFluid())); + values.setProperty(prefix + "raw_natural_sources", Long.toString(result.rawNaturalSources())); + values.setProperty(prefix + "structure_natural_sources", Long.toString(result.structureNaturalSources())); + values.setProperty(prefix + "vegetation_natural_sources", Long.toString(result.vegetationNaturalSources())); + values.setProperty(prefix + "cave_pockets", Long.toString(result.cavePockets())); + 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 + "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; } @@ -754,9 +935,37 @@ private static boolean prepareTerrain(IWorld world, IChunk chunk) { } } } + if (!roofed) placeRawNaturalSources(world, chunk, pos, minX, minZ); return true; } + private static void placeRawNaturalSources(IWorld world, IChunk chunk, BlockPos.MutableBlockPos pos, + int minX, int minZ) { + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index); + int z = naturalZ(minZ, index); + int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); + chunk.setBlockState(pos.setPos(x, groundY - 12, z), + NATURAL_SOURCES[index].getDefaultState(), false); + chunk.setBlockState(pos.setPos(x, groundY - 11, z), + (index < NATURAL_SOURCES.length / 2 ? Blocks.AIR : Blocks.WATER) + .getDefaultState(), false); + } + int bedrockGroundY = findMarkedGround(chunk, pos, minX + 11, minZ + 12, 0, 256); + chunk.setBlockState(pos.setPos(minX + 11, bedrockGroundY - 24, minZ + 12), + Blocks.BEDROCK.getDefaultState(), false); + int chestGroundY = findMarkedGround(chunk, pos, minX + 12, minZ + 12, 0, 256); + world.setBlockState(pos.setPos(minX + 12, chestGroundY - 24, minZ + 12), + Blocks.CHEST.getDefaultState(), 2); + if (world.getTileEntity(pos) instanceof ChestTileEntity) { + ChestTileEntity chest = (ChestTileEntity) world.getTileEntity(pos); + ItemStack sentinel = new ItemStack(Items.EMERALD); + sentinel.setDisplayName(new StringTextComponent(RAW_CHEST_ITEM_NAME)); + chest.setInventorySlotContents(0, sentinel); + chest.markDirty(); + } + } + private static boolean solid(BlockState state) { return !state.isAir() && state.getFluidState().isEmpty(); } @@ -778,6 +987,7 @@ private static boolean placeStructureSentinels(IWorld world, IChunk chunk) { chest.setInventorySlotContents(0, sentinel); chest.markDirty(); } + placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 16); return true; } @@ -794,9 +1004,56 @@ private static boolean placeVegetationSentinels(IWorld world, IChunk chunk) { int vegetationY = markedGround(chunk, pos, minX + 6, minZ + 6, world); world.setBlockState(pos.setPos(minX + 6, vegetationY + 1, minZ + 6), Blocks.DIRT.getDefaultState(), 2); world.setBlockState(pos.setPos(minX + 6, vegetationY + 2, minZ + 6), Blocks.OAK_SAPLING.getDefaultState(), 2); + placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 20); + placeWeatherMaterialSentinels(world, chunk, pos, minX, minZ); return true; } + private static void placeWeatherMaterialSentinels(IWorld world, IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ) { + int snowGroundY = markedGround(chunk, pos, minX + 2, minZ + 2, world); + int iceGroundY = markedGround(chunk, pos, minX + 3, minZ + 2, world); + ResourceLocation dimension = DimensionType.getKey(world.getWorld().dimension.getType()); + if (ROOFED_ID.equals(dimension)) { + world.setBlockState(pos.setPos(minX + 2, snowGroundY + 11, minZ + 2), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(pos.setPos(minX + 3, iceGroundY + 11, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + return; + } + if (!OPEN_ID.equals(dimension)) return; + world.setBlockState(pos.setPos(minX + 2, snowGroundY + 1, minZ + 2), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(pos.setPos(minX + 3, iceGroundY + 1, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + int buriedSnowGroundY = markedGround(chunk, pos, minX + 2, minZ + 3, world); + int buriedIceGroundY = markedGround(chunk, pos, minX + 3, minZ + 3, world); + world.setBlockState(pos.setPos(minX + 2, buriedSnowGroundY - 24, minZ + 3), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(pos.setPos(minX + 3, buriedIceGroundY - 24, minZ + 3), + Blocks.ICE.getDefaultState(), 2); + } + + private static void placeAuthoredNaturalSources(IWorld world, IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ, int depth) { + if (!OPEN_ID.equals(DimensionType.getKey(world.getWorld().dimension.getType()))) return; + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index); + int z = naturalZ(minZ, index); + int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); + world.setBlockState(pos.setPos(x, groundY - depth, z), + NATURAL_SOURCES[index].getDefaultState(), 2); + } + } + + private static int naturalX(int minX, int index) { + return minX + 12 + index % 4; + } + + private static int naturalZ(int minZ, int index) { + return minZ + 1 + index / 4; + } + private static int markedGround(IChunk chunk, BlockPos.MutableBlockPos pos, int x, int z, IWorld world) { return findMarkedGround(chunk, pos, x, z, 0, 256); @@ -839,6 +1096,55 @@ private static final class SpringResult { ResourceLocation block() { return block; } } + private static final class NaturalSourceAudit { + final long rawConverted; + final long structurePreserved; + final long vegetationPreserved; + final long cavePreserved; + final long underwaterPreserved; + final long bedrockPreserved; + final long blockEntityPreserved; + + NaturalSourceAudit(long rawConverted, long structurePreserved, + long vegetationPreserved, long cavePreserved, long underwaterPreserved, + long bedrockPreserved, long blockEntityPreserved) { + this.rawConverted = rawConverted; + this.structurePreserved = structurePreserved; + this.vegetationPreserved = vegetationPreserved; + this.cavePreserved = cavePreserved; + this.underwaterPreserved = underwaterPreserved; + this.bedrockPreserved = bedrockPreserved; + this.blockEntityPreserved = blockEntityPreserved; + } + } + + 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; @@ -851,10 +1157,28 @@ private static final class AuditResult { private final int edgeChanges; private final int sentinels; private final long aquiferFluid; + private final long rawNaturalSources; + private final long structureNaturalSources; + private final long vegetationNaturalSources; + private final long cavePockets; + private final long underwaterPockets; + private final long rawBedrock; + private final long rawBlockEntities; + 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 aquiferFluid, long rawNaturalSources, long structureNaturalSources, + long vegetationNaturalSources, long cavePockets, long underwaterPockets, + long rawBedrock, long rawBlockEntities, + long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { this.top = top; this.underwater = underwater; this.filler = filler; @@ -866,6 +1190,19 @@ private static final class AuditResult { this.edgeChanges = edgeChanges; this.sentinels = sentinels; this.aquiferFluid = aquiferFluid; + this.rawNaturalSources = rawNaturalSources; + this.structureNaturalSources = structureNaturalSources; + this.vegetationNaturalSources = vegetationNaturalSources; + this.cavePockets = cavePockets; + this.underwaterPockets = underwaterPockets; + this.rawBedrock = rawBedrock; + this.rawBlockEntities = rawBlockEntities; + this.exposedSnowConverted = exposedSnowConverted; + this.surfaceIceConverted = surfaceIceConverted; + this.buriedSnowPreserved = buriedSnowPreserved; + this.buriedIcePreserved = buriedIcePreserved; + this.unconfiguredSnowPreserved = unconfiguredSnowPreserved; + this.unconfiguredIcePreserved = unconfiguredIcePreserved; } long top() { return top; } @@ -879,5 +1216,18 @@ private static final class AuditResult { int edgeChanges() { return edgeChanges; } int sentinels() { return sentinels; } long aquiferFluid() { return aquiferFluid; } + long rawNaturalSources() { return rawNaturalSources; } + long structureNaturalSources() { return structureNaturalSources; } + long vegetationNaturalSources() { return vegetationNaturalSources; } + long cavePockets() { return cavePockets; } + long underwaterPockets() { return underwaterPockets; } + long rawBedrock() { return rawBedrock; } + long rawBlockEntities() { return rawBlockEntities; } + 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/api/WorldgenProvider.java b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java index bf8d421e..366ca40e 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 d2c259cf..86d1bf8f 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; } @@ -1169,6 +1169,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/BakedGeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java index ff7c3cfc..b5f52a9c 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/BakedTerrainDimension.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java index c6400d0d..e69a401a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java @@ -7,6 +7,7 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.block.Block; import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; /** Immutable setup-time resolution of one terrain replacement dimension. */ final class BakedTerrainDimension { @@ -36,6 +37,10 @@ boolean hasBiomeFilter() { } boolean isReplaceable(BlockState state) { + if (state.isAir() || !state.getFluidState().isEmpty() + || state.getBlock() == Blocks.BEDROCK) { + return false; + } if (smallHostSet != null) { Block block = state.getBlock(); for (Block host : smallHostSet) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java index faf33628..04765e5a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java @@ -67,7 +67,6 @@ static void installFeatures(Biome biome, boolean terrain, if (vanillaOreGate) VanillaOreFeatureGate.wrapFeatureList(underground); if (terrain) { StoneReplacer.removeVanillaMatchingStoneFeatures(underground); - addUnique(underground, StoneReplacer.configuredFeature()); } if (managedOres) addUnique(underground, OreSpawnOreGeneration.configuredFeature()); if (fluidDeposits) addUnique(underground, FluidDepositFeature.configuredFeature()); @@ -76,13 +75,22 @@ static void installFeatures(Biome biome, boolean terrain, VanillaOreFeatureGate.wrapFeatureList( biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_DECORATION)); } - installSurfaceStages(biome, surfaces, flatBedrock); + installSurfaceStages(biome, terrain, surfaces, flatBedrock); } - static boolean installSurfaceStages(Biome biome, boolean surfaces, boolean flatBedrock) { - boolean changed = surfaces && addUnique(biome.getFeatures( - GenerationStage.Decoration.LOCAL_MODIFICATIONS), - BiomeSurfaceFeature.configuredFeature()); + static boolean installSurfaceStages(Biome biome, boolean terrain, + boolean surfaces, boolean flatBedrock) { + List> local = + biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS); + boolean changed = false; + if (terrain) { + changed |= placeUniqueAt(local, StoneReplacer.configuredFeature(), 0); + if (surfaces) { + changed |= placeUniqueAt(local, BiomeSurfaceFeature.configuredFeature(), 1); + } + } else if (surfaces) { + changed |= addUnique(local, BiomeSurfaceFeature.configuredFeature()); + } changed |= flatBedrock && addUnique(biome.getFeatures( GenerationStage.Decoration.TOP_LAYER_MODIFICATION), FlatBedrockFeature.configuredFeature()); @@ -103,4 +111,15 @@ private static boolean addUnique(List> features, features.add(feature); return true; } + + private static boolean placeUniqueAt(List> features, + ConfiguredFeature feature, int index) { + if (feature == null) return false; + int current = features.indexOf(feature); + int target = Math.min(index, features.size() - (current >= 0 ? 1 : 0)); + if (current == target) return false; + if (current >= 0) features.remove(current); + features.add(target, feature); + return true; + } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java index 75f6b506..6a831243 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java @@ -111,8 +111,9 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (; y >= 0; y--) { cursor.setPos(x, y, z); BlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current) - || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) { + if ((terrain.isReplaceable(current) + || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) + && chunk.getTileEntity(cursor) == null) { BlockState replacement = pickReplacement(baseRockVal, geomeBase, y); if (!GeomeGeology.changes(current, replacement)) continue; chunk.setBlockState(cursor, replacement, false); 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 4fa642c3..c63b5316 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -115,7 +115,7 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (int y = surfaceY; y >= 0; y--) { cursor.setPos(x, y, z); BlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current)) { + if (terrain.isReplaceable(current) && chunk.getTileEntity(cursor) == null) { BlockState replacement = pickReplacement( geomeIndex, baseRockValue, formationRegion, x, y, z); if (!changes(current, replacement)) continue; @@ -140,7 +140,6 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.MutableBlockPos curso int layerStart = layerIndex * layerThickness; int layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - BlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); boolean changed = false; cursor.setPos(x, surfaceY, z); @@ -151,11 +150,12 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.MutableBlockPos curso layerStart -= layerThickness; layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); } cursor.setY(y); BlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current) && changes(current, replacement)) { + BlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex, y); + if (terrain.isReplaceable(current) && chunk.getTileEntity(cursor) == null + && changes(current, replacement)) { chunk.setBlockState(cursor, replacement, false); changed = true; } @@ -253,7 +253,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); @@ -263,7 +263,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; @@ -289,8 +289,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 50ac750c..5b4fe027 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.8.114041 Upgrade Report"); + lines.add("OreSpawn 4.0.16.114041 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 e40c2845..d967c7e2 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.8.114041 Upgrade Report"); + lines.add("OreSpawn 4.0.16.114041 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 a6f1b946..4ccdc557 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.getTopBlockY(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.setPos(minX + localX, top + 1, minZ + localZ); + if (chunk.getBlockState(cursor).getBlock() == Blocks.SNOW) { + chunk.setBlockState(cursor, materials.snow, false); + } + } for (int offset = 0; offset <= 2; offset++) { cursor.setPos(minX + localX, top - offset, minZ + localZ); BlockState state = chunk.getBlockState(cursor); 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..77e7365c --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java @@ -0,0 +1,98 @@ +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: '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.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")); + 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")); + } + + 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; + } +} 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 41fb3180..7993377d 100644 --- a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java @@ -13,6 +13,32 @@ import net.minecraft.util.ResourceLocation; class WorldgenProviderTest { + @Test + void terrainHostContractRetainsNaturalSourceOrder() { + ResourceLocation dimension = id("surfaceprobe:the_end"); + WorldgenProvider provider = WorldgenProvider.builder("surfaceprobe", 1) + .terrainDimension(dimension, terrain -> terrain + .hostBlock(id("minecraft:dirt")) + .hostBlock(id("minecraft:grass_block")) + .hostBlock(id("minecraft:coarse_dirt")) + .hostBlock(id("minecraft:podzol")) + .hostBlock(id("minecraft:gravel")) + .hostBlock(id("minecraft:sand")) + .hostBlock(id("minecraft:red_sand")) + .hostBlock(id("minecraft:clay")) + .hostBlock(id("minecraft:terracotta"))) + .build(); + + assertEquals("[\"minecraft:dirt\",\"minecraft:grass_block\"," + + "\"minecraft:coarse_dirt\",\"minecraft:podzol\"," + + "\"minecraft:gravel\",\"minecraft:sand\"," + + "\"minecraft:red_sand\",\"minecraft:clay\"," + + "\"minecraft:terracotta\"]", + provider.toJson().getAsJsonObject("terrain_dimensions") + .getAsJsonObject(dimension.toString()) + .getAsJsonArray("host_blocks").toString()); + } + @Test void serializesTypedSchemaFourProvider() { ResourceLocation overworld = id("minecraft:overworld"); @@ -242,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 4221fbe0..c9d18464 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()); + } } diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java index ed356895..604239f9 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; + import net.minecraft.util.registry.Bootstrap; import net.minecraft.world.biome.Biome; import net.minecraft.world.gen.GenerationStage; @@ -35,7 +37,7 @@ void staticInstallerKeepsSurfacesEarlyAndBedrockLast() throws ReflectiveOperatio .surfaceBuilder(SurfaceBuilder.DEFAULT, SurfaceBuilder.GRASS_DIRT_GRAVEL_CONFIG)); BiomeFeatureInstaller.installFeatures(biome, true, true, true, true, true, true); - assertTrue(biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_ORES) + assertFalse(biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_ORES) .contains(StoneReplacer.configuredFeature())); assertTrue(biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_ORES) .contains(OreSpawnOreGeneration.configuredFeature())); @@ -43,7 +45,11 @@ void staticInstallerKeepsSurfacesEarlyAndBedrockLast() throws ReflectiveOperatio .contains(FluidDepositFeature.configuredFeature())); ConfiguredFeature surfaces = BiomeSurfaceFeature.configuredFeature(); ConfiguredFeature bedrock = FlatBedrockFeature.configuredFeature(); - assertTrue(biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS).contains(surfaces)); + List> local = + biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS); + assertTrue(local.size() >= 2); + assertTrue(local.get(0) == StoneReplacer.configuredFeature()); + assertTrue(local.get(1) == surfaces); assertFalse(biome.getFeatures(GenerationStage.Decoration.TOP_LAYER_MODIFICATION).contains(surfaces)); assertTrue(biome.getFeatures(GenerationStage.Decoration.TOP_LAYER_MODIFICATION).contains(bedrock)); assertFalse(biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS).contains(bedrock)); 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..2c123b72 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java @@ -0,0 +1,55 @@ +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.register(); + } + + @Test + void legalWorldYRemainsEligibleWhenFormationFallsBelowMinimum() { + BakedGeomeConfig config = netherFloorConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.DIORITE, + 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"); + } + + @Test + void illegalWorldYRemainsIneligibleWhenFormationFallsInsideRange() { + BakedGeomeConfig config = netherFloorConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + 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.DIORITE.getDefaultState(), 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); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java index 12dd0690..b1658102 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java @@ -3,6 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Collections; +import java.util.LinkedHashSet; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -39,4 +42,21 @@ void nonOverworldBiomesAreNeverChanged() { assertFalse(TerrainFeaturePolicy.shouldRemoveVanillaMatchingStoneFeatures( Category.THEEND, true, true)); } + + @Test + void invalidTerrainHostsRemainUnsafeEvenWhenDeclared() { + LinkedHashSet hosts = new LinkedHashSet<>(); + hosts.add(Blocks.AIR); + hosts.add(Blocks.WATER); + hosts.add(Blocks.BEDROCK); + hosts.add(Blocks.DIRT); + BakedTerrainDimension terrain = new BakedTerrainDimension( + new net.minecraft.util.ResourceLocation("surfaceprobe:the_end"), + Collections.emptySet(), Collections.emptySet(), hosts); + + assertFalse(terrain.isReplaceable(Blocks.AIR.getDefaultState())); + assertFalse(terrain.isReplaceable(Blocks.WATER.getDefaultState())); + assertFalse(terrain.isReplaceable(Blocks.BEDROCK.getDefaultState())); + assertTrue(terrain.isReplaceable(Blocks.DIRT.getDefaultState())); + } }