diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc1a9f76..6d030b92 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.110021.jar - build/libs/OreSpawn-4.0.8.110021-sources.jar - build/libs/OreSpawn-4.0.8.110021-javadoc.jar + build/libs/OreSpawn-4.0.16.110021.jar + build/libs/OreSpawn-4.0.16.110021-sources.jar + build/libs/OreSpawn-4.0.16.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cce7f409..9c234164 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 b19b787c..238baa2d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,47 @@ +Version 4.0.16.110021 + +* Adopt the shared 4.0.16 release identity. Forge 1.10 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.110021 + +* Convert exposed one-layer Snow at the first free height (`top + 1`) while + retaining full-Snow and Ice conversion at the surface and two blocks below. +* Preserve buried or authored weather materials, unsafe source states and later + structure or vegetation output. +* Existing profiles, chunks, API major 1 and schemas remain unchanged. + +Version 4.0.13.110021 + +* 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 world behaviour + are unchanged. + +Version 4.0.10.110021 + +* Evaluate Stable Layers rock min_y and max_y bounds against actual world Y + instead of the vertically shifted formation coordinate. +* Preserve shifted formation identity for layer, family, and rock choice while + preventing vanilla Stone fallback near the Y 0/255 dimension bounds. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + +Version 4.0.9.110021 + +* Replace provider-declared natural terrain hosts during the existing early + geology scan before structures and vegetation can author matching blocks. +* Keep air, liquids, 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.110021 * Preserve all eight ore/fluid host, tag, and biome editor fields when saved values exceed Minecraft 1.10.2's default 32-character text limit. diff --git a/README.md b/README.md index 8df6715c..c4419edb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.0.8.110021`: the OreSpawn 4.0.8 +This branch builds target-qualified version `4.0.16.110021`: the OreSpawn 4.0.16 feature set for Minecraft 1.10.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. @@ -109,8 +109,12 @@ exported to `config/orespawn-guide/` without overwriting existing files. ## Building -Run Gradle with JDK 17 from the repository root. The build uses a Java 8 -toolchain for compilation and every executable Minecraft/Forge gate: +Run Gradle with exact Temurin `17.0.1+12` from the repository root. Install +exact Temurin `25.0.3+9` for ForgeGradle's Mavenizer and exact Temurin +`8.0.502+7` for Minecraft 1.10.2 production and fixture compilation. Java 17 +remains the Gradle runtime and production bytecode remains Java 8; the build +rejects a different Java 8 toolchain. Hosted CI also proves an online bootstrap +from an empty Gradle home followed by an offline replay from that same cache: ```powershell .\gradlew.bat clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums --no-daemon diff --git a/build.gradle b/build.gradle index d65111ea..3bf50c08 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 @@ -43,6 +44,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.languageVersion = JavaLanguageVersion.of(8) @@ -104,7 +108,7 @@ minecraft { } register('client') register('server') { - args '--nogui' + args 'nogui' } } } @@ -353,6 +357,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 @@ -395,6 +400,26 @@ task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) { || result.getProperty('dynamic_fluid_placements').toInteger() <= 0) { throw new GradleException("Dynamic fluid-deposit probe did not place any blocks: ${marker}") } + [ + 'end.raw_natural_sources': '108', + 'end.structure_natural_sources': '108', + 'end.vegetation_natural_sources': '108', + 'end.cave_pockets': '54', + 'end.underwater_pockets': '54', + 'end.raw_bedrock': '9', + 'end.raw_block_entities': '9', + 'end.exposed_snow_converted': '9', + 'end.surface_ice_converted': '9', + 'end.buried_snow_preserved': '9', + 'end.buried_ice_preserved': '9', + 'nether.unconfigured_snow_preserved': '9', + 'nether.unconfigured_ice_preserved': '9' + ].each { key, expected -> + if (result.getProperty(key) != expected) { + throw new GradleException("Unexpected surface integration result ${key}: " + + "expected ${expected}, found ${result.getProperty(key)}") + } + } File latest = new File(surfaceIntegrationRunDirectory, 'logs/latest.log') if (!latest.isFile() || latest.text.contains('Tried to assign a mutable BlockPos')) { throw new GradleException("Surface integration leaked a mutable position into tick data: ${latest}") @@ -766,6 +791,34 @@ task benchmarkIntegrationProcess(type: JavaExec, dependsOn: prepareWorldgenBench } } +def trackedDocumentationFiles = { + fileTree('docs') { include '**/*' }.files.findAll { it.isFile() } + .sort { project.relativePath(it) } +} +def documentationRelativePath = { File source -> + file('docs').toPath().relativize(source.toPath()).toString().replace('\\', '/') +} + +tasks.register('verifyDocumentationParity') { + group = 'verification' + description = 'Verifies every tracked guide has an identical processed production resource.' + dependsOn tasks.named('processResources') + doLast { + List tracked = trackedDocumentationFiles() + if (tracked.size() != 21) { + throw new GradleException("Expected 21 tracked documentation files, found ${tracked.size()}") + } + tracked.each { File source -> + String relative = documentationRelativePath(source) + File processed = layout.buildDirectory.file( + "resources/main/META-INF/orespawn/docs/${relative}").get().asFile + if (!processed.isFile() || !java.util.Arrays.equals(source.bytes, processed.bytes)) { + throw new GradleException("Processed documentation differs for ${relative}") + } + } + } +} + tasks.named('processResources', ProcessResources) { filteringCharset = 'UTF-8' inputs.property('version', project.version) @@ -784,6 +837,20 @@ tasks.named('processResources', ProcessResources) { filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) } +def prepareEclipseResources = tasks.register('prepareEclipseResources') { + group = 'ide' + description = 'Copies Gradle-processed production resources into Eclipse merged output.' + dependsOn tasks.named('processResources') + doLast { + // Eclipse owns bin/main, so copy into it without declaring the + // directory as a Gradle output or deleting compiled classes. + project.copy { + from(layout.buildDirectory.dir('resources/main')) + into(layout.projectDirectory.dir('bin/main')) + } + } +} + tasks.named('jar', Jar) { archiveClassifier = 'deobf' destinationDirectory = layout.buildDirectory.dir('libs-dev') @@ -797,7 +864,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}" ]) @@ -893,7 +960,8 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' doLast { - if (project.mod_version != '4.0.8.110021') { + if (project.mod_version != '4.0.16.110021' + || project.mod_group != expectedMavenGroup) { throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") } if (project.minecraft_version != '1.10.2' @@ -906,8 +974,8 @@ tasks.register('verifyReleaseConfiguration') { || project.java_version != '8' || project.gradle_java_version != '17') { throw new GradleException('Unexpected dispatcher target metadata') } - if (project.group.toString() != 'zone.moddev.mc' - || base.archivesName.get() != 'OreSpawn' + if (project.group.toString() != expectedMavenGroup + || base.archivesName.get() != expectedMavenArtifact || project.curseforge_project_id != '245586') { throw new GradleException('Unexpected Maven or CurseForge publication identity') } @@ -920,8 +988,8 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.8.110021')) { - throw new GradleException("Authoritative release location does not contain 4.0.8.110021: ${path}") + if (!file(path).getText('UTF-8').contains('4.0.16.110021')) { + throw new GradleException("Authoritative release location does not contain 4.0.16.110021: ${path}") } } if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { @@ -1013,6 +1081,18 @@ tasks.register('verifyReleaseArtifacts') { throw new GradleException("Release jar is missing ${required}") } } + List trackedDocs = trackedDocumentationFiles() + if (trackedDocs.size() != 21) { + throw new GradleException("Expected 21 tracked documentation files, found ${trackedDocs.size()}") + } + trackedDocs.each { File source -> + String relative = documentationRelativePath(source) + def entry = zip.getEntry("META-INF/orespawn/docs/${relative}") + if (entry == null || !java.util.Arrays.equals(source.bytes, + zip.getInputStream(entry).withCloseable { it.bytes })) { + throw new GradleException("Packaged documentation differs for ${relative}") + } + } String metadata = zip.getInputStream(zip.getEntry('mcmod.info')) .getText(StandardCharsets.UTF_8.name()) @@ -1044,6 +1124,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 || manifest.getValue('Timestamp') != null) { throw new GradleException('Release manifest identity/API/FMLAT is incorrect or volatile') @@ -1161,8 +1242,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()) @@ -1208,6 +1289,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' description = 'Prevents Maven publication from targeting a local or incomplete repository.' @@ -1227,6 +1328,7 @@ tasks.register('validateMavenReleaseCredentials') { tasks.withType(PublishToMavenRepository).configureEach { dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn tasks.named('verifyMavenCoordinates') if (preparedReleaseDir.isPresent()) { dependsOn tasks.named('verifyPreparedReleaseArtifacts') } else { @@ -1234,6 +1336,11 @@ tasks.withType(PublishToMavenRepository).configureEach { } } +tasks.named('check') { + dependsOn tasks.named('verifyMavenCoordinates') + dependsOn tasks.named('verifyDocumentationParity') +} + idea { module { downloadSources = true @@ -1287,6 +1394,7 @@ tasks.register('isolateEclipseProductionRuns') { description = 'Marks ordinary ForgeGradle 7 Eclipse launches as production-only.' dependsOn tasks.named('genEclipseRuns') dependsOn tasks.named('configureEclipseBuildship') + dependsOn prepareEclipseResources doLast { obsoleteForgeGradle2EclipseLaunches.each { String name, String mainClass -> @@ -1329,6 +1437,35 @@ tasks.register('verifyEclipseProductionClasspath') { if (!buildshipPreferences.isFile()) { throw new GradleException('Missing Eclipse Buildship project preferences') } + File eclipseResources = file('bin/main') + List trackedDocs = trackedDocumentationFiles() + if (trackedDocs.size() != 21) { + throw new GradleException("Expected 21 tracked documentation files, found ${trackedDocs.size()}") + } + trackedDocs.each { File source -> + String relative = documentationRelativePath(source) + File eclipseCopy = new File(eclipseResources, "META-INF/orespawn/docs/${relative}") + if (!eclipseCopy.isFile() + || !java.util.Arrays.equals(source.bytes, eclipseCopy.bytes)) { + throw new GradleException("Eclipse documentation differs for ${relative}") + } + } + [ + 'mcmod.info', + 'META-INF/orespawn/docs/README.md', + 'META-INF/orespawn/docs/VERSIONS.md' + ].each { relative -> + File required = new File(eclipseResources, relative) + if (!required.isFile()) { + throw new GradleException( + "Eclipse production output is missing processed resource ${relative}") + } + } + String eclipseMetadata = new File(eclipseResources, 'mcmod.info').getText('UTF-8') + if (!eclipseMetadata.contains("\"version\": \"${project.version}\"") + || !eclipseMetadata.contains("\"mcversion\": \"${minecraft_version}\"")) { + throw new GradleException('Eclipse mcmod.info retains unexpanded build placeholders') + } List launchFiles = fileTree(project.projectDir) { include 'runClient.launch' include 'runServer.launch' @@ -1365,6 +1502,10 @@ tasks.register('verifyEclipseProductionClasspath') { if (!contents.contains('PROJECT_ATTR" value="OreSpawn"')) { throw new GradleException("${launch.name} targets the wrong Eclipse project") } + if (launch.name.toLowerCase().contains('server') + && (!contents.contains('nogui') || contents.contains('--nogui'))) { + throw new GradleException("${launch.name} does not use Forge 1.10's bare nogui argument") + } } } } diff --git a/docs/API.md b/docs/API.md index 477e2fc1..5b21d46c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -84,6 +84,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 2ab081cf..796a80ae 100644 --- a/docs/BIOMES.md +++ b/docs/BIOMES.md @@ -137,6 +137,13 @@ 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. +Provider-declared `terrain_dimensions.host_blocks` are resolved by the single +terrain scan at the start of Forge 1.10's early generation coordinator, +immediately before provider surfaces. Matching natural blocks already present +in base terrain are eligible for geology; matching blocks authored later by +structures or vegetation are not. Air, liquids, bedrock, and block-entity +states remain protected even if a provider mistakenly lists their block IDs. + Dimension materials support the ordinary aquifer fluid and replacements for vanilla snow and ice. Minecraft 1.10.2 has one exposed generator-fluid field, so `default_fluid` is fully supported. Later-format `deep_aquifer_fluid` and diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 381269ae..2649879a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -112,8 +112,15 @@ A rock requires `enabled`, `family`, `depth_peak`, `depth_spread`, `min_y`, is omitted. `dimensions` limits membership, and `geomes` multiplies selection weight by province. A weight of zero prevents selection in that context. +`min_y` and `max_y` are inclusive actual-world height limits. Stable Layers +may shift a layer vertically to preserve its formation, family, and lithology +identity, but that shifted coordinate never makes an out-of-range world block +eligible or rejects an otherwise legal world height. + 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 75a3cd36..43f94896 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -98,6 +98,10 @@ public void init(FMLInitializationEvent event) { .quantityRange(4, 11) .pattern(OrePattern.VEIN) .heightDistribution(OreHeightDistribution.TRIANGLE) + .biome(new ResourceLocation("minecraft", "plains")) + .biomeDictionary("FOREST") + .excludeBiome(new ResourceLocation("minecraft", "roofed_forest")) + .excludeBiomeDictionary("SPOOKY") .hostTag(new ResourceLocation("forge", "stone")))) .build(); @@ -112,6 +116,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 7010eebf..09f03e1a 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full version | | --- | --- | ---: | --- | -| 1.10.2 | Forge | `110021` | `4.0.8.110021` | +| 1.10.2 | Forge | `110021` | `4.0.16.110021` | | 1.12.2 | Forge | `112021` | `4.0.6.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | @@ -145,7 +145,15 @@ remain on their target-qualified 4.0.6 versions. If a different branch later receives a separate fix, it uses the next unused Bug number, such as `4.0.8`, even if the `4.0.7` fix was not applicable to it. -A branch may therefore legitimately skip functional version numbers. +Forge 1.10 uses a static, two-dimensional biome system, 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 exposed one-layer Snow is included in configured +weather-material conversion. It also has no Y-sensitive three-dimensional +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 42b4031c..aacbd2ac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ curseforge_project_id=245586 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.8.110021 -mod_group=zone.moddev.mc +mod_version=4.0.16.110021 +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 3f6d69e4..32c4bb63 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -17,6 +17,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + import zone.moddev.mc.orespawn.api.BiomePlacementMode; import zone.moddev.mc.orespawn.api.BiomeRegionSize; import zone.moddev.mc.orespawn.api.BiomeReplacementScope; @@ -33,6 +39,7 @@ import zone.moddev.mc.orespawn.api.WorldgenProvider.BiomeSurfaceDefinition; import zone.moddev.mc.orespawn.api.WorldgenProvider.TerrainDimensionDefinition; import zone.moddev.mc.orespawn.worldgen.SurfaceProbeSpringBridge; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; import net.minecraft.block.Block; import net.minecraft.block.BlockDynamicLiquid; @@ -61,6 +68,7 @@ import net.minecraftforge.fml.common.Mod.EventHandler; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.FMLServerAboutToStartEvent; import net.minecraftforge.fml.common.event.FMLServerStartedEvent; import net.minecraftforge.fml.common.eventhandler.EventPriority; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; @@ -85,10 +93,21 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation SPRING_ROCK = new ResourceLocation(MODID, "rock/spring_host"); private static final ProbeLiquid DEPOSIT_FLUID = new ProbeLiquid(); private static final BlockPos SPRING_POS = new BlockPos(1128, 32, 1128); + private static final IBlockState[] NATURAL_SOURCES = { + Blocks.DIRT.getStateFromMeta(0), Blocks.GRASS.getDefaultState(), + Blocks.DIRT.getStateFromMeta(1), Blocks.DIRT.getStateFromMeta(2), + Blocks.GRAVEL.getDefaultState(), Blocks.SAND.getStateFromMeta(0), + Blocks.SAND.getStateFromMeta(1), Blocks.CLAY.getDefaultState(), + Blocks.HARDENED_CLAY.getDefaultState(), + Blocks.STAINED_HARDENED_CLAY.getStateFromMeta(0), + Blocks.STAINED_HARDENED_CLAY.getStateFromMeta(1), + Blocks.STAINED_HARDENED_CLAY.getStateFromMeta(14) + }; private static final int MIN_CHUNK = 63; private static final int MAX_CHUNK = 65; private static final int COLUMNS = 9 * 16 * 16; private static final int FILLER = COLUMNS * 3; + private static final int NATURAL_SOURCE_COUNT = 9 * NATURAL_SOURCES.length; private static final int GROUND_Y = 200; private static final int MARKER_Y = GROUND_Y - 5; private static final int ROOF_UNDERSIDE_Y = 220; @@ -98,6 +117,9 @@ public final class SurfaceProbeTestMod { 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 Block WEATHER_SNOW_REPLACEMENT = Blocks.WOOL; + private static final Block WEATHER_ICE_REPLACEMENT = Blocks.PACKED_ICE; private static final ResourceLocation[] BUILT_IN_GEOMES = { new ResourceLocation("orespawn", "stable_craton"), new ResourceLocation("orespawn", "mountain_belt"), @@ -167,6 +189,9 @@ public void placeControlledTerrain(DecorateBiomeEvent.Pre event) { Chunk chunk = world.getChunkProvider().provideChunk(chunkX, chunkZ); ProbeGenerator.placeTerrain(chunk, chunkX << 4, chunkZ << 4, world.provider.getDimension() == -1); + if (world.provider.getDimension() == 1) { + placeRawNaturalSources(world, chunk, chunkX << 4, chunkZ << 4); + } } @SubscribeEvent(priority = EventPriority.LOWEST) @@ -195,6 +220,9 @@ public void init(FMLInitializationEvent event) { // opposite sides of the 1,024-block Tiny-region boundary. addPalette(provider, "end_palette_1", END, false); addPalette(provider, "nether_palette_1", NETHER, true); + provider.dimensionMaterials(new ResourceLocation(MODID, "materials/end"), END, + materials -> materials.snowBlock(id(WEATHER_SNOW_REPLACEMENT)) + .iceBlock(id(WEATHER_ICE_REPLACEMENT))); if (!OreSpawnApi.enqueue(provider.build())) { throw new IllegalStateException("Could not enqueue the surfaceprobe provider"); } @@ -220,8 +248,43 @@ private static void addGeology(WorldgenProvider.Builder provider) { }); provider.biome(BIOME_A, Collections.singletonMap(PROBE_GEOME, 100.0D)); provider.biome(BIOME_B, Collections.singletonMap(PROBE_GEOME, 100.0D)); - provider.terrainDimension(TerrainDimensionDefinition.builder(END) - .biomeNamespace(MODID).hostBlock(id(Blocks.END_STONE)).build()); + TerrainDimensionDefinition.Builder terrain = TerrainDimensionDefinition.builder(END) + .biomeNamespace(MODID).hostBlock(id(Blocks.END_STONE)); + for (Block block : Arrays.asList(Blocks.DIRT, Blocks.GRASS, Blocks.GRAVEL, + Blocks.SAND, Blocks.CLAY, Blocks.HARDENED_CLAY, + Blocks.STAINED_HARDENED_CLAY)) { + terrain.hostBlock(id(block)); + } + provider.terrainDimension(terrain.build()); + } + + @EventHandler + public void serverAboutToStart(FMLServerAboutToStartEvent event) { + Path profile = worldRoot(event.getServer()).resolve("serverconfig") + .resolve("orespawn-worldgen.json"); + JsonObject root; + try (BufferedReader reader = Files.newBufferedReader(profile)) { + root = new JsonParser().parse(reader).getAsJsonObject(); + } catch (IOException | RuntimeException exception) { + throw new IllegalStateException("Could not read the test-owned End geology profile", exception); + } + try { + root.addProperty("place_fluid_deposits", true); + JsonObject terrain = root.getAsJsonObject("terrain_dimensions"); + JsonObject end = terrain.getAsJsonObject(END.toString()); + JsonArray hosts = end.getAsJsonArray("host_blocks"); + for (Block block : Arrays.asList(Blocks.AIR, Blocks.WATER, Blocks.BEDROCK, Blocks.CHEST)) { + hosts.add(new JsonPrimitive(id(block).toString())); + } + try (BufferedWriter writer = Files.newBufferedWriter(profile)) { + new GsonBuilder().setPrettyPrinting().create().toJson(root, writer); + } + } catch (IOException | RuntimeException exception) { + throw new IllegalStateException("Could not write the test-owned End geology profile", exception); + } + if (!WorldGeologyProfileManager.reloadActiveProfile()) { + throw new IllegalStateException("Could not reload the test-owned End geology profile"); + } } private static void addPalette(WorldgenProvider.Builder provider, String name, @@ -323,6 +386,10 @@ private static WorldServer requireWorld(MinecraftServer server, int dimension) { private static Audit audit(WorldServer world, boolean roofed) { long dry = 0, wet = 0, filler = 0, geology = 0, ceiling = 0, roof = 0; + long rawNatural = 0, structureNatural = 0, vegetationNatural = 0; + long cavePockets = 0, underwaterPockets = 0, rawBedrock = 0, rawBlockEntities = 0; + long exposedSnow = 0, surfaceIce = 0, buriedSnow = 0, buriedIce = 0; + long unconfiguredSnow = 0, unconfiguredIce = 0; int biomeA = 0, biomeB = 0, edges = 0, sentinels = 0; BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); loadPopulationBorder(world); @@ -334,6 +401,8 @@ private static Audit audit(WorldServer world, boolean roofed) { + chunkX + "," + chunkZ); } int minX = chunkX << 4, minZ = chunkZ << 4; + zone.moddev.mc.orespawn.worldgen.WorldMaterialWeather.onChunkLoad( + new net.minecraftforge.event.world.ChunkEvent.Load(chunk)); for (int localZ = 0; localZ < 16; localZ++) { for (int localX = 0; localX < 16; localX++) { int x = minX + localX, z = minZ + localZ; @@ -375,17 +444,110 @@ private static Audit audit(WorldServer world, boolean roofed) { } } sentinels += auditSentinels(world, minX, minZ); + if (!roofed) { + NaturalSourceAudit natural = auditNaturalSources(world, chunk, cursor, minX, minZ); + rawNatural += natural.rawConverted; + structureNatural += natural.structurePreserved; + vegetationNatural += natural.vegetationPreserved; + cavePockets += natural.cavePreserved; + underwaterPockets += natural.underwaterPreserved; + rawBedrock += natural.bedrockPreserved; + rawBlockEntities += natural.blockEntityPreserved; + } + WeatherMaterialAudit weather = auditWeatherMaterials(chunk, cursor, minX, minZ, roofed); + exposedSnow += weather.exposedSnowConverted; + surfaceIce += weather.surfaceIceConverted; + buriedSnow += weather.buriedSnowPreserved; + buriedIce += weather.buriedIcePreserved; + unconfiguredSnow += weather.unconfiguredSnowPreserved; + unconfiguredIce += weather.unconfiguredIcePreserved; } } if (dry != COLUMNS - 9 || wet != 9 || filler != FILLER || biomeA == 0 || biomeB == 0 || edges == 0 || sentinels != 36 || geology != (roofed ? 0 : FILLER) - || (roofed && (ceiling != COLUMNS || roof != COLUMNS))) { + || (roofed && (ceiling != COLUMNS || roof != COLUMNS + || unconfiguredSnow != 9 || unconfiguredIce != 9 + || exposedSnow != 0 || surfaceIce != 0 || buriedSnow != 0 || buriedIce != 0)) + || (!roofed && (rawNatural != NATURAL_SOURCE_COUNT + || structureNatural != NATURAL_SOURCE_COUNT + || vegetationNatural != NATURAL_SOURCE_COUNT + || cavePockets != NATURAL_SOURCE_COUNT / 2 + || underwaterPockets != NATURAL_SOURCE_COUNT / 2 + || rawBedrock != 9 || rawBlockEntities != 9 + || exposedSnow != 9 || surfaceIce != 9 + || buriedSnow != 9 || buriedIce != 9 + || unconfiguredSnow != 0 || unconfiguredIce != 0))) { throw new IllegalStateException("Incomplete surface audit: dry=" + dry + ", wet=" + wet + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edges + ", sentinels=" + sentinels + ", geology=" + geology - + ", ceiling=" + ceiling + ", roof=" + roof); + + ", ceiling=" + ceiling + ", roof=" + roof + + ", rawNatural=" + rawNatural + + ", structureNatural=" + structureNatural + + ", vegetationNatural=" + vegetationNatural + + ", cavePockets=" + cavePockets + + ", underwaterPockets=" + underwaterPockets + + ", rawBedrock=" + rawBedrock + + ", rawBlockEntities=" + rawBlockEntities + + ", exposedSnow=" + exposedSnow + ", surfaceIce=" + surfaceIce + + ", buriedSnow=" + buriedSnow + ", buriedIce=" + buriedIce + + ", unconfiguredSnow=" + unconfiguredSnow + + ", unconfiguredIce=" + unconfiguredIce); + } + return new Audit(dry, wet, filler, geology, ceiling, roof, biomeA, biomeB, edges, sentinels, + rawNatural, structureNatural, vegetationNatural, cavePockets, + underwaterPockets, rawBedrock, rawBlockEntities, + exposedSnow, surfaceIce, buriedSnow, buriedIce, unconfiguredSnow, unconfiguredIce); + } + + private static WeatherMaterialAudit auditWeatherMaterials(Chunk chunk, + BlockPos.MutableBlockPos cursor, int minX, int minZ, boolean roofed) { + if (roofed) { + long snow = chunk.getBlockState(cursor.setPos(minX + 2, GROUND_Y + 11, minZ + 2)) + .getBlock() == Blocks.SNOW_LAYER ? 1 : 0; + long ice = chunk.getBlockState(cursor.setPos(minX + 3, GROUND_Y + 11, minZ + 2)) + .getBlock() == Blocks.ICE ? 1 : 0; + return new WeatherMaterialAudit(0, 0, 0, 0, snow, ice); + } + long snow = chunk.getBlockState(cursor.setPos(minX + 2, GROUND_Y + 1, minZ + 2)) + .getBlock() == WEATHER_SNOW_REPLACEMENT ? 1 : 0; + long ice = chunk.getBlockState(cursor.setPos(minX + 3, GROUND_Y + 1, minZ + 2)) + .getBlock() == WEATHER_ICE_REPLACEMENT ? 1 : 0; + long buriedSnow = chunk.getBlockState(cursor.setPos(minX + 2, GROUND_Y - 24, minZ + 3)) + .getBlock() == Blocks.SNOW ? 1 : 0; + long buriedIce = chunk.getBlockState(cursor.setPos(minX + 3, GROUND_Y - 24, minZ + 3)) + .getBlock() == Blocks.ICE ? 1 : 0; + return new WeatherMaterialAudit(snow, ice, buriedSnow, buriedIce, 0, 0); + } + + private static NaturalSourceAudit auditNaturalSources(WorldServer world, Chunk chunk, + BlockPos.MutableBlockPos cursor, int minX, int minZ) { + long raw = 0, structure = 0, vegetation = 0, cave = 0, underwater = 0; + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index), z = naturalZ(minZ, index); + if (chunk.getBlockState(cursor.setPos(x, GROUND_Y - 12, z)).getBlock() == Blocks.PRISMARINE) raw++; + IBlockState pocket = chunk.getBlockState(cursor.setPos(x, GROUND_Y - 11, z)); + if (index < NATURAL_SOURCES.length / 2) { + if (pocket.getBlock() == Blocks.AIR) cave++; + } else if (pocket.getBlock() == Blocks.WATER) { + underwater++; + } + if (NATURAL_SOURCES[index].equals(chunk.getBlockState( + cursor.setPos(x, GROUND_Y - 16, z)))) structure++; + if (NATURAL_SOURCES[index].equals(chunk.getBlockState( + cursor.setPos(x, GROUND_Y - 20, z)))) vegetation++; + } + long bedrock = chunk.getBlockState(cursor.setPos(minX + 11, GROUND_Y - 24, minZ + 12)) + .getBlock() == Blocks.BEDROCK ? 1 : 0; + BlockPos chestPos = new BlockPos(minX + 12, GROUND_Y - 24, minZ + 12); + long blockEntity = 0; + if (chunk.getBlockState(chestPos).getBlock() == Blocks.CHEST + && world.getTileEntity(chestPos) instanceof TileEntityChest) { + ItemStack stack = ((TileEntityChest) world.getTileEntity(chestPos)).getStackInSlot(0); + if (stack.getItem() == Items.EMERALD && RAW_CHEST_ITEM_NAME.equals(stack.getDisplayName())) { + blockEntity = 1; + } } - return new Audit(dry, wet, filler, geology, ceiling, roof, biomeA, biomeB, edges, sentinels); + return new NaturalSourceAudit(raw, structure, vegetation, cave, underwater, bedrock, blockEntity); } private static void loadPopulationBorder(WorldServer world) { @@ -465,6 +627,61 @@ private static int markedGround(Chunk chunk, BlockPos.MutableBlockPos cursor, in throw new IllegalStateException("Surface marker missing at " + x + "," + z); } + private static void placeRawNaturalSources(World world, Chunk chunk, int minX, int minZ) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index), z = naturalZ(minZ, index); + chunk.setBlockState(pos.setPos(x, GROUND_Y - 12, z), NATURAL_SOURCES[index]); + chunk.setBlockState(pos.setPos(x, GROUND_Y - 11, z), + index < NATURAL_SOURCES.length / 2 + ? Blocks.AIR.getDefaultState() : Blocks.WATER.getDefaultState()); + } + chunk.setBlockState(pos.setPos(minX + 11, GROUND_Y - 24, minZ + 12), + Blocks.BEDROCK.getDefaultState()); + BlockPos chestPos = new BlockPos(minX + 12, GROUND_Y - 24, minZ + 12); + world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); + if (world.getTileEntity(chestPos) instanceof TileEntityChest) { + ItemStack stack = new ItemStack(Items.EMERALD); + stack.setStackDisplayName(RAW_CHEST_ITEM_NAME); + ((TileEntityChest) world.getTileEntity(chestPos)).setInventorySlotContents(0, stack); + } + chunk.setChunkModified(); + } + + private static void placeAuthoredNaturalSources(World world, int minX, int minZ, int depth) { + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + world.setBlockState(new BlockPos(naturalX(minX, index), GROUND_Y - depth, + naturalZ(minZ, index)), NATURAL_SOURCES[index], 2); + } + } + + private static void placeWeatherMaterialSentinels(World world, int minX, int minZ) { + if (world.provider.getDimension() == -1) { + world.setBlockState(new BlockPos(minX + 2, GROUND_Y + 11, minZ + 2), + Blocks.SNOW_LAYER.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 3, GROUND_Y + 11, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + return; + } + if (world.provider.getDimension() != 1) return; + world.setBlockState(new BlockPos(minX + 2, GROUND_Y + 1, minZ + 2), + Blocks.SNOW_LAYER.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 3, GROUND_Y + 1, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 2, GROUND_Y - 24, minZ + 3), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 3, GROUND_Y - 24, minZ + 3), + Blocks.ICE.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 void assertBlock(Chunk chunk, BlockPos.MutableBlockPos cursor, int x, int y, int z, Block expected, String purpose) { Block actual = chunk.getBlockState(cursor.setPos(x, y, z)).getBlock(); @@ -560,6 +777,11 @@ private static void placeSentinels(World world, int minX, int minZ) { ItemStack stack = new ItemStack(Items.DIAMOND); stack.setStackDisplayName(CHEST_ITEM_NAME); ((TileEntityChest) world.getTileEntity(chestPos)).setInventorySlotContents(0, stack); + if (world.provider.getDimension() == 1) { + placeAuthoredNaturalSources(world, minX, minZ, 16); + placeAuthoredNaturalSources(world, minX, minZ, 20); + } + placeWeatherMaterialSentinels(world, minX, minZ); } } @@ -602,11 +824,26 @@ int placements() { private static final class Audit { final long dry, wet, filler, geology, ceiling, roof; final int biomeA, biomeB, edges, sentinels; + final long rawNatural, structureNatural, vegetationNatural; + final long cavePockets, underwaterPockets, rawBedrock, rawBlockEntities; + final long exposedSnow, surfaceIce, buriedSnow, buriedIce; + final long unconfiguredSnow, unconfiguredIce; Audit(long dry, long wet, long filler, long geology, long ceiling, long roof, - int biomeA, int biomeB, int edges, int sentinels) { + int biomeA, int biomeB, int edges, int sentinels, + long rawNatural, long structureNatural, long vegetationNatural, + long cavePockets, long underwaterPockets, long rawBedrock, long rawBlockEntities, + long exposedSnow, long surfaceIce, long buriedSnow, long buriedIce, + long unconfiguredSnow, long unconfiguredIce) { this.dry = dry; this.wet = wet; this.filler = filler; this.geology = geology; this.ceiling = ceiling; this.roof = roof; this.biomeA = biomeA; this.biomeB = biomeB; this.edges = edges; this.sentinels = sentinels; + this.rawNatural = rawNatural; this.structureNatural = structureNatural; + this.vegetationNatural = vegetationNatural; this.cavePockets = cavePockets; + this.underwaterPockets = underwaterPockets; this.rawBedrock = rawBedrock; + this.rawBlockEntities = rawBlockEntities; + this.exposedSnow = exposedSnow; this.surfaceIce = surfaceIce; + this.buriedSnow = buriedSnow; this.buriedIce = buriedIce; + this.unconfiguredSnow = unconfiguredSnow; this.unconfiguredIce = unconfiguredIce; } void put(Properties properties, String prefix) { properties.setProperty(prefix + ".dry", Long.toString(dry)); @@ -619,10 +856,55 @@ void put(Properties properties, String prefix) { properties.setProperty(prefix + ".biome_b", Integer.toString(biomeB)); properties.setProperty(prefix + ".edges", Integer.toString(edges)); properties.setProperty(prefix + ".sentinels", Integer.toString(sentinels)); + properties.setProperty(prefix + ".raw_natural_sources", Long.toString(rawNatural)); + properties.setProperty(prefix + ".structure_natural_sources", Long.toString(structureNatural)); + properties.setProperty(prefix + ".vegetation_natural_sources", Long.toString(vegetationNatural)); + properties.setProperty(prefix + ".cave_pockets", Long.toString(cavePockets)); + properties.setProperty(prefix + ".underwater_pockets", Long.toString(underwaterPockets)); + properties.setProperty(prefix + ".raw_bedrock", Long.toString(rawBedrock)); + properties.setProperty(prefix + ".raw_block_entities", Long.toString(rawBlockEntities)); + properties.setProperty(prefix + ".exposed_snow_converted", Long.toString(exposedSnow)); + properties.setProperty(prefix + ".surface_ice_converted", Long.toString(surfaceIce)); + properties.setProperty(prefix + ".buried_snow_preserved", Long.toString(buriedSnow)); + properties.setProperty(prefix + ".buried_ice_preserved", Long.toString(buriedIce)); + properties.setProperty(prefix + ".unconfigured_snow_preserved", Long.toString(unconfiguredSnow)); + properties.setProperty(prefix + ".unconfigured_ice_preserved", Long.toString(unconfiguredIce)); } @Override public String toString() { return "Audit{dry=" + dry + ", wet=" + wet + ", filler=" + filler + ", geology=" + geology + ", ceiling=" + ceiling + ", sentinels=" + sentinels + "}"; } } + + private static final class WeatherMaterialAudit { + final long exposedSnowConverted, surfaceIceConverted; + final long buriedSnowPreserved, buriedIcePreserved; + final long unconfiguredSnowPreserved, 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; + } + } + + private static final class NaturalSourceAudit { + final long rawConverted, structurePreserved, vegetationPreserved; + final long cavePreserved, underwaterPreserved, bedrockPreserved, 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; + } + } } diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index 0828af5a..4e1093fd 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -985,7 +985,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.8.110021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/cyano/orespawn/OreSpawn.java b/src/main/java/cyano/orespawn/OreSpawn.java index 5a943010..b150c79f 100644 --- a/src/main/java/cyano/orespawn/OreSpawn.java +++ b/src/main/java/cyano/orespawn/OreSpawn.java @@ -19,7 +19,7 @@ public class OreSpawn { public static OreSpawn INSTANCE; public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.0.8.110021"; + public static final String VERSION = "4.0.16.110021"; public static final List oreSpawnConfigFiles = new ArrayList<>(); public static final List additionalStoneBlocks = new ArrayList<>(); public static boolean disableVanillaOreGen; diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index e56e3e1e..761ada97 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.0.8.110021"; + public static final String VERSION = "4.0.16.110021"; private static final Logger LOGGER = LogManager.getLogger(); 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 667ebed9..736885ca 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java @@ -531,6 +531,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; @@ -554,6 +558,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); } @@ -580,6 +589,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; } @@ -615,6 +628,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; } @@ -638,6 +655,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<>(); @@ -668,6 +689,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 edbb84c3..3e035bad 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -629,8 +629,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(); @@ -690,7 +690,7 @@ List validate() { } for (Entry entry : terrainActive ? geomes.entrySet() : Collections.>emptySet()) { - if (!entry.getKey().matches("[a-z0-9_.-]+") || !entry.getValue().isJsonObject()) { + if (!validGeomeId(entry.getKey()) || !entry.getValue().isJsonObject()) { errors.add("Invalid geome: " + entry.getKey()); continue; } @@ -1180,6 +1180,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/documentation/DocumentationExporter.java b/src/main/java/zone/moddev/mc/orespawn/documentation/DocumentationExporter.java index a92c6b60..2b2c7269 100644 --- a/src/main/java/zone/moddev/mc/orespawn/documentation/DocumentationExporter.java +++ b/src/main/java/zone/moddev/mc/orespawn/documentation/DocumentationExporter.java @@ -18,6 +18,7 @@ public final class DocumentationExporter { private static final String RESOURCE_ROOT = "/META-INF/orespawn/docs/"; private static final String[] FILES = { "README.md", + "VERSIONS.md", "PLAYER_GUIDE.md", "DEVELOPER_GUIDE.md", "CONFIGURATION.md", 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 c54debbe..b67a535b 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 328f8db0..9ee251f6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java @@ -6,7 +6,9 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.block.Block; +import net.minecraft.block.material.Material; 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 +38,11 @@ boolean hasBiomeFilter() { } boolean isReplaceable(IBlockState state) { + if (state.getMaterial() == Material.AIR || state.getMaterial().isLiquid() + || state.getBlock() == Blocks.BEDROCK + || state.getBlock().hasTileEntity(state)) { + return false; + } if (smallHostSet != null) { Block block = state.getBlock(); for (Block host : smallHostSet) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeature.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeature.java index 044a4931..76a08df6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeature.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeature.java @@ -134,7 +134,9 @@ private static boolean replaceable(World world, BlockPos pos, IBlockState state) || block == Blocks.SAND || block == Blocks.GRAVEL || block == Blocks.NETHERRACK || block == Blocks.END_STONE) return true; BakedTerrainDimension terrain = GeomeConfig.terrainDimension(WorldIds.dimension(world)); - return terrain != null && terrain.isReplaceable(state); + if (terrain != null && terrain.isReplaceable(state)) return true; + BakedGeomeConfig geology = GeomeConfig.baked(WorldIds.dimension(world)); + return geology != null && geology.familyOf(state) != null; } private static long pack(int high, int low) { 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 a27feba6..18ed01e8 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java @@ -110,8 +110,10 @@ public void replaceStoneInChunk(World world, Chunk chunk, BakedTerrainDimension 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.getBlock().hasTileEntity(current) + && (terrain.isReplaceable(current) + || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) + && chunk.getTileEntity(cursor, Chunk.EnumCreateEntityType.CHECK) == null) { IBlockState replacement = pickReplacement(baseRockVal, geomeBase, y); if (!GeomeGeology.changes(current, replacement)) continue; chunk.setBlockState(cursor, replacement); 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 52af2967..87cdbafe 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -114,7 +114,8 @@ public void replaceStoneInChunk(World world, Chunk chunk, BakedTerrainDimension 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) + && chunk.getTileEntity(cursor, Chunk.EnumCreateEntityType.CHECK) == null) { IBlockState replacement = pickReplacement( geomeIndex, baseRockValue, formationRegion, x, y, z); if (!changes(current, replacement)) continue; @@ -139,7 +140,6 @@ private boolean replaceStableColumn(Chunk chunk, BlockPos.MutableBlockPos cursor 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); @@ -150,11 +150,13 @@ private boolean replaceStableColumn(Chunk chunk, BlockPos.MutableBlockPos cursor 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) + && chunk.getTileEntity(cursor, Chunk.EnumCreateEntityType.CHECK) == null + && changes(current, replacement)) { chunk.setBlockState(cursor, replacement); changed = true; } @@ -252,7 +254,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); @@ -262,7 +264,8 @@ 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; @@ -288,8 +291,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/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index ff6cc696..413115bc 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -125,7 +125,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.8.110021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + mineralogyVersion diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index 4a8add81..b36f5c09 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.0.8.110021 Upgrade Report" + newline + String text = "OreSpawn 4.0.16.110021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline 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 569d568b..0ac3c04a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java @@ -55,6 +55,12 @@ private static void convertChunk(Chunk chunk, DimensionMaterials materials) { for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int top = chunk.getHeightValue(localX, localZ) - 1; + if (materials.snow != null && top + 1 < 256) { + cursor.setPos(minX + localX, top + 1, minZ + localZ); + if (chunk.getBlockState(cursor).getBlock() == Blocks.SNOW_LAYER) { + chunk.setBlockState(cursor, materials.snow); + } + } 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..3d745cc3 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java @@ -0,0 +1,88 @@ +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.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")); + assertFalse(build.contains("Mavenizer compatibility")); + assertTrue(build.contains("args 'nogui'")); + assertFalse(build.contains("args '--nogui'")); + + 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")); + } + + 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 cba31bdd..c4bc6d13 100644 --- a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java @@ -8,11 +8,34 @@ import java.util.Collections; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import org.junit.jupiter.api.Test; import net.minecraft.util.ResourceLocation; class WorldgenProviderTest { + @Test + void terrainHostContractRetainsLegacyNaturalSourceOrder() { + ResourceLocation dimension = id("surfaceprobe:the_end"); + WorldgenProvider provider = WorldgenProvider.builder("surfaceprobe", 1) + .terrainDimension(dimension, terrain -> terrain + .hostBlock(id("minecraft:dirt")) + .hostBlock(id("minecraft:grass")) + .hostBlock(id("minecraft:gravel")) + .hostBlock(id("minecraft:sand")) + .hostBlock(id("minecraft:clay")) + .hostBlock(id("minecraft:hardened_clay")) + .hostBlock(id("minecraft:stained_hardened_clay"))) + .build(); + + assertEquals("[\"minecraft:dirt\",\"minecraft:grass\",\"minecraft:gravel\"," + + "\"minecraft:sand\",\"minecraft:clay\",\"minecraft:hardened_clay\"," + + "\"minecraft:stained_hardened_clay\"]", + provider.toJson().getAsJsonObject("terrain_dimensions") + .getAsJsonObject(dimension.toString()) + .getAsJsonArray("host_blocks").toString()); + } + @Test void serializesTypedSchemaFourProvider() { ResourceLocation overworld = id("minecraft:overworld"); @@ -242,6 +265,67 @@ void serializesRangedQuantityAndBroadDimensionSelector() { assertFalse(rule.has("quantity")); } + @Test + void oreBiomeFiltersMatchFluidBuilderForDimensionsAndSelectors() { + ResourceLocation overworld = id("minecraft:overworld"); + ResourceLocation plains = id("minecraft:plains"); + ResourceLocation roofedForest = id("minecraft:roofed_forest"); + WorldgenProvider.OreDimensionDefinition explicit = WorldgenProvider.OreDimensionDefinition + .builder(overworld) + .enabled(false) + .hostTag(id("forge:stone")) + .biome(plains) + .biomeDictionary("FOREST") + .excludeBiome(roofedForest) + .excludeBiomeDictionary("SPOOKY") + .build(); + WorldgenProvider.OreDimensionDefinition selector = WorldgenProvider.OreDimensionDefinition + .builder(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id()) + .hostTag(id("forge:stone")) + .biome(plains) + .biomeDictionary("FOREST") + .excludeBiome(roofedForest) + .excludeBiomeDictionary("SPOOKY") + .build(); + + assertEquals(Collections.singleton(plains), explicit.biomeIds()); + assertEquals(Collections.singleton(roofedForest), 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:roofed_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(new JsonPrimitive("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 b0327a30..89579826 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java @@ -146,4 +146,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/documentation/DocumentationExporterTest.java b/src/test/java/zone/moddev/mc/orespawn/documentation/DocumentationExporterTest.java index f477f277..237cd6ca 100644 --- a/src/test/java/zone/moddev/mc/orespawn/documentation/DocumentationExporterTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/documentation/DocumentationExporterTest.java @@ -6,6 +6,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -17,7 +21,22 @@ class DocumentationExporterTest { @Test void exportsCompleteGuideAndDoesNotOverwriteExistingFiles() throws Exception { int firstExport = DocumentationExporter.exportMissing(temporaryDirectory); - assertTrue(firstExport >= 19); + List tracked; + try (Stream paths = Files.walk(Paths.get("docs"))) { + tracked = paths.filter(Files::isRegularFile) + .sorted() + .collect(Collectors.toList()); + } + assertEquals(21, tracked.size()); + assertEquals(tracked.size(), firstExport); + for (Path source : tracked) { + Path relative = Paths.get("docs").relativize(source); + Path exported = temporaryDirectory.resolve(relative.toString()); + assertTrue(Files.isRegularFile(exported), "missing runtime export " + relative); + assertEquals(new String(Files.readAllBytes(source), StandardCharsets.UTF_8), + new String(Files.readAllBytes(exported), StandardCharsets.UTF_8), + "runtime export differs for " + relative); + } assertTrue(Files.isRegularFile(temporaryDirectory.resolve("README.md"))); assertTrue(Files.isRegularFile(temporaryDirectory.resolve("DEVELOPER_GUIDE.md"))); assertTrue(Files.isRegularFile(temporaryDirectory.resolve("BIOMES.md"))); 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..d28bdba2 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java @@ -0,0 +1,56 @@ +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.Forge12TestBootstrap; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.RockEntry; + +class StableLayerHeightEligibilityTest { + static { + Forge12TestBootstrap.registerVanilla(); + } + + @Test + void legalWorldYRemainsEligibleWhenFormationFallsBelowMinimum() { + BakedGeomeConfig config = boundedConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.PRISMARINE, + 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.PRISMARINE.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 e8ea50c6..aa02ccf5 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; @@ -38,4 +41,23 @@ void nonOverworldBiomesAreNeverChanged() { assertFalse(TerrainFeaturePolicy.shouldRemoveVanillaMatchingStoneFeatures( 1, 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())); + } }