diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b2034e..8535b6a9 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.113021.jar - build/libs/OreSpawn-4.0.8.113021-sources.jar - build/libs/OreSpawn-4.0.8.113021-javadoc.jar + build/libs/OreSpawn-4.0.16.113021.jar + build/libs/OreSpawn-4.0.16.113021-sources.jar + build/libs/OreSpawn-4.0.16.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5312991f..e55aff14 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 8db0c4be..ceccc816 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,47 @@ +Version 4.0.16.113021 + +* Adopt the shared 4.0.16 release identity. Forge 1.13 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.113021 + +* 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.113021 + +* 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.113021 + +* 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.113021 + +* 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.113021 * Preserve long host, tag, and biome-list values when OreSpawn editors load diff --git a/README.md b/README.md index e8785a02..6603a033 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.113021`: the OreSpawn 4.0.8 +This branch builds target-qualified version `4.0.16.113021`: the OreSpawn 4.0.16 feature set for Minecraft 1.13.2 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.13.2; 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.13.2 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 453268a0..e29ea486 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 { @@ -462,6 +466,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 @@ -671,7 +676,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}" ]) @@ -727,7 +732,8 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.8.113021' + if (project.mod_version != '4.0.16.113021' + || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' || project.mapping_channel != 'snapshot' @@ -740,11 +746,11 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.8.113021.jar', - 'OreSpawn-4.0.8.113021-sources.jar', - 'OreSpawn-4.0.8.113021-javadoc.jar' + 'OreSpawn-4.0.16.113021.jar', + 'OreSpawn-4.0.16.113021-sources.jar', + 'OreSpawn-4.0.16.113021-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') } @@ -759,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.8.113021')) { + if (!file(path).getText('UTF-8').contains('4.0.16.113021')) { throw new GradleException("Release identity missing from ${path}") } } @@ -871,6 +877,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') } @@ -969,8 +976,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()) @@ -1006,6 +1013,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 { @@ -1021,11 +1048,14 @@ tasks.register('validateMavenReleaseCredentials') { } tasks.withType(PublishToMavenRepository).configureEach { dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn tasks.named('verifyMavenCoordinates') dependsOn preparedReleaseDir.isPresent() ? tasks.named('verifyPreparedReleaseArtifacts') : tasks.named('verifyReleaseArtifacts') } +tasks.named('check') { dependsOn tasks.named('verifyMavenCoordinates') } + eclipse { classpath { downloadSources = true @@ -1184,6 +1214,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 6436bd28..de09df69 100644 --- a/docs/API.md +++ b/docs/API.md @@ -83,6 +83,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. + Create one `BiomeRegistrar` during normal mod construction. It attaches to the calling mod's event bus and defers biome factories until Forge's biome registry event. `OreSpawnBiomes.copyAndRegister` clones a known biome without adding a diff --git a/docs/BIOMES.md b/docs/BIOMES.md index 51388daa..3a922201 100644 --- a/docs/BIOMES.md +++ b/docs/BIOMES.md @@ -133,6 +133,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 8e774a11..026f66ec 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 36695682..8c754315 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 f76d3d21..eb27966a 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | -| 1.13.2 | Forge | `113021` | `4.0.8.113021` | +| 1.13.2 | Forge | `113021` | `4.0.16.113021` | | 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` | @@ -143,8 +143,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.13.2's `4.0.8.113021`, 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.13.2 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.13 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.13 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 09f8c850..7e8dce16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,8 +19,8 @@ mcp_version=20190213.203750 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.8.113021 -mod_group=zone.moddev.mc +mod_version=4.0.16.113021 +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 8e73c743..38f2e7ab 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -111,6 +111,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"), @@ -131,9 +140,12 @@ 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 IBlockState WEATHER_SNOW_REPLACEMENT = Blocks.WHITE_WOOL.getDefaultState(); + private static final IBlockState WEATHER_ICE_REPLACEMENT = Blocks.BLUE_ICE.getDefaultState(); public SurfaceProbeTestMod() { FMLJavaModLoadingContext context = FMLJavaModLoadingContext.get(); @@ -212,6 +224,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())) { @@ -269,6 +284,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); @@ -440,12 +457,29 @@ private static AuditResult auditDimension(WorldServer 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++) { for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) { level.getChunk(chunkX, chunkZ); Chunk chunk = level.getChunk(chunkX, chunkZ); + // Exercise the production conversion seam after later weather/authored + // sentinels have been placed, as a real chunk reload would. + zone.moddev.mc.orespawn.worldgen.WorldMaterialWeather.onChunkLoad( + new net.minecraftforge.event.world.ChunkEvent.Load(chunk)); int chunkMinX = chunkX << 4; int chunkMinZ = chunkZ << 4; for (int localZ = 0; localZ < 16; localZ++) { @@ -508,22 +542,151 @@ private static AuditResult auditDimension(WorldServer 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))) { + || (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.func_212678_a(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, + IBlockState expected, String label) { + IBlockState 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(WorldServer 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 + 10, minZ + 10, 0, 256); + pos.setPos(minX + 10, chestGroundY + 1, minZ + 10); + if (chunk.getBlockState(pos).getBlock() == Blocks.CHEST + && level.getTileEntity(pos) instanceof TileEntityChest) { + TileEntityChest chest = (TileEntityChest) level.getTileEntity(pos); + if (chest.getStackInSlot(0).getItem() == Items.DIAMOND + && CHEST_ITEM_NAME.equals(chest.getStackInSlot(0).getDisplayName().getString())) { + blockEntityPreserved++; + } + } + return new NaturalSourceAudit(rawConverted, structurePreserved, + vegetationPreserved, cavePreserved, underwaterPreserved, + bedrockPreserved, blockEntityPreserved); } private static long auditDynamicFluid(WorldServer level) { @@ -667,6 +830,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; } @@ -708,6 +884,8 @@ public boolean func_212245_a(IWorld world, BlockPos origin, NoFeatureConfig config) { IChunk chunk = world.getChunk((origin.getX() >> 4) + 1, (origin.getZ() >> 4) + 1); + if (chunk.getPos().x < MINIMUM_CHUNK || chunk.getPos().x > MAXIMUM_CHUNK + || chunk.getPos().z < MINIMUM_CHUNK || chunk.getPos().z > MAXIMUM_CHUNK) return false; if (stage == ProbeStage.TERRAIN) return prepareTerrain(world, chunk); if (stage == ProbeStage.STRUCTURE) return placeStructureSentinels(world, chunk); return placeVegetationSentinels(world, chunk); @@ -766,9 +944,32 @@ private static boolean prepareTerrain(IWorld world, IChunk chunk) { // Decoration writes do not update Forge 25's frozen *_WG maps. Rebuild // the controlled fixture map so the production pass sees the exposed Y. SurfaceProbeSpringBridge.rebuildWorldSurfaceHeight(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) { + // Forge 25 decorates through a wider ProtoChunk neighbourhood than the + // three-by-three audit. Do not leave block-entity sentinels in incomplete + // boundary chunks that the server cannot serialize safely. + if (chunk.getPos().x < MINIMUM_CHUNK || chunk.getPos().x > MAXIMUM_CHUNK + || chunk.getPos().z < MINIMUM_CHUNK || chunk.getPos().z > MAXIMUM_CHUNK) 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); + 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); + } + private static boolean solid(IBlockState state) { return !state.isAir() && state.getFluidState().isEmpty(); } @@ -782,14 +983,16 @@ private static boolean placeStructureSentinels(IWorld world, IChunk chunk) { world.setBlockState(pos.setPos(minX + 8, structureY + 1, minZ + 8), Blocks.GOLD_BLOCK.getDefaultState(), 2); int chestY = markedGround(chunk, pos, minX + 10, minZ + 10, world); - world.setBlockState(pos.setPos(minX + 10, chestY + 1, minZ + 10), Blocks.CHEST.getDefaultState(), 2); - if (world.getTileEntity(pos) instanceof TileEntityChest) { - TileEntityChest chest = (TileEntityChest) world.getTileEntity(pos); + BlockPos chestPos = new BlockPos(minX + 10, chestY + 1, minZ + 10); + world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); + if (world.getTileEntity(chestPos) instanceof TileEntityChest) { + TileEntityChest chest = (TileEntityChest) world.getTileEntity(chestPos); ItemStack sentinel = new ItemStack(Items.DIAMOND); sentinel.setDisplayName(new TextComponentString(CHEST_ITEM_NAME)); chest.setInventorySlotContents(0, sentinel); chest.markDirty(); } + placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 16); return true; } @@ -806,9 +1009,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.func_212678_a(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.func_212678_a(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); @@ -851,6 +1101,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; @@ -863,10 +1162,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; @@ -878,6 +1195,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; } @@ -891,5 +1221,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 20bce863..0ad223ba 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -623,8 +623,8 @@ 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)) { + String normalized = id.trim(); + if (!validGeomeId(normalized) || section("geomes").has(normalized)) { return; } JsonObject geome = new JsonObject(); @@ -684,7 +684,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; } @@ -1168,6 +1168,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 87e4212a..5e03dc9d 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 IBlockState[] 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 IBlockState[rocks.length]; for (int i = 0; i < rocks.length; i++) { rockStates[i] = rocks[i].state; @@ -154,13 +159,59 @@ RockFamily pickFamily(int geomeIndex, int y, int formationValue, int diversitySl public IBlockState 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); } + 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; + } + + IBlockState 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 +334,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 +345,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 +363,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 +415,10 @@ 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 +431,21 @@ 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 +700,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 c23f3f0e..908f9d4b 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.state.IBlockState; +import net.minecraft.init.Blocks; /** Immutable setup-time resolution of one terrain replacement dimension. */ final class BakedTerrainDimension { @@ -36,6 +37,10 @@ boolean hasBiomeFilter() { } boolean isReplaceable(IBlockState state) { + if (state.isAir() || !state.getFluidState().isEmpty() || state.hasTileEntity() + || 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 75522973..c3e153d2 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, + CompositeFeature 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 50a429ee..55e863b7 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,10 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (; y >= 0; y--) { cursor.setPos(x, y, z); IBlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current) - || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) { + if (!current.hasTileEntity() + && (terrain.isReplaceable(current) + || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) + && chunk.getTileEntity(cursor) == null) { IBlockState 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 68edcb8a..68a94fe6 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,8 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (int y = surfaceY; y >= 0; y--) { cursor.setPos(x, y, z); IBlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current)) { + if (terrain.isReplaceable(current) && !current.hasTileEntity() + && chunk.getTileEntity(cursor) == null) { IBlockState replacement = pickReplacement( geomeIndex, baseRockValue, formationRegion, x, y, z); if (!changes(current, replacement)) continue; @@ -142,7 +143,6 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.MutableBlockPos curso int layerStart = layerIndex * layerThickness; int layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - IBlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); boolean changed = false; cursor.setPos(x, surfaceY, z); @@ -153,11 +153,13 @@ 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); IBlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current) && changes(current, replacement)) { + IBlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex, y); + if (terrain.isReplaceable(current) && !current.hasTileEntity() + && chunk.getTileEntity(cursor) == null + && changes(current, replacement)) { chunk.setBlockState(cursor, replacement, false); changed = true; } @@ -255,7 +257,7 @@ private net.minecraft.block.state.IBlockState pickReplacement(int geomeIndex, in 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); @@ -265,7 +267,7 @@ private net.minecraft.block.state.IBlockState pickReplacement(int geomeIndex, in return config.pickRock(geomeIndex, family, layerY, rockHash); } - private IBlockState pickStableReplacement(int geomeIndex, long formationRegion, int layerIndex) { + private IBlockState 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; @@ -291,8 +293,9 @@ private IBlockState pickStableReplacement(int geomeIndex, long formationRegion, // 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 e2cb66e2..cd228a09 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.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.113021 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 9199114a..74646eba 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.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.113021 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 72bae63a..50787d94 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); + // One-layer Snow is non-motion-blocking and occupies the first free + // cell immediately above this heightmap's highest occupied 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); IBlockState 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..995c4de5 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java @@ -0,0 +1,91 @@ +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 releaseAndHostedWorkflowContractsRemainTargetNative() 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")); + + String build = new String(Files.readAllBytes(Paths.get("build.gradle")), StandardCharsets.UTF_8); + assertTrue(build.contains("tasks.register('verifyMavenCoordinates')")); + assertTrue(build.contains("generatePomFileForMavenJavaPublication")); + assertTrue(build.contains("dependsOn tasks.named('verifyMavenCoordinates')")); + assertTrue(build.contains("expectedMavenCoordinate")); + + 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..d2c31437 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,65 @@ 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 5b39a506..6b7d9deb 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,37 @@ 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()); + + GeologyEditorSession invalidSession = new GeologyEditorSession(WorldGeologyProfile.recommended(false)); + invalidSession.configureDefaultVanillaStrata(); + invalidSession.addGeome(""); + invalidSession.addGeome("BAD:UPPER"); + invalidSession.addGeome(geomeId); + invalidSession.addGeome(geomeId); + assertFalse(invalidSession.section("geomes").has("")); + assertFalse(invalidSession.section("geomes").has("bad:upper")); + assertTrue(invalidSession.section("geomes").has(geomeId)); + } } 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 d326f9bf..b869f7eb 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.List; import zone.moddev.mc.orespawn.test.Forge25TestBootstrap; import net.minecraft.world.biome.Biome; @@ -39,7 +40,7 @@ void staticInstallerKeepsSurfacesEarlyAndBedrockLast() throws Exception { Biome.GRASS_DIRT_GRAVEL_SURFACE))); 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())); @@ -47,7 +48,11 @@ void staticInstallerKeepsSurfacesEarlyAndBedrockLast() throws Exception { .contains(FluidDepositFeature.configuredFeature())); CompositeFeature surfaces = BiomeSurfaceFeature.configuredFeature(); CompositeFeature 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..5ae746ef --- /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.init.Blocks; + +import zone.moddev.mc.orespawn.test.Forge25TestBootstrap; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.RockEntry; + +class StableLayerHeightEligibilityTest { + static { + Forge25TestBootstrap.registerVanilla(); + } + + @Test + void legalWorldYRemainsEligibleWhenFormationFallsBelowMinimum() { + BakedGeomeConfig config = boundedConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.DIORITE, + geology.getStoneAt(0, geomeScores, -64, 0L, 0, 20, 0), + "a legal Y must not fall back to Stone when waviness shifts its formation below min_y"); + } + + @Test + void illegalWorldYRemainsIneligibleWhenFormationFallsInsideRange() { + BakedGeomeConfig config = boundedConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.STONE, + geology.getStoneAt(0, geomeScores, -184, 0L, 0, 200, 0), + "a shifted formation inside the range must not make an illegal actual Y eligible"); + } + + private static BakedGeomeConfig boundedConfig() { + GeomeDefinition[] geomes = { + new GeomeDefinition("test:floor", 1.0D, new double[] { 0.0D, 0.0D, 0.0D, 1.0D }) + }; + RockEntry[] rocks = { + new RockEntry(Blocks.DIORITE.getDefaultState(), RockFamily.IGNEOUS_VOLCANIC, + 24, 68, 16, 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 b3dc8403..b2927814 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,23 @@ 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.CHEST); + 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())); + assertFalse(terrain.isReplaceable(Blocks.CHEST.getDefaultState())); + assertTrue(terrain.isReplaceable(Blocks.DIRT.getDefaultState())); + } }