diff --git a/.gitattributes b/.gitattributes index 83eed3b..c6ea9f8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,10 @@ *.jar binary *.png binary *.zip binary + +# Keep the native generated defaults byte-for-byte for log-control checks. +src/bootstrapTest/defaultconfigs/config/forge-client.toml whitespace=-blank-at-eof +src/bootstrapTest/defaultconfigs/defaultconfigs/forge-server.toml whitespace=-blank-at-eof + +# Keep the checksum-verified Mavenizer fixture byte-for-byte. +ci-fixtures/tools/** -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0738b69..1759183 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,24 @@ -name: Sky's Terrain Smoother 1.12 CI +name: Sky's Terrain Smoother CI on: push: branches: - - master-1.12.2 + - 'master-*' - 'feature/**' pull_request: branches: - - master-1.12.2 + - 'master-*' permissions: contents: read concurrency: - group: skysterrainsmoother-1.12-${{ github.workflow }}-${{ github.ref }} + group: skysterrainsmoother-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - build: - name: Build, test, and audit + cold-neoforge-bootstrap: + name: Cold NeoForge bootstrap runs-on: ubuntu-latest timeout-minutes: 60 @@ -26,27 +26,116 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Install pinned Java 21 Gradle runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: '21.0.7+6.0.LTS' + + - name: Read pinned dependency sources + id: content + run: | + set -euo pipefail + grass="$(sed -n 's/^grass.commit=//p' gradle/content-dependencies.properties)" + [[ "$grass" =~ ^[0-9a-f]{40}$ ]] + echo "grass=$grass" >> "$GITHUB_OUTPUT" + pieces="$(sed -n 's/^pieces.commit=//p' gradle/content-dependencies.properties)" + [[ "$pieces" =~ ^[0-9a-f]{40}$ ]] + echo "pieces=$pieces" >> "$GITHUB_OUTPUT" + - name: Check out SkysGrassSlabs + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + repository: SkyBlade1978/SkysGrassSlabs + ref: ${{ steps.content.outputs.grass }} + path: dependencies/grass + - name: Check out SkysBuildingPieces + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + repository: SkyBlade1978/SkysBuildingPieces + ref: ${{ steps.content.outputs.pieces }} + path: dependencies/pieces + - name: Build development dependencies + run: | + set -euo pipefail + chmod +x dependencies/*/gradlew + (cd dependencies/grass && ./gradlew assemble --no-daemon --stacktrace) + grass="$(sed -n 's/^grass.version=//p' gradle/content-dependencies.properties)" + dependency="$GITHUB_WORKSPACE/dependencies/grass" + (cd dependencies/pieces && ./gradlew assemble --no-daemon --stacktrace \ + "-PgrassSlabsMainJar=$dependency/build/libs/SkysGrassSlabs-$grass.jar" \ + "-PgrassSlabsDeobfJar=$dependency/build/libs/SkysGrassSlabs-$grass.jar") + + + - name: Bootstrap NeoForge from an empty cache + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/skysterrainsmoother-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 test assemble verifyReleaseArtifacts writeReleaseChecksums -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + cp build/release/SHA256SUMS "$RUNNER_TEMP/terrain-cold-SHA256SUMS" + + - name: Verify same-cache bootstrap offline + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/skysterrainsmoother-cold-gradle + run: | + set -euo pipefail + gradle_args=( + classes test assemble verifyReleaseArtifacts writeReleaseChecksums -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces + --rerun-tasks --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew clean --offline --no-daemon --no-build-cache + ./gradlew "${gradle_args[@]}" + cmp build/release/SHA256SUMS "$RUNNER_TEMP/terrain-cold-SHA256SUMS" + + build: + name: Build, test, and audit + runs-on: ubuntu-latest + timeout-minutes: 60 - - name: Pin production Java + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Read target build metadata + id: target + shell: bash run: | - echo "SGS_JAVA8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" - echo "SBP_JAVA8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" - echo "STS_JAVA8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + set -euo pipefail + value() { sed -n "s/^$1=//p" gradle.properties; } + toolchain="$(value java_toolchain_version)" + selector="$(value java_setup_version)" + echo "java_setup_version=${selector:-$toolchain}" >> "$GITHUB_OUTPUT" + echo "java_version=$(value java_version)" >> "$GITHUB_OUTPUT" + echo "minecraft_version=$(value minecraft_version)" >> "$GITHUB_OUTPUT" + echo "gradle_java_version=$(value gradle_java_toolchain_version)" >> "$GITHUB_OUTPUT" + echo "artifact_prefix=SkysTerrainSmoother-$(value mod_version)" >> "$GITHUB_OUTPUT" - - name: Install Java 17 for Gradle + - name: Install target Java toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: ${{ steps.target.outputs.java_setup_version }} - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Read pinned dependency sources id: content run: | @@ -73,32 +162,52 @@ jobs: run: | set -euo pipefail chmod +x dependencies/*/gradlew - (cd dependencies/grass && ./gradlew jar --no-daemon --stacktrace) - (cd dependencies/pieces && ./gradlew jar --no-daemon --stacktrace) + (cd dependencies/grass && ./gradlew assemble --no-daemon --stacktrace) + grass="$(sed -n 's/^grass.version=//p' gradle/content-dependencies.properties)" + dependency="$GITHUB_WORKSPACE/dependencies/grass" + (cd dependencies/pieces && ./gradlew assemble --no-daemon --stacktrace \ + "-PgrassSlabsMainJar=$dependency/build/libs/SkysGrassSlabs-$grass.jar" \ + "-PgrassSlabsDeobfJar=$dependency/build/libs/SkysGrassSlabs-$grass.jar") + - name: Make the wrapper executable run: chmod +x ./gradlew - name: Build, test, and audit release artifacts run: >- - ./gradlew clean check integrationTest build javadoc verifyReleaseArtifacts writeReleaseChecksums - verifyEclipseProductionClasspath -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces --no-daemon --stacktrace + ./gradlew clean check build javadoc preparePackagedIntegrationProbe verifyReleaseArtifacts writeReleaseChecksums + verifyEclipseProductionClasspath -Declipse.application=org.eclipse.ui.ide.workbench -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces --no-daemon --stacktrace - name: Replay artifact checks offline run: >- - ./gradlew assemble verifyReleaseArtifacts writeReleaseChecksums - -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces --offline --no-daemon --stacktrace + ./gradlew assemble verifyReleaseArtifacts writeReleaseChecksums -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces --offline --no-daemon --stacktrace + + - name: Check fresh generation and reload in NeoGradle + shell: bash + run: | + set -euo pipefail + version() { sed -n "s/^$1.version=//p" gradle/content-dependencies.properties; } + common=( + --mode server --directory build/ci-generation-run + --probe build/integration-test-mod/skysterrainsmoother-integration-test.jar + --extra-mod "dependencies/grass/build/libs/SkysGrassSlabs-$(version grass).jar" + --extra-mod "dependencies/pieces/build/libs/SkysBuildingPieces-$(version pieces).jar" + --gradle-argument=-PgrassSlabsDirectory=dependencies/grass + --gradle-argument=-PbuildingPiecesDirectory=dependencies/pieces + ) + python3 tools/run_development_checks.py "${common[@]}" --phase fresh --fresh + python3 tools/run_development_checks.py "${common[@]}" --phase reload - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: SkysTerrainSmoother-1.12.2-${{ github.sha }} + name: SkysTerrainSmoother-${{ steps.target.outputs.minecraft_version }}-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/SkysTerrainSmoother-0.1.0.112021.jar - build/libs/SkysTerrainSmoother-0.1.0.112021-sources.jar - build/libs/SkysTerrainSmoother-0.1.0.112021-javadoc.jar + build/libs/${{ steps.target.outputs.artifact_prefix }}.jar + build/libs/${{ steps.target.outputs.artifact_prefix }}-sources.jar + build/libs/${{ steps.target.outputs.artifact_prefix }}-javadoc.jar build/release/SHA256SUMS CHANGELOG.md @@ -106,7 +215,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: SkysTerrainSmoother-1.12.2-diagnostics-${{ github.sha }} + name: SkysTerrainSmoother-${{ steps.target.outputs.minecraft_version }}-diagnostics-${{ github.sha }} if-no-files-found: ignore retention-days: 14 path: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2212bab..b03ae85 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.12.2 + - 'master-*' - 'feature/**' pull_request: branches: - - master-1.12.2 + - 'master-*' schedule: - cron: '47 7 * * 4' @@ -26,27 +26,30 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '8.0.502+7' - - - name: Pin production Java + - name: Read target build metadata + id: target + shell: bash run: | - echo "SGS_JAVA8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" - echo "SBP_JAVA8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" - echo "STS_JAVA8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + set -euo pipefail + value() { sed -n "s/^$1=//p" gradle.properties; } + toolchain="$(value java_toolchain_version)" + selector="$(value java_setup_version)" + echo "java_setup_version=${selector:-$toolchain}" >> "$GITHUB_OUTPUT" + echo "java_version=$(value java_version)" >> "$GITHUB_OUTPUT" + echo "minecraft_version=$(value minecraft_version)" >> "$GITHUB_OUTPUT" + echo "gradle_java_version=$(value gradle_java_toolchain_version)" >> "$GITHUB_OUTPUT" + echo "artifact_prefix=SkysTerrainSmoother-$(value mod_version)" >> "$GITHUB_OUTPUT" - - name: Install Java 17 for Gradle + - name: Install target Java toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: ${{ steps.target.outputs.java_setup_version }} - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Read pinned dependency sources id: content run: | @@ -73,8 +76,13 @@ jobs: run: | set -euo pipefail chmod +x dependencies/*/gradlew - (cd dependencies/grass && ./gradlew jar --no-daemon --stacktrace) - (cd dependencies/pieces && ./gradlew jar --no-daemon --stacktrace) + (cd dependencies/grass && ./gradlew assemble --no-daemon --stacktrace) + grass="$(sed -n 's/^grass.version=//p' gradle/content-dependencies.properties)" + dependency="$GITHUB_WORKSPACE/dependencies/grass" + (cd dependencies/pieces && ./gradlew assemble --no-daemon --stacktrace \ + "-PgrassSlabsMainJar=$dependency/build/libs/SkysGrassSlabs-$grass.jar" \ + "-PgrassSlabsDeobfJar=$dependency/build/libs/SkysGrassSlabs-$grass.jar") + - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 9066817..b8f7ab7 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -322,8 +322,8 @@ jobs: run: | set -euo pipefail chmod +x dependencies/*/gradlew - (cd dependencies/grass && ./gradlew jar --no-daemon --stacktrace) - (cd dependencies/pieces && ./gradlew jar --no-daemon --stacktrace) + (cd dependencies/grass && ./gradlew assemble --no-daemon --stacktrace) + (cd dependencies/pieces && ./gradlew assemble --no-daemon --stacktrace) - name: Build, test, and audit once env: @@ -519,8 +519,8 @@ jobs: run: | set -euo pipefail chmod +x dependencies/*/gradlew - (cd dependencies/grass && ./gradlew jar --no-daemon --stacktrace) - (cd dependencies/pieces && ./gradlew jar --no-daemon --stacktrace) + (cd dependencies/grass && ./gradlew assemble --no-daemon --stacktrace) + (cd dependencies/pieces && ./gradlew assemble --no-daemon --stacktrace) - name: Download immutable release artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index 94f7c7f..82501ed 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -19,7 +19,11 @@ jobs: set -euo pipefail version="$(sed -n 's/^mod_version=//p' gradle.properties)" test "$GITHUB_REF_NAME" = "$version" - [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.112021$ ]] + minecraft="$(sed -n 's/^minecraft_version=//p' gradle.properties)" + loader="$(sed -n 's/^loader_code=//p' gradle.properties)" + IFS=. read -r major minor patch <<<"$minecraft" + printf -v suffix '%s%02d%02d%s' "$major" "$((10#$minor))" "$((10#${patch:-0}))" "$loader" + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.${suffix}$ ]] checks="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?per_page=100" --jq '[.check_runs[] | select(.name == "Build, test, and audit" and .conclusion == "success")] | length')" test "$checks" -gt 0 echo "verifyReleaseArtifacts passed in CI. This tag check publishes nothing." diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index 8959c1a..d71fd0e 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -3,11 +3,11 @@ name: Validate Gradle Wrapper on: push: branches: - - master-1.12.2 + - 'master-*' - 'feature/**' pull_request: branches: - - master-1.12.2 + - 'master-*' permissions: contents: read diff --git a/.gitignore b/.gitignore index 30e9b77..8d22558 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,7 @@ agent_notes/ # Files from Forge MDK forge*changelog.txt /README.txt + +# Python caches can contain local source paths. +__pycache__/ +*.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b3b8e..e457ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## 0.1.0.121012 + +- Updated for Minecraft 1.21.1 on NeoForge. +- Sand slabs keep their colour, orientation and waterlogging when upgrading. + +## 0.1.0.120062 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.20.6 on NeoForge. +- Use the matching Grass Slabs and Building Pieces versions. +- Use Mineralogy's natural rock pieces when its NeoForge build is installed. + +## 0.1.0.120012 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.20.1. +- Use the matching Grass Slabs and Building Pieces versions. + +## 0.1.0.119041 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.19.4. +- Use the matching Grass Slabs and Building Pieces versions. + +## 0.1.0.118021 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.18.2. +- Include the lower caves and taller terrain in newly generated chunks. +- Use the matching Grass Slabs and Building Pieces versions. + +## 0.1.0.117011 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.17.1. +- Use the matching Grass Slabs and Building Pieces versions. + +## 0.1.0.116051 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.16.5. +- Use the matching Grass Slabs and Building Pieces versions. + +## 0.1.0.115021 (unreleased) + +- Bring surface and cave smoothing to Minecraft 1.15.2. +- Use the matching Grass Slabs and Building Pieces releases. + +## 0.1.0.114041 + +- Added Minecraft 1.14.4 support, including cave floors and all three smoothing levels. +- Kept existing sand slabs, colours, orientations and custom item data when upgrading from 1.12.2. +- Moved settings to TOML while preserving choices from the old CFG file. +- Added the native Mods Config screen and kept changes pending until Minecraft restarts. +- Kept Mineralogy's slabs and Building Pieces' steps and corners working together. + ## 0.1.0.112021 - Added ingredient-based recipe unlocks, including slab recombination. diff --git a/README.md b/README.md index 232a3d2..f590e22 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ sandstone, sand and hardened clay. ## Smoothing levels -Set `worldgen.smoothingLevel` in `config/skysterrainsmoother.cfg`: +Set `worldgen.smoothingLevel` in `config/skysterrainsmoother-common.toml`: 1. **Slabs** - half-block transitions. 2. **Slabs and steps** - adds pieces covering half a slab's footprint. @@ -37,9 +37,8 @@ blocks in a row make six slabs, and two slabs side by side make a full block. Like sand blocks, the slabs fall when unsupported. Sand uses slabs at every smoothing level. -Mineralogy is supported without another add-on. When it's installed, the -smoother uses its natural rock slabs and the matching steps and corners -included in Building Pieces. Disabled native slab types are skipped. +Install the NeoForge build of Mineralogy to smooth its natural rock. When +its slabs are enabled, Building Pieces provides the matching steps and corners. Ores, gravel, wood and decorative stone aren't smoothing materials. @@ -48,14 +47,16 @@ For BOP grass and soils, add the ## Installation and settings -For **Minecraft 1.12.2**, use **Forge 14.23.5.2859** and **Java 8**, with: +For **Minecraft 1.21.1**, use **NeoForge 21.1.247** and **Java 21**, with: -- Sky's Grass Slabs **1.1.2.112021**. -- Sky's Building Pieces **0.3.8.112021**. -- Sky's Terrain Smoother **0.1.0.112021**. +- Sky's Grass Slabs **1.2.0.121012**. +- Sky's Building Pieces **0.3.9.121012**. +- Sky's Terrain Smoother **0.1.0.121012**. -Install all three main jars on both the client and server. +Install all three NeoForge jars on both the client and server. If you use +the BOP add-on, keep its matching dependencies installed too. +The old CFG settings are copied once into TOML; the original file is kept. New configurations start at level 3. Existing saved choices are kept. Select the mod in **Mods > Config** to choose a level, turn smoothing off or disable Mineralogy support, then restart Minecraft. diff --git a/build.gradle b/build.gradle index bb90f98..3b4e30b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,118 +1,83 @@ -import groovy.json.JsonSlurper -import java.nio.charset.StandardCharsets import java.nio.file.Files +import org.apache.tools.ant.filters.FixCrLfFilter import java.security.MessageDigest +import java.nio.charset.StandardCharsets import java.util.zip.ZipFile -import org.apache.tools.ant.filters.FixCrLfFilter plugins { - id 'java' id 'eclipse' id 'idea' + id 'java-library' id 'maven-publish' - id 'net.minecraftforge.renamer' version '1.1.5' - id 'net.minecraftforge.gradle' version '7.0.34' + id 'net.neoforged.gradle.userdev' version '7.1.38' } -group = project.mod_group -version = project.mod_version -base.archivesName = 'SkysTerrainSmoother' - -def versionParts = project.mod_version.toString().tokenize('.') -if (versionParts.size() != 4 || !versionParts.every { it ==~ /\d+/ }) { - throw new GradleException("mod_version must use Major.Minor.Bug.Target numeric form: ${project.mod_version}") -} -def minecraftVersionParts = project.minecraft_version.toString().tokenize('.') -def expectedTargetVersion = "${minecraftVersionParts[0]}" + - "${minecraftVersionParts[1].padLeft(2, '0')}" + - "${minecraftVersionParts[2].padLeft(2, '0')}" + project.loader_code -if (versionParts[3] != expectedTargetVersion) { - throw new GradleException("mod_version target ${versionParts[3]} does not match ${expectedTargetVersion}") +def sha256Of = { File inputFile -> + MessageDigest digest = MessageDigest.getInstance('SHA-256') + inputFile.withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + digest.digest().encodeHex().toString().toUpperCase() } -ext.functional_version = versionParts[0..2].join('.') -ext.display_version = project.mod_version -ext.release_tag = project.mod_version -def pinnedJava8 = providers.environmentVariable('STS_JAVA8_HOME') -// Gradle 9.6.1's ordinary language-level selector prefers the newest cached -// Java 8. This opt-in specific installation keeps local/CI qualification exact. -def exactJava8Spec = pinnedJava8.isPresent() ? - org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec.fromJavaHome( - project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava8.get())) : null +version = mod_version +group = mod_group + +base { + archivesName = 'SkysTerrainSmoother' +} java { - toolchain.languageVersion = JavaLanguageVersion.of(8) + toolchain { + languageVersion = JavaLanguageVersion.of(21) + vendor = JvmVendorSpec.ADOPTIUM + } withSourcesJar() withJavadocJar() } tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(21) + vendor = JvmVendorSpec.ADOPTIUM } - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 options.encoding = 'UTF-8' - options.compilerArgs.addAll(['-Xmaxerrs', '1000']) - if (pinnedJava8.isPresent()) { - javaCompiler = javaToolchains.compilerFor(exactJava8Spec) - } } -def grassSlabsDirectory = file(providers.gradleProperty('grassSlabsDirectory').getOrElse('../..' + '/SkysGrassSlabs112/SkysGrassSlabs')) -def buildingPiecesDirectory = file(providers.gradleProperty('buildingPiecesDirectory').getOrElse('../..' + '/SkysBuildingPieces112/SkysBuildingPieces')) -def contentLock=new Properties() -file('gradle/content-dependencies.properties').withInputStream { contentLock.load(it) } - -def localGrassVersion = providers.gradleProperty('grassSlabsVersion') -if (localGrassVersion.isPresent() && - (System.getenv('CI') != null || !providers.gradleProperty('grassSlabsDirectory').isPresent())) { - throw new GradleException('A local grass-version override needs an explicit directory and is not allowed in CI.') +tasks.withType(Javadoc).configureEach { + failOnError = true + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') + options.addBooleanOption('notimestamp', true) } -def grassVersion = localGrassVersion.getOrElse(contentLock.getProperty('grass.version')) - - -def grassSlabsDevelopmentJar = new File(grassSlabsDirectory, - "build/libs-dev/SkysGrassSlabs-${grassVersion}-deobf.jar") -def localPiecesVersion = providers.gradleProperty('buildingPiecesCoreVersion') -if (localPiecesVersion.isPresent() && - (System.getenv('CI') != null || !providers.gradleProperty('buildingPiecesDirectory').isPresent())) { - throw new GradleException('A local core-version override needs an explicit core directory and is not allowed in CI.') +def java21Launcher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + vendor = JvmVendorSpec.ADOPTIUM } -def piecesVersion = localPiecesVersion.getOrElse(contentLock.getProperty('pieces.version')) -def buildingPiecesDevelopmentJar = new File(buildingPiecesDirectory, - "build/libs-dev/SkysBuildingPieces-${piecesVersion}-deobf.jar") -def verifyContentDependencies=tasks.register('verifyContentDependencies') { + +tasks.register('verifyJava21Toolchain') { + group = 'verification' + description = 'Verifies the pinned Temurin Java 21 production toolchain.' doLast { - [grass:grassSlabsDirectory,pieces:buildingPiecesDirectory].each { name,directory -> - Properties metadata=new Properties() - new File(directory,'gradle.properties').withInputStream { metadata.load(it) } - String expectedVersion = name == 'grass' ? grassVersion : name == 'pieces' ? piecesVersion : contentLock.getProperty(name + '.version') - if (metadata.mod_version != expectedVersion) { - String localHelp = name == 'pieces' && System.getenv('CI') == null ? - ' For a local candidate, supply both -PbuildingPiecesDirectory=' + - ' and -PbuildingPiecesCoreVersion=, then regenerate the Eclipse runs.' : '' - throw new GradleException("Content dependency version does not match its lock: ${name}" + - " (expected ${expectedVersion}, found ${metadata.mod_version})." + localHelp) - } - if(System.getenv('CI')!=null) { - Process git=new ProcessBuilder('git','-C',directory.path,'rev-parse','HEAD').start() - String sha=git.inputStream.getText('UTF-8').trim() - if(git.waitFor()!=0||sha!=contentLock.getProperty(name+'.commit')) - throw new GradleException('Content dependency source does not match its lock: '+name) - } + def metadata = java21Launcher.get().metadata + String runtimeVersion = metadata.javaRuntimeVersion + if (project.java_toolchain_version != '21.0.7+6' + || metadata.vendor.toString() != 'Eclipse Temurin' + || !(runtimeVersion == '21.0.7+6' || runtimeVersion == '21.0.7+6-LTS')) { + throw new GradleException("Expected Temurin ${project.java_toolchain_version}, found " + + "${metadata.vendor} ${runtimeVersion} at ${metadata.installationPath}") } } } -tasks.named('compileJava') { - dependsOn verifyContentDependencies - doFirst { - [grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar].each { dependency -> - if (!dependency.isFile()) throw new GradleException("Build the coordinated content dependency first: " + dependency) - } - } +tasks.named('check') { + dependsOn tasks.named('verifyJava21Toolchain') } tasks.named('compileTestJava', JavaCompile) { @@ -122,28 +87,22 @@ tasks.named('compileTestJava', JavaCompile) { tasks.withType(Test).configureEach { useJUnitPlatform() workingDir = project.projectDir - if (pinnedJava8.isPresent()) javaLauncher = javaToolchains.launcherFor(exactJava8Spec) -} -tasks.withType(JavaExec).configureEach { - if (pinnedJava8.isPresent()) javaLauncher = javaToolchains.launcherFor(exactJava8Spec) + // These saved-world audits read files outside the compiled test resources. + inputs.file('tools/native-log-profiles.json') + inputs.dir('src/test/resources/fixtures') + javaLauncher = java21Launcher } - -tasks.withType(Javadoc).configureEach { - if (pinnedJava8.isPresent()) javadocTool = javaToolchains.javadocToolFor(exactJava8Spec) - failOnError = false - options.encoding = 'UTF-8' - options.addStringOption('Xdoclint:none', '-quiet') - options.addBooleanOption('notimestamp', true) -} - tasks.withType(AbstractArchiveTask).configureEach { + includeEmptyDirs = false + filteringCharset = 'UTF-8' preserveFileTimestamps = false reproducibleFileOrder = true + inputs.property('archiveLineEndings', 'utf8-lf-with-toml') } def archiveTextSuffixes = [ '.cfg', '.css', '.html', '.info', '.java', '.js', '.json', '.mcmeta', '.md', - '.properties', '.txt', '.xml' + '.properties', '.toml', '.txt', '.xml' ] def archiveTextPatterns = archiveTextSuffixes.collect { "**/*${it}".toString() } archiveTextPatterns.addAll([ @@ -156,287 +115,185 @@ def normalizeArchiveLineEndings = { details -> eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) } -def developmentRunDirectory = providers.gradleProperty('skysTerrainSmootherRunDirectory').getOrElse('run') -minecraft { - mappings channel: project.mapping_channel, version: project.mapping_version - runs { - configureEach { - workingDir.convention layout.projectDirectory.dir(developmentRunDirectory) - environment 'MC_VERSION', project.minecraft_version.toString() - systemProperty 'forge.logging.console.level', 'debug' - systemProperty 'fml.coreMods.load', 'zone.moddev.mc.skysbuildingpieces.legacy.LoaderPlugin' - } - register('client') - register('server') { args '--nogui' } - } +println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}" +def grassSlabsDirectory = file(providers.gradleProperty('grassSlabsDirectory').getOrElse('../..' + '/SkysGrassSlabs1211NEO/SkysGrassSlabs')) +def buildingPiecesDirectory = file(providers.gradleProperty('buildingPiecesDirectory').getOrElse('../..' + '/SkysBuildingPieces1211NEO/SkysBuildingPieces')) +def contentLock=new Properties() +file('gradle/content-dependencies.properties').withInputStream { contentLock.load(it) } + +def localGrassVersion = providers.gradleProperty('grassSlabsVersion') +if (localGrassVersion.isPresent() && + (System.getenv('CI') != null || !providers.gradleProperty('grassSlabsDirectory').isPresent())) { + throw new GradleException('A local grass-version override needs an explicit directory and is not allowed in CI.') } +def grassVersion = localGrassVersion.getOrElse(contentLock.getProperty('grass.version')) -// Forge 1.12 discovers a development mod from one classes/resources root. -def forge12DevelopmentOutput = layout.buildDirectory.dir('forge12-development/main') -def syncForge10DevelopmentResources = tasks.register('syncForge10DevelopmentResources', Sync) { - group = 'ide' - dependsOn tasks.named('classes') - from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory } - from sourceSets.main.output.resourcesDir - into forge12DevelopmentOutput -} -tasks.configureEach { - if (name in ['runClient', 'runServer', 'runTestClient', 'runTestServer']) { - dependsOn syncForge10DevelopmentResources - doFirst { - if (pinnedJava8.isPresent()) executable = javaToolchains.launcherFor(exactJava8Spec).get().executablePath.asFile.path - Set splitMainOutputs = [ - tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile, - sourceSets.main.output.resourcesDir - ] as Set - classpath = files(forge12DevelopmentOutput, classpath.filter { - !splitMainOutputs.contains(it) - }) + +def grassSlabsDevelopmentJar = new File(grassSlabsDirectory, + "build/libs/SkysGrassSlabs-${grassVersion}.jar") + +def localPiecesVersion = providers.gradleProperty('buildingPiecesCoreVersion') +if (localPiecesVersion.isPresent() && + (System.getenv('CI') != null || !providers.gradleProperty('buildingPiecesDirectory').isPresent())) { + throw new GradleException('A local core-version override needs an explicit core directory and is not allowed in CI.') +} +def piecesVersion = localPiecesVersion.getOrElse(contentLock.getProperty('pieces.version')) +def buildingPiecesDevelopmentJar = new File(buildingPiecesDirectory, + "build/libs/SkysBuildingPieces-${piecesVersion}.jar") +def verifyContentDependencies = tasks.register('verifyContentDependencies') { + doLast { + Map dependencies = [ + grass: [directory: grassSlabsDirectory, version: grassVersion, + override: localGrassVersion.isPresent(), artifact: 'SkysGrassSlabs'], + pieces: [directory: buildingPiecesDirectory, version: piecesVersion, + override: localPiecesVersion.isPresent(), artifact: 'SkysBuildingPieces'] + ] + dependencies.each { name, selected -> + File directory = selected.directory + Properties metadata = new Properties() + new File(directory, 'gradle.properties').withInputStream { metadata.load(it) } + if (metadata.mod_version != selected.version) { + throw new GradleException("Content dependency version does not match its lock: ${name}" + + " (expected ${selected.version}, found ${metadata.mod_version}).") + } + if (!selected.override) { + Process revision = new ProcessBuilder('git', '-C', directory.path, 'rev-parse', 'HEAD').start() + String commit = revision.inputStream.getText('UTF-8').trim() + if (revision.waitFor() != 0 || commit != contentLock.getProperty(name + '.commit')) { + throw new GradleException('Content dependency source does not match its lock: ' + name) + } + Process status = new ProcessBuilder('git', '-C', directory.path, + 'status', '--porcelain', '--untracked-files=no').start() + String changes = status.inputStream.getText('UTF-8').trim() + if (status.waitFor() != 0 || !changes.isEmpty()) { + throw new GradleException('The pinned content checkout has tracked changes: ' + name) + } + [ + [path: "build/libs/${selected.artifact}-${selected.version}.jar", key: '.sha256'], + [path: "build/libs/${selected.artifact}-${selected.version}.jar", key: '.development.sha256'] + ].each { artifact -> + File jar = new File(directory, artifact.path) + if (!jar.isFile()) { + throw new GradleException('Build the pinned content dependency first: ' + jar) + } + String checksum = MessageDigest.getInstance('SHA-256').digest(Files.readAllBytes(jar.toPath())) + .encodeHex().toString().toUpperCase(Locale.ROOT) + if (checksum != contentLock.getProperty(name + artifact.key)) { + throw new GradleException('Content dependency artifact does not match its lock: ' + jar.name) + } + } + } } } } -// Clone ForgeGradle's generated launch at execution time so verification runs -// use the same mappings and Java 8 runtime without exposing probe code to the -// ordinary client or server classpath. -def configureFromForgeRun = { JavaExec process, String runTaskName -> - process.actions.clear() - process.dependsOn { - JavaExec run = tasks.getByName(runTaskName) as JavaExec - run.taskDependencies.getDependencies(run) - } - process.doLast { - JavaExec run = tasks.getByName(runTaskName) as JavaExec - String runConfigurationName = runTaskName.substring(3).uncapitalize() - def runOptions = minecraft.runs.getByName(runConfigurationName) - File originalConfiguredWorkingDir = runOptions.workingDir.get().asFile - File originalWorkingDir = run.workingDir - List originalArgs = new ArrayList<>(run.args) - List originalJvmArgs = new ArrayList<>(run.jvmArgs) - Map originalSystemProperties = new LinkedHashMap<>(run.systemProperties) - Map originalEnvironment = new LinkedHashMap<>(run.environment) - FileCollection originalClasspath = files(run.classpath.files) - String originalMinHeap = run.minHeapSize - String originalMaxHeap = run.maxHeapSize - String originalMainClass = run.mainClass.orNull - try { - runOptions.workingDir.set(process.workingDir) - run.workingDir(process.workingDir) - run.args(process.args) - run.jvmArgs(process.jvmArgs) - run.systemProperties(process.systemProperties) - run.environment(process.environment) - Set splitMainOutputs = [ - tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile, - sourceSets.main.output.resourcesDir - ] as Set - run.setClasspath(files(forge12DevelopmentOutput, run.classpath.filter { - !splitMainOutputs.contains(it) - })) - if (pinnedJava8.isPresent()) run.executable = javaToolchains.launcherFor(exactJava8Spec).get().executablePath.asFile.path - run.exec() - } finally { - runOptions.workingDir.set(originalConfiguredWorkingDir) - run.workingDir(originalWorkingDir) - run.setArgs(originalArgs) - run.setJvmArgs(originalJvmArgs) - run.setSystemProperties(originalSystemProperties) - run.setEnvironment(originalEnvironment) - run.setClasspath(originalClasspath) - run.minHeapSize = originalMinHeap - run.maxHeapSize = originalMaxHeap - if (originalMainClass == null) { - run.mainClass.unset() - } else { - run.mainClass.set(originalMainClass) - } +tasks.named('compileJava') { + dependsOn verifyContentDependencies + doFirst { + [grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar].each { dependency -> + if (!dependency.isFile()) throw new GradleException("Build the coordinated content dependency first: " + dependency) } } } -repositories { - minecraft.mavenizer(it) - maven fg.forgeMaven - maven fg.minecraftLibsMaven - exclusiveContent { - forRepository { - maven { - name = 'Sponge' - url = 'https://repo.spongepowered.org/repository/maven-public' - } +def developmentRunDirectory = providers.gradleProperty('skysTerrainSmootherRunDirectory').getOrElse('run') + +def stageContentJars = { File gameDirectory -> + File mods = new File(gameDirectory, 'mods') + Set expected = [grassSlabsDevelopmentJar.name, buildingPiecesDevelopmentJar.name] as Set + fileTree(mods).matching { include '*.jar' }.each { existing -> + if (existing.name ==~ /Skys(?:GrassSlabs|BuildingPieces)-[0-9].*\.jar/ && !expected.contains(existing.name)) { + throw new GradleException('An older content jar is already in the development profile: ' + existing) + } + if (existing.name.contains('integration-test') || existing.name.contains('native-checks')) { + throw new GradleException('Ordinary development runs must not contain runtime probes: ' + existing) } - filter { includeGroupAndSubgroups('org.spongepowered') } } - mavenCentral() - maven { - name = 'MinecraftLibraries' - url = 'https://libraries.minecraft.net/' + copy { + from grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar + into mods } } -dependencies { - implementation files(grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar) - implementation minecraft.dependency( - "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}") - compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' -} - -def integrationTestClasses = layout.buildDirectory.dir('integration-test-mod/classes') -def compileIntegrationTestMod = tasks.register('compileIntegrationTestMod', JavaCompile) { - group = 'verification' - dependsOn tasks.named('classes') - source fileTree('src/integrationTest/java') - classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) - destinationDirectory = integrationTestClasses - options.encoding = 'UTF-8' +def prepareContentDevelopmentRun = tasks.register('prepareContentDevelopmentRun') { + group = 'ide' + dependsOn verifyContentDependencies + doLast { stageContentJars(file(developmentRunDirectory)) } } -def integrationTestModJar = tasks.register('integrationTestModJar', Jar) { - group = 'verification' - dependsOn compileIntegrationTestMod - archiveFileName = 'skysterrainsmoother-integration-test.jar' - destinationDirectory = layout.buildDirectory.dir('integration-test-mod') - from integrationTestClasses - from 'src/integrationTest/resources' -} +ext.stageTerrainContentJars = stageContentJars -def packagedIntegrationTestModJar = renamer.classes(integrationTestModJar) { - map.from minecraft.dependency.toSrgFile - libraries.from tasks.named('jar', Jar).flatMap { it.archiveFile } - libraries.from sourceSets.main.compileClasspath - output = layout.buildDirectory.file( - 'integration-test-mod/skysterrainsmoother-integration-test-reobf.jar') -} +def probePhase = providers.gradleProperty('skysTerrainSmootherProbePhase') -tasks.register('preparePackagedIntegrationProbe') { - group = 'verification' - description = 'Builds the reobfuscated probe used only in disposable packaged runtimes.' - dependsOn packagedIntegrationTestModJar -} +runs { + matching { it.name == 'gameTestServer' }.configureEach { + shouldExportToIDE.set(false) + } -def integrationRunDirectory = layout.buildDirectory.dir('integration-test-run') -def prepareIntegrationTest = tasks.register('prepareIntegrationTest') { - group = 'verification' - dependsOn integrationTestModJar - doLast { - File runDirectory = integrationRunDirectory.get().asFile - project.delete(runDirectory) - File mods = new File(runDirectory, 'mods') - mods.mkdirs() - project.copy { - from integrationTestModJar.flatMap { it.archiveFile } - into mods + configureEach { + workingDirectory project.layout.projectDirectory.dir(developmentRunDirectory) + if (probePhase.isPresent()) { + shouldExportToIDE.set(false) } - new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') - ['banned-players.json','banned-ips.json','ops.json','whitelist.json'].each { name -> - new File(runDirectory,name).setText('[]\n','UTF-8') + if (probePhase.isPresent()) { + systemProperty 'skysterrainsmoother.integrationPhase', probePhase.get() } - new File(runDirectory, 'server.properties').setText('''\ -level-name=world -level-seed=skys-terrain-smoother-integration -level-type=default -online-mode=false -server-ip=127.0.0.1 -server-port=0 -generate-structures=false -spawn-animals=false -spawn-monsters=false -spawn-protection=0 -max-tick-time=120000 -''', 'UTF-8') } -} -def createIntegrationProcess = { String phase, Object dependency -> - tasks.register("integration${phase}Process", JavaExec) { - group = 'verification' - dependsOn dependency - workingDir integrationRunDirectory - systemProperty 'skysterrainsmoother.integrationPhase', phase.toLowerCase(Locale.ROOT) - systemProperty 'log4j.configurationFile', file('src/integrationTest/resources/log4j2.xml').toURI().toString() - configureFromForgeRun(delegate, 'runServer') - doLast { - File marker = new File(integrationRunDirectory.get().asFile, - 'world/skysterrainsmoother-integration.properties') - if (!marker.isFile()) { - throw new GradleException("${phase} integration marker is missing: ${marker}") - } - Properties values = new Properties() - marker.withInputStream { values.load(it) } - if (values.getProperty("${phase.toLowerCase(Locale.ROOT)}_complete") != 'true') { - throw new GradleException("${phase} integration did not complete: ${marker}") - } - } + client { + modSource project.sourceSets.main } -} -def integrationFreshProcess = createIntegrationProcess('Fresh', prepareIntegrationTest) -def integrationReloadProcess = createIntegrationProcess('Reload', integrationFreshProcess) + server { + argument '--nogui' + modSource project.sourceSets.main + } -tasks.register('integrationTest') { - group = 'verification' - description = 'Runs the build-only legacy gameplay probe through fresh and reload servers.' - dependsOn integrationReloadProcess - doLast { - File runDirectory = integrationRunDirectory.get().asFile - File marker = new File(runDirectory, 'world/skysterrainsmoother-integration.properties') - Properties values = new Properties() - marker.withInputStream { values.load(it) } - ['fresh_complete', 'reload_complete', 'gameplay_checks', 'generation_checks'].each { key -> - if (!values.getProperty(key)) throw new GradleException("Missing integration evidence ${key}") - } - List crashReports = fileTree(new File(runDirectory, 'crash-reports')).files as List - if (!crashReports.isEmpty()) { - throw new GradleException("Integration runtime produced crash reports: ${crashReports}") - } - File latest = new File(runDirectory, 'logs/latest.log') - if (!latest.isFile() || latest.text.contains('Encountered an unexpected exception')) { - throw new GradleException("Integration runtime log is missing or contains a server failure: ${latest}") - } + data { + workingDirectory project.layout.projectDirectory.dir('run-data') + modSource project.sourceSets.main + arguments.addAll '--mod', mod_id, '--all', '--output', + file('src/generated/resources/').absolutePath, '--existing', + file('src/main/resources/').absolutePath } } -tasks.register('integrationClientProcess', JavaExec) { - group = 'verification' - dependsOn prepareIntegrationTest - workingDir integrationRunDirectory - systemProperty 'skysterrainsmoother.integrationPhase', 'client' - systemProperty 'log4j.configurationFile', file('src/integrationTest/resources/log4j2.xml').toURI().toString() - configureFromForgeRun(delegate, 'runClient') - doLast { - if (!new File(integrationRunDirectory.get().asFile, 'terrain-smoother-client-pass.txt').isFile()) - throw new GradleException('Client did not complete resource baking') - } +configurations { + runtimeClasspath.extendsFrom localRuntime } -tasks.named('check') { - dependsOn tasks.named('integrationTest') +repositories { + mavenCentral() +} + +dependencies { + implementation "net.neoforged:neoforge:${neo_version}" + compileOnly files(grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar) + testImplementation files(grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar) + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +tasks.withType(JavaExec).configureEach { javaLauncher = java21Launcher } +tasks.matching { it.name == 'runClient' || it.name == 'runServer' }.configureEach { + if (!probePhase.isPresent()) { + dependsOn prepareContentDevelopmentRun + } } tasks.named('processResources', ProcessResources) { filteringCharset = 'UTF-8' - inputs.properties([ - version: project.version, - mcversion: project.minecraft_version, - modid: project.mod_id, - name: project.mod_name, - description: project.mod_description - ]) - filesMatching('mcmod.info') { - expand version: project.version, mcversion: project.minecraft_version, - modid: project.mod_id, name: project.mod_name, - description: project.mod_description - } + Map metadata = ['loader_version_range', 'mod_license', 'mod_id', 'mod_version', + 'mod_name', 'mod_authors', 'mod_description', 'neo_version_range', + 'minecraft_version_range'].collectEntries { key -> [(key): project.property(key)] } + inputs.properties(metadata) + filesMatching('META-INF/neoforge.mods.toml') { expand metadata } filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) } tasks.named('jar', Jar) { - archiveClassifier = 'deobf' - destinationDirectory = layout.buildDirectory.dir('libs-dev') from(['LICENSE', 'LICENSE.spdx', 'NOTICE']) { into 'META-INF' } manifest { attributes([ @@ -444,19 +301,14 @@ tasks.named('jar', Jar) { 'Implementation-Title': base.archivesName.get(), 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_authors, - 'Built-On-Java': '8', - 'Built-On': "${project.minecraft_version}-${project.forge_version}" + 'Built-On-Java': '21', + 'Built-On': "${project.minecraft_version}-${project.neo_version}" ]) } filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) } -def releaseJar = renamer.classes(tasks.named('jar', Jar)) { - map.from minecraft.dependency.toSrgFile - libraries.from sourceSets.main.compileClasspath - archiveClassifier = null - output = layout.buildDirectory.file("libs/${base.archivesName.get()}-${project.version}.jar") -} +def releaseJar = tasks.named('jar', Jar) tasks.named('sourcesJar', Jar) { from(['LICENSE', 'LICENSE.spdx', 'NOTICE', 'BUILDINGBRICKS-MIT.txt']) { into 'META-INF' } @@ -467,10 +319,6 @@ tasks.named('javadocJar', Jar) { filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) } -['apiElements', 'runtimeElements'].each { configurationName -> - configurations.named(configurationName) { artifacts.clear() } - artifacts { add(configurationName, releaseJar) } -} tasks.named('assemble') { dependsOn releaseJar, tasks.named('sourcesJar'), tasks.named('javadocJar') } @@ -486,12 +334,11 @@ def releaseFiles = provider { tasks.register('verifyReleaseMetadata') { group = 'verification' doLast { - if (project.minecraft_version != '1.12.2' || project.forge_version != '14.23.5.2859' || - project.mapping_channel != 'stable' || project.mapping_version != '39-1.12' || - project.loader_name != 'forge' || project.loader_code != '1' || - project.java_version != '8' || project.java_toolchain_version != '8.0.502+7' || + if (project.minecraft_version != '1.21.1' || project.neo_version != '21.1.247' || + project.loader_name != 'neoforge' || project.loader_code != '2' || + project.java_version != '21' || project.java_toolchain_version != '21.0.7+6' || project.mod_id != 'skysterrainsmoother') { - throw new GradleException('Unexpected 1.12.2 release metadata') + throw new GradleException('Unexpected 1.21.1 release metadata') } } } @@ -510,7 +357,7 @@ tasks.register('verifyReleaseArtifacts') { ZipFile zip = new ZipFile(new File(libs, releaseFiles.get()[0])) try { Set names = zip.entries().collect { it.name } as Set - ['mcmod.info', 'META-INF/LICENSE', 'META-INF/LICENSE.spdx', 'META-INF/NOTICE', + ['META-INF/neoforge.mods.toml', 'META-INF/LICENSE', 'META-INF/LICENSE.spdx', 'META-INF/NOTICE', 'zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.class'].each { required -> if (!names.contains(required)) throw new GradleException("Release jar is missing ${required}") } @@ -535,8 +382,8 @@ tasks.register('verifyReleaseArtifacts') { if (!entry.directory) { byte[] auditBytes = zip.getInputStream(entry).bytes if (entry.name.endsWith('.class') && - (((auditBytes[6] & 255) << 8) | (auditBytes[7] & 255)) != 52) { - throw new GradleException("Non-Java-8 production class: ${entry.name}") + (((auditBytes[6] & 255) << 8) | (auditBytes[7] & 255)) != 65) { + throw new GradleException("Non-Java-21 production class: ${entry.name}") } String content = new String(zip.getInputStream(entry).bytes, StandardCharsets.ISO_8859_1).toLowerCase(Locale.ROOT) @@ -605,40 +452,14 @@ ext.releaseJar = releaseJar apply from: 'gradle/release.gradle' -// Keep the old entry point disabled until publication is configured. +// Releases use the validated MMD workflow. tasks.register('publishRelease') { - doLast { throw new GradleException('Publication is disabled for the local beta.') } -} - -idea { - module { downloadSources = true; downloadJavadoc = true } -} -eclipse { - classpath { - downloadSources = true - downloadJavadoc = true - file.whenMerged { classpath -> - // Eclipse copies these files on every build. Use Gradle's expanded metadata, never the template. - def resources = classpath.entries.findAll { - it instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder && - it.path in ['src/main/resources', 'build/resources/main'] - } - // Keep one entry when Gradle merges an existing Eclipse classpath. - resources.drop(1).each { classpath.entries.remove(it) } - resources.take(1).each { entry -> - entry.path = 'build/resources/main' - } - } - } - synchronizationTasks 'processResources', 'genEclipseRuns' -} - -tasks.named('eclipseClasspath') { - dependsOn tasks.named('processResources'), verifyContentDependencies + doLast { throw new GradleException('Use the MMD release workflow to publish this mod.') } } def prepareEclipseResources = tasks.register('prepareEclipseResources') { group = 'ide' + description = 'Copies processed resources into Eclipse production output.' dependsOn tasks.named('processResources') doLast { project.copy { @@ -650,19 +471,24 @@ def prepareEclipseResources = tasks.register('prepareEclipseResources') { def configureEclipseBuildship = tasks.register('configureEclipseBuildship') { group = 'ide' + description = 'Pins Eclipse Buildship to the Gradle home used for NeoForge.' doLast { + String requestedGradleHome = System.getenv('GRADLE_USER_HOME') + File gradleHome = requestedGradleHome == null || requestedGradleHome.trim().isEmpty() + ? gradle.gradleUserHomeDir : file(requestedGradleHome) File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs') Properties preferences = new Properties() - if (preferencesFile.isFile()) preferencesFile.withInputStream { preferences.load(it) } - [ - 'eclipse.preferences.version': '1', - 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', - 'connection.gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, - 'connection.project.dir': '', - 'gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, - 'override.workspace.settings': 'true' - ].each { key, value -> preferences.setProperty(key, value) } - // Refreshes must use the same checkouts, without retaining an old candidate override. + if (preferencesFile.isFile()) { + preferencesFile.withInputStream { preferences.load(it) } + } + preferences.setProperty('eclipse.preferences.version', '1') + preferences.setProperty('connection.gradle.distribution', 'GRADLE_DISTRIBUTION(WRAPPER)') + preferences.setProperty('connection.gradle.user.home', gradleHome.canonicalPath) + preferences.setProperty('connection.project.dir', '') + preferences.setProperty('gradle.user.home', gradleHome.canonicalPath) + preferences.setProperty('java.home', System.getProperty('java.home')) + preferences.setProperty('override.workspace.settings', 'true') + // Keep explicit local dependency choices through a Buildship refresh. List arguments = preferences.getProperty('arguments', '').tokenize(' ') arguments.removeAll { it.startsWith('-PgrassSlabsDirectory=') || it.startsWith('-PgrassSlabsVersion=') || @@ -677,131 +503,202 @@ def configureEclipseBuildship = tasks.register('configureEclipseBuildship') { } preferences.setProperty('arguments', arguments.join(' ')) preferencesFile.parentFile.mkdirs() - preferencesFile.withOutputStream { preferences.store(it, 'Generated Buildship configuration.') } + preferencesFile.withOutputStream { preferences.store(it, + 'Generated Buildship configuration for the NeoForge development environment.') } } } -tasks.register('isolateEclipseProductionRuns') { - group = 'ide' - dependsOn prepareEclipseResources - doLast { - ['runClient.launch', 'runServer.launch', 'runTestClient.launch', 'runTestServer.launch'].each { name -> - File launch = file(name) - if (!launch.isFile()) throw new GradleException("ForgeGradle did not generate ${name}") - String contents = launch.getText('UTF-8') - contents = contents.replace('${MC_VERSION}', project.minecraft_version.toString()) - boolean production = name in ['runClient.launch', 'runServer.launch'] - if (production && !contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) { - contents = contents.replace('', - ' ' + - System.lineSeparator() + '') +eclipse { + project { + natures 'org.eclipse.buildship.core.gradleprojectnature' + buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' + } + classpath { + downloadSources = true + downloadJavadoc = true + file.whenMerged { classpath -> + def libraryPaths = new LinkedHashSet() + classpath.entries = classpath.entries.findAll { entry -> + !(entry instanceof org.gradle.plugins.ide.eclipse.model.Library) + || libraryPaths.add(entry.path) } - ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option -> - String prefix = option + ' ' - int start = contents.indexOf(prefix) - if (start < 0) throw new GradleException("${name} is missing ${option}") - start += prefix.length() - int end = contents.indexOf(' --', start) - if (end < 0) throw new GradleException("${name} has malformed ${option}") - String value = contents.substring(start, end) - if (value.find(/\s/) != null && !value.startsWith('"')) { - contents = contents.substring(0, start) + '"' + value + '"' + contents.substring(end) - } + classpath.entries.removeAll { entry -> + entry instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder && + ['src/main/resources', 'src/generated/resources'].contains(entry.path) + } + if (!classpath.entries.any { entry -> entry.path == 'build/resources/main' }) { + classpath.entries.add(new org.gradle.plugins.ide.eclipse.model.SourceFolder( + 'build/resources/main', 'bin/main')) } - launch.setText(contents, 'UTF-8') } } + synchronizationTasks 'prepareEclipseResources' } -tasks.named('genEclipseRuns') { - finalizedBy configureEclipseBuildship, 'isolateEclipseProductionRuns' +tasks.named('eclipse').configure { + dependsOn prepareEclipseResources + dependsOn configureEclipseBuildship + doLast { + fileTree(file('.eclipse/configurations')) { include '**/*.launch' }.files.each { launch -> + def model = new groovy.xml.XmlParser(false, false).parse(launch) + if (model.@type != 'org.eclipse.jdt.launching.localJavaApplication') return + def exclude = model.booleanAttribute.find { + it.@key == 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' + } + if (exclude == null) { + model.appendNode('booleanAttribute', [ + key: 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE', value: 'true']) + } else { + exclude.@value = 'true' + } + launch.setText(groovy.xml.XmlUtil.serialize(model).replace('\r\n', '\n'), 'UTF-8') + } + } } -tasks.register('verifyEclipseProductionClasspath') { +def verifyEclipseConfiguration = tasks.register('verifyEclipseConfiguration') { group = 'verification' - dependsOn tasks.named('genEclipseRuns'), tasks.named('eclipse') + description = 'Rejects duplicate Eclipse dependencies and production/test classpath leakage.' + dependsOn tasks.named('eclipse') doLast { - File prefsFile = file('.settings/org.eclipse.buildship.core.prefs') - if (!prefsFile.isFile()) throw new GradleException('Missing Buildship preferences') - Properties prefs = new Properties() - prefsFile.withInputStream { prefs.load(it) } - if (file(prefs.getProperty('connection.gradle.user.home', '')).canonicalFile != gradle.gradleUserHomeDir.canonicalFile) { - throw new GradleException('Eclipse Buildship uses the wrong Gradle home') + File classpathFile = file('.classpath') + if (!classpathFile.isFile()) throw new GradleException('Eclipse .classpath was not generated') + String classpathText = classpathFile.getText('UTF-8') + if (!classpathText.contains('path="build/resources/main"') || + classpathText.contains('path="src/main/resources"') || + classpathText.contains('path="src/generated/resources"')) { + throw new GradleException('Eclipse must consume only Gradle-processed production resources') + } + def classpath = new groovy.xml.XmlParser(false, false).parse(classpathFile) + def entries = classpath.classpathentry + if (entries.find { it.@kind == 'output' }.@path != 'bin/default' || + entries.find { it.@path == 'src/main/java' }.@output != 'bin/main' || + entries.find { it.@path == 'src/test/java' }.@output != 'bin/test' || + !classpathText.contains('/JavaSE-21/')) { + throw new GradleException('Eclipse production, test and default output must be separate on Java 21') + } + List libraries = entries.findAll { it.@kind == 'lib' } + .collect { file(it.@path).canonicalPath } + if (libraries.size() != libraries.toSet().size()) { + throw new GradleException('Eclipse has a duplicate library path') + } + List grassLibraries = libraries.collect { file(it) }.findAll { it.name.startsWith('SkysGrassSlabs-') } + List piecesLibraries = libraries.collect { file(it) }.findAll { it.name.startsWith('SkysBuildingPieces-') } + if (grassLibraries != [grassSlabsDevelopmentJar.canonicalFile] || + piecesLibraries != [buildingPiecesDevelopmentJar.canonicalFile]) { + throw new GradleException('Eclipse must use the same two content jars as Gradle') } - List arguments = prefs.getProperty('arguments', '').tokenize(' ') + File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs') + if (!preferencesFile.isFile()) throw new GradleException('Missing Buildship preferences') + Properties preferences = new Properties() + preferencesFile.withInputStream { preferences.load(it) } + if (preferences.getProperty('override.workspace.settings') != 'true') { + throw new GradleException('Eclipse Buildship workspace override is not active') + } + List arguments = preferences.getProperty('arguments', '').tokenize(' ') if (!arguments.contains('-PgrassSlabsDirectory=' + relativePath(grassSlabsDirectory)) || !arguments.contains('-PbuildingPiecesDirectory=' + relativePath(buildingPiecesDirectory))) { throw new GradleException('Eclipse Buildship lost the selected content checkouts') } List overrides = arguments.findAll { it.startsWith('-PbuildingPiecesCoreVersion=') } - List expectedOverrides = localPiecesVersion.isPresent() ? - ['-PbuildingPiecesCoreVersion=' + piecesVersion] : [] - if (overrides != expectedOverrides) { + if (overrides != (localPiecesVersion.isPresent() ? ['-PbuildingPiecesCoreVersion=' + piecesVersion] : [])) { throw new GradleException('Eclipse Buildship retained an incorrect Building Pieces version override') } - File metadata = file('bin/main/mcmod.info') - def eclipseClasspath = new groovy.xml.XmlSlurper().parse(file('.classpath')) - def piecesLibraries = eclipseClasspath.classpathentry.findAll { - it.@kind == 'lib' && it.@path.text().tokenize('/\\').last().startsWith('SkysBuildingPieces-') - }*.@path*.text().collect { file(it).canonicalFile } - if (piecesLibraries != [buildingPiecesDevelopmentJar.canonicalFile]) { - throw new GradleException('Eclipse must use only the selected Building Pieces development jar') + Set runLaunches = fileTree(file('.eclipse/configurations')) { + include '**/*.launch' + }.files.findAll { launch -> + String name = launch.name.toLowerCase(Locale.ROOT) + name.startsWith('run ') && (name.contains('client') || name.contains('server')) } - def grassLibraries = eclipseClasspath.classpathentry.findAll { - it.@kind == 'lib' && it.@path.text().tokenize('/\\').last().startsWith('SkysGrassSlabs-') - }*.@path*.text().collect { file(it).canonicalFile } - if (grassLibraries != [grassSlabsDevelopmentJar.canonicalFile]) { - throw new GradleException('Eclipse must use only the pinned Grass Slabs development jar') + if (runLaunches.size() != 4 || runLaunches.count { it.name.toLowerCase(Locale.ROOT).contains('mineralogy') } != 2) { + throw new GradleException('Expected isolated ordinary and Mineralogy Eclipse client/server launches') } - def resourceFolders = eclipseClasspath.classpathentry.findAll { it.@kind == 'src' }*.@path*.text() - if (resourceFolders.contains('src/main/resources') || - resourceFolders.count { it == 'build/resources/main' } != 1) { - throw new GradleException('Eclipse must copy processed resources, not overwrite them with templates') + runLaunches.each { launch -> + String launchText = launch.getText('UTF-8') + if (launchText.contains('neoforge.enabledGameTestNamespaces=') || + launchText.contains('integration-test') || launchText.contains('src/test')) { + throw new GradleException("${launch.name} exposes test code to a regular run") + } + def model = new groovy.xml.XmlParser(false, false).parse(launch) + def exclude = model.booleanAttribute.find { + it.@key == 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' + } + def modSources = model.mapAttribute.mapEntry.flatten().find { it.@key == 'MOD_CLASSES' } + String expectedOutput = file('bin/main').canonicalPath.replace('\\', '/') + if (model.@type != 'org.eclipse.jdt.launching.localJavaApplication' || + exclude?.@value != 'true' || + modSources?.@value?.toString()?.replace('\\', '/') != project.name + '%%' + expectedOutput) { + throw new GradleException("${launch.name} must use only the merged production output") + } } - def parsed = new JsonSlurper().parseText(metadata.getText('UTF-8')) - if (parsed[0].version != project.mod_version || parsed[0].mcversion != project.minecraft_version) { - throw new GradleException('Eclipse output contains incorrect mcmod.info') + File metadata = file('bin/main/META-INF/neoforge.mods.toml') + if (!metadata.isFile() || metadata.getText('UTF-8').contains('${mod_version}') || + !metadata.getText('UTF-8').contains("version=\"${project.version}\"")) { + throw new GradleException('Eclipse output contains missing or unexpanded NeoForge metadata') } - ['runClient.launch', 'runServer.launch'].each { name -> - String contents = file(name).getText('UTF-8').replace('\\', '/') - if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE') || - contents.contains('src/test') || contents.contains('bin/test') || contents.contains('junit-')) { - throw new GradleException("${name} exposes test code or dependencies") + List forbidden = ['src/test', 'bin/test', 'build/classes/java/test', + 'net.minecraftforge', 'META-INF/mods.toml'] + fileTree(project.projectDir) { include 'run*.launch' }.files.each { launch -> + List leaked = forbidden.findAll { launch.getText('UTF-8').contains(it) } + if (!leaked.isEmpty()) { + throw new GradleException("${launch.name} exposes test/Forge/local content: ${leaked}") } } } } -tasks.named('check') { - dependsOn tasks.named('verifyReleaseMetadata') -} -def clientFresh = tasks.register('integrationClientFreshProcess', JavaExec) { - dependsOn prepareIntegrationTest - workingDir integrationRunDirectory - systemProperty 'skysterrainsmoother.integrationPhase', 'client-fresh' - systemProperty 'log4j.configurationFile', file('src/integrationTest/resources/log4j2.xml').toURI().toString() - configureFromForgeRun(delegate, 'runClient') - doLast { - File marker=new File(integrationRunDirectory.get().asFile,'saves/terrain-smoother-world/skysterrainsmoother-integration.properties') - if(!marker.isFile() || !marker.text.contains('fresh_complete=true')) throw new GradleException('Integrated fresh client did not complete') + + +tasks.named('eclipse') { dependsOn prepareContentDevelopmentRun } +tasks.register('verifyEclipseProductionClasspath') { dependsOn verifyEclipseConfiguration } + +apply from: 'gradle/recipe-advancements.gradle' +apply from: 'gradle/mineralogy-runs.gradle' + +// The native probes are separate jars so neither source set can leak into ordinary games. +def addNativeProbe = { String taskName, String sources, String output, String fileName -> + def classes = layout.buildDirectory.dir(output + '/classes') + def compile = tasks.register('compile' + taskName, JavaCompile) { + dependsOn tasks.named('classes') + source fileTree('src/' + sources + '/java') + classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) + destinationDirectory = classes } -} -tasks.register('integrationClientReloadProcess', JavaExec) { - dependsOn clientFresh - workingDir integrationRunDirectory - systemProperty 'skysterrainsmoother.integrationPhase', 'client-reload' - systemProperty 'log4j.configurationFile', file('src/integrationTest/resources/log4j2.xml').toURI().toString() - configureFromForgeRun(delegate, 'runClient') - doLast { - File marker=new File(integrationRunDirectory.get().asFile,'saves/terrain-smoother-world/skysterrainsmoother-integration.properties') - if(!marker.isFile() || !marker.text.contains('reload_complete=true')) throw new GradleException('Integrated reload client did not complete') + tasks.register(taskName.uncapitalize(), Jar) { + dependsOn compile + archiveFileName = fileName + destinationDirectory = layout.buildDirectory.dir(output) + from classes + from('src/' + sources + '/resources') { exclude 'log4j2.xml' } } } +def integrationJar = addNativeProbe('IntegrationTestModJar', 'integrationTest', + 'integration-test-mod', 'skysterrainsmoother-integration-test.jar') +def bootstrapJar = addNativeProbe('NativeBootstrapProbeJar', 'bootstrapTest', + 'native-bootstrap', 'terrain-smoother-native-checks.jar') +tasks.register('preparePackagedIntegrationProbe') { dependsOn integrationJar, bootstrapJar } -ext.configureMineralogyVerificationRun = configureFromForgeRun -ext.mineralogyProfileProbeId = 'skysterrainsmoother' -ext.mineralogyProfileAdditionalMods = files() -apply from: 'gradle/mineralogy-runs.gradle' +def launcherClasses = tasks.register('compileNativeUnitLauncher', JavaCompile) { + source fileTree(new File(buildingPiecesDirectory, 'src/testLauncher/java')) + classpath = files() + destinationDirectory = layout.buildDirectory.dir('native-unit-launcher/classes') +} +def launcher = tasks.register('nativeUnitLauncherJar', Jar) { + dependsOn launcherClasses + archiveFileName = 'native-unit-launcher.jar' + destinationDirectory = layout.buildDirectory.dir('native-unit-launcher') + from launcherClasses.flatMap { it.destinationDirectory } + manifest.attributes('Premain-Class': 'zone.moddev.mc.skysbuildingpieces.testlaunch.NativeTestLauncher') +} +tasks.withType(Test).configureEach { + dependsOn launcher + doFirst { jvmArgs '-javaagent:' + launcher.get().archiveFile.get().asFile.absolutePath } +} -apply from: 'gradle/recipe-advancements.gradle' +tasks.register('verifyNativeLogPolicy', Exec) { + group = 'verification' + commandLine providers.gradleProperty('pythonExecutable').getOrElse(System.getProperty('os.name').startsWith('Windows') ? 'python' : 'python3'), + '-m', 'unittest', 'discover', '-s', 'tools' +} +tasks.named('check') { dependsOn tasks.named('verifyNativeLogPolicy') } diff --git a/docs/API.md b/docs/API.md index f759c24..8d86c3c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,20 +1,24 @@ # Add-on API, version 1 Use `zone.moddev.mc.skysterrainsmoother.api.TerrainSmoothingApi` during your -mod's initialization or post-initialization. Declare ordering after -`skysterrainsmoother`. Registration closes at load completion. +mod's common or client setup. Declare ordering after `skysterrainsmoother`. +Registration opens after Terrain Smoother has loaded its built-in materials +and closes at load completion. NeoForge can run setup callbacks in parallel; +the catalogue serializes registration and publishes immutable lookups. +Biome and protection callbacks must also be safe to call from generation threads. Create an immutable `TerrainMaterial` with a namespaced logical identity, a set of exact full-block source states and a bottom slab state. Optionally supply arrays of four bottom steps and four bottom corners. Array order is north, east, south, west; corners use northwest, northeast, southeast, southwest. Steps occupy one quarter of a block's volume and corners one eighth. -Every output must be half a block tall, non-tile, and match that footprint. +Every output must be half a block tall, dry, non-tile, and fill that footprint. +Sources and converted supports must be dry full blocks without tile entities. ```java TerrainSmoothingApi.registerMaterial(new TerrainMaterial( - new ResourceLocation("example", "natural_rock"), - Collections.singleton(naturalRock.getDefaultState()), + ResourceLocation.fromNamespaceAndPath("example", "natural_rock"), + Collections.singleton(naturalRock.defaultBlockState()), bottomSlab, null, null, false, null)); ``` diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 3a6a6e2..cbd33fa 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -1,79 +1,54 @@ # Building from source -Use the Gradle 9.6.1 wrapper with Java 17 and Temurin 8.0.502+7 for production. -Set `STS_JAVA8_HOME` to the Java 8 installation. This branch targets -Minecraft 1.12.2 with ForgeGradle 7.0.34. +This branch uses NeoForge 21.1.247, NeoGradle 7.1.38 and the Gradle 9.2.1 +wrapper. Run Gradle with Temurin 21.0.7+6. The same Java version compiles and +runs the mod. The log checks use Python 3.11 or newer. -Build the exact Grass Slabs and Building Pieces source commits listed in -`gradle/content-dependencies.properties` with their `jar` tasks first. +Build the Grass Slabs and Building Pieces commits listed in +`gradle/content-dependencies.properties` with their `assemble` tasks first. +Both development and packaged games use the same jars. The build checks the +source commits and jar hashes. It does not include the dependency classes +or assets in Terrain Smoother. -The source lock records released Grass Slabs 1.1.2.112021 and Building Pieces -0.3.8.112021. - -The default layout uses sibling checkouts. For another layout, pass -`-PgrassSlabsDirectory=` and -`-PbuildingPiecesDirectory=`. -Then run: +The default layout uses sibling checkouts. For a different layout, pass +`-PgrassSlabsDirectory=` and `-PbuildingPiecesDirectory=`. +When testing another local Building Pieces candidate, supply its directory +and `-PbuildingPiecesCoreVersion=`. CI always uses the lock. ```text -gradlew check build javadoc verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath +gradlew check build javadoc verifyReleaseArtifacts writeReleaseChecksums +gradlew verifyEclipseProductionClasspath ``` -Development dependencies use deobfuscated jars. Packaged games use the normal -main jars; dependency classes and assets are not bundled. CI builds the exact -pinned commits rather than moving branches. - -A local-only `buildingPiecesCoreVersion` override can test another core -candidate. It requires an explicit core directory and is rejected in CI. -Eclipse's ignored Buildship preferences retain the selected checkouts. A version -override is saved only when explicitly supplied; normal generation clears any -older override. Refresh the Gradle project in Eclipse after generating the runs. - -Use `genEclipseRuns eclipse` to prepare the Java 8 IDE setup. Ordinary launches -exclude unit tests and build-only runtime probes. See [the release guide](RELEASING.md) -for publication. - -## Testing with Mineralogy - -Run `gradlew genEclipseRuns eclipse`, then refresh the Eclipse project. Select -`runMineralogyClient` under Java Application in Run Configurations. The matching -`runMineralogyServer` profile is available for a dedicated server. From a terminal, -use `gradlew runMineralogyClient` or `gradlew runMineralogyServer`. - -These profiles use a separate `run-mineralogy` folder and load Mineralogy plus -its OreSpawn dependency. -Ordinary client and server profiles keep their existing mod sets and worlds. -Runtime probes and unit tests are excluded from these manual profiles. +If Python isn't on your path, pass `-PpythonExecutable=` to the first +command. The Eclipse task prepares the processed resources and the content +jars without clearing your game settings or saves. Import the Gradle project +with Java 21 and refresh it after generating the launches. The regular +client and server launches exclude all test code. -The expected release jars and their checksums are listed in -`gradle/mineralogy-runtime.properties`. The default paths use sibling checkouts; -for a different layout, pass `-PmineralogyRuntimeJar=` and -`-PorespawnRuntimeJar=` when generating the profiles. Missing optional jars -do not prevent an ordinary build; `prepareMineralogyDevelopmentRun` explains -what is needed before the combined profile can run. The jars are copied, not -modified. Their access rules are translated for the mapped development game. +## Trying Mineralogy -If a sibling build has moved on, keep the qualified jars under -`.gradle/qualification-dependencies/` with their original filenames. -The profiles prefer those copies, but still require the locked checksums. -An explicit runtime-jar property takes precedence over either location. +Use `runMineralogyClient` or `runMineralogyServer` for a separate development +game in `run-mineralogy`. These runs use the NeoForge jars pinned in +`gradle/mineralogy-runtime.properties`. Point Gradle to them with +`-PmineralogyRuntimeJar=` and `-PorespawnRuntimeJar=` if you keep them +elsewhere. Eclipse also generates Mineralogy client and server launches. -New configurations use smoothing level 3. An existing saved level is kept, so -choose level 3 in Mods > Config and restart if that test folder already has a -different setting. With Building Pieces 0.3.8, Mineralogy rocks use the core's -steps at level 2 and corners at level 3, without a separate add-on. +With Mineralogy installed and its slabs enabled, natural rock has steps and +corners from Building Pieces at smoothing levels 2 and 3. New configurations +start at level 3; existing choices are kept. -`gradlew integrationMineralogyTest` checks fresh-world and reload behaviour in -a disposable build folder. `integrationMineralogyClientProcess` checks client -resources there. Neither task opens the manual profile's saved worlds. +## World checks -## Recipe-book resources +`preparePackagedIntegrationProbe` builds separate test mods for disposable +games. The runtime tools in `tools/` check fresh worlds and reloads, including +recipes, configuration, slopes and cave floors. For packaged upgrades, pass a +sealed fixture directory to `run_packaged_checks.py`. Always use a new +directory under `build/` for the first load. The tool retains the logs and +checks that the source archive stays unchanged. -The build generates unlock advancements from `src/main/recipe-book/recipes.json`. -The runtime checks compare those entries with the registered recipes and exercise -inventory discovery, crafting-table discovery and recipe-book persistence. -Optional catalogue entries load only when their recipe exists. +The recipe unlocks are generated from `src/main/recipe-book/recipes.json`. +The four sand recipes keep their IDs. The runtime checks exercise crafting, +inventory discovery and recipe-book persistence. -Recipe unlocks have no shared advancement parent. Minecraft 1.12 walks that -visibility tree recursively when loading completed progress, which can overflow -with a large catalogue. The inventory and crafting unlock criteria are unchanged. +See [the release guide](RELEASING.md) for release preparation. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index b8c8e2f..d3c6521 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -4,7 +4,7 @@ Open **Mods**, select **Sky's Terrain Smoother** and choose **Config**. You can turn smoothing on or off, choose level 1–3 and enable or disable Mineralogy support. Save with **Done**, then restart Minecraft to apply your choices. -You can also edit `config/skysterrainsmoother.cfg` while the game or server is +You can also edit `config/skysterrainsmoother-common.toml` while the game or server is stopped. On a dedicated server, edit the server's file and restart it; the client's Config screen does not change a remote server. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 5337f8b..babb389 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -4,8 +4,7 @@ Pushes and pull requests run the build, tests, artifact audit, CodeQL and Gradle wrapper checks. CI uploads the main, sources and Javadoc jars with their checksums. Tags only validate a candidate; they never publish it. -This mod publishes to CurseForge project 1768953. Publication is enabled only -in MMD; a fork build never uploads a release. +This mod uses CurseForge project 1768953. Only MMD can publish it. The MMD release workflow uses the organisation's `CURSEFORGE_TOKEN`, `MAVEN_UPLOAD_URL`, `MAVEN_UPLOAD_USERNAME` and `MAVEN_UPLOAD_PASSWORD`. diff --git a/docs/WORLD-UPGRADES.md b/docs/WORLD-UPGRADES.md index 47d9acf..1c75585 100644 --- a/docs/WORLD-UPGRADES.md +++ b/docs/WORLD-UPGRADES.md @@ -16,3 +16,52 @@ their terrain can be smoothed. Turning smoothing off does not remove pieces already placed. They remain ordinary blocks owned by the relevant content mod. + +## Upgrading to Minecraft 1.21.1 + +Update Grass Slabs, Building Pieces and Terrain Smoother together. Keep +your configuration and world data so your settings and migration history +carry over. Sand slabs keep their colour, orientation and waterlogging. +Travel to new chunks to see smoothing on newly generated terrain. + +You can also move a Minecraft 1.21.1 Forge save to NeoForge. First check +that every other mod in the world has a compatible NeoForge build. Use a +copy of the save and install the NeoForge versions of all three Sky mods. + +## Upgrading from Minecraft 1.18.2 + +Update Terrain Smoother, Grass Slabs and Building Pieces together. Keep your +configuration and world data when you move the save. Sand slabs retain their +colour, slab half and waterlogging. Smoothing applies to newly generated chunks. + +## Upgrading from Minecraft 1.17.1 + +Update the three mods together and keep your configuration and world data. +Saved pieces, inventories and migration history are retained. The taller world +range also applies to newly generated cave floors; existing chunks stay unchanged. + +## Upgrading from Minecraft 1.16.5 + +Update Grass Slabs, Building Pieces and Terrain Smoother together. Keep the +configuration and world data files to retain your choices and migration history. +Saved sand slabs keep their colour, upper or lower half, water and item data. +Existing chunks stay unchanged; smoothing still applies only to new terrain. + +## Upgrading from Minecraft 1.14.4 + +Update Grass Slabs, Building Pieces and Terrain Smoother together. Existing +pieces keep their named states, including sand colour, slab halves and water. +Keep your configuration files to retain the smoothing level and other choices. +Saved chunks stay unchanged; smoothing still applies only to new terrain. + +## Upgrading from Minecraft 1.12.2 + +Use a backup and keep both required content mods installed. Sand slabs keep their +colour and upper/lower placement. Red sand now has its own block and item ID, +following Minecraft's flattened block format; saved red sand slabs and inventory +stacks are converted automatically, including unopened chunks. Custom item data +and stack counts are kept. An unsupported saved sand value stops the upgrade +with a report rather than replacing it with ordinary sand. + +This conversion does not smooth old terrain. Existing pieces remain in place, +and the saved-chunk index excludes previously generated chunks from new smoothing. diff --git a/gradle.properties b/gradle.properties index 8f9e378..e5ccc0e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,24 +3,25 @@ org.gradle.daemon=false org.gradle.configuration-cache=false org.gradle.caching=true org.gradle.parallel=false -net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.12.2 -minecraft_version_range=[1.12.2] -forge_version=14.23.5.2859 -mapping_channel=stable -mapping_version=39-1.12 -loader_name=forge -loader_code=1 -java_version=8 -java_toolchain_version=8.0.502+7 -gradle_java_version=17 +minecraft_version=1.21.1 +minecraft_version_range=[1.21.1,1.22) +neo_version=21.1.247 +neo_version_range=[21.1.247,21.2) +loader_version_range=[3,) +loader_name=neoforge +loader_code=2 +java_version=21 +java_toolchain_version=21.0.7+6 +java_setup_version=21.0.7+6.0.LTS +gradle_java_version=21 +gradle_java_toolchain_version=21.0.7+6 mod_id=skysterrainsmoother release_repository=MinecraftModDevelopmentMods/SkysTerrainSmoother release_enabled=true curseforge_project_id=1768953 mod_name=Sky's Terrain Smoother mod_license=LGPL-2.1-only -mod_version=0.1.0.112021 +mod_version=0.1.0.121012 mod_group=zone.moddev.mc.skysterrainsmoother mod_authors=SkyBlade1978 mod_description=Smooths new Overworld surfaces with existing slabs, steps and corners. Optional Mineralogy support. diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index 3cac94c..ffc56dd 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,6 +1,8 @@ -grass.repository=MinecraftModDevelopmentMods/SkysGrassSlabs -grass.commit=b2636d8a67efa3f257a8a69293ce513e979010fd -grass.version=1.1.2.112021 -pieces.repository=MinecraftModDevelopmentMods/SkysBuildingPieces -pieces.commit=af3ce9b73d215a2f9e27a11324da56a752e4edac -pieces.version=0.3.8.112021 +grass.version=1.2.0.121012 +grass.commit=6061fa264f3d4e710fde7279ffe6d3958fc98929 +grass.sha256=F095DD84AF9BFB6F90A9C3A091B526508C9516D3B76E0162ED67B470004322F8 +grass.development.sha256=F095DD84AF9BFB6F90A9C3A091B526508C9516D3B76E0162ED67B470004322F8 +pieces.version=0.3.9.121012 +pieces.commit=98a8c72378829b7815527015c8aa124718beea04 +pieces.sha256=6E992F1A5CC79235C4FBD3E8EFCC2A865E147B793D2C7D20D6EF1815630F3105 +pieces.development.sha256=6E992F1A5CC79235C4FBD3E8EFCC2A865E147B793D2C7D20D6EF1815630F3105 diff --git a/gradle/development-log4j2.xml b/gradle/development-log4j2.xml deleted file mode 100644 index 1281087..0000000 --- a/gradle/development-log4j2.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/gradle/mineralogy-runs.gradle b/gradle/mineralogy-runs.gradle index ede971a..cd6506b 100644 --- a/gradle/mineralogy-runs.gradle +++ b/gradle/mineralogy-runs.gradle @@ -1,290 +1,83 @@ import java.security.MessageDigest -import java.nio.file.Files -// These are optional development mods, never compile or production dependencies. -def runtimeLock = new Properties() -file('gradle/mineralogy-runtime.properties').withInputStream { runtimeLock.load(it) } -def runtimeSources = ['mineralogy', 'orespawn'].collectEntries { mod -> - File siblingJar = file(runtimeLock.getProperty(mod + '.path')) +// Optional mods belong to a separate game folder, outside the production classpath. +def lock = new Properties() +file('gradle/mineralogy-runtime.properties').withInputStream { lock.load(it) } +def runtimeSources = ['mineralogy', 'orespawn'].collectEntries { name -> + File siblingJar = file(lock.getProperty(name + '.path')) File retainedJar = file('.gradle/qualification-dependencies/' + siblingJar.name) - // Sibling builds can change during other work. A retained copy still has to - // pass the same checksum check before it can enter a development profile. - [(mod): file(providers.gradleProperty(mod + 'RuntimeJar') + [(name): file(providers.gradleProperty(name + 'RuntimeJar') .getOrElse((retainedJar.isFile() ? retainedJar : siblingJar).path))] } -def extraMods = project.ext.mineralogyProfileAdditionalMods def gameDirectory = layout.projectDirectory.dir( providers.gradleProperty('mineralogyRunDirectory').getOrElse('run-mineralogy')) -def configureVerification = project.ext.configureMineralogyVerificationRun - -// Legacy release jars declare SRG access rules. The mapped development game needs -// their MCP names before launch; the original optional jars remain untouched. -configurations { mineralogyDevelopmentMappings } -repositories { - maven { - name = 'MineralogyDevelopmentMappings' - url = 'https://maven.minecraftforge.net/' - metadataSources { artifact() } - content { includeModule('de.oceanlabs.mcp', 'mcp_stable') } - } -} -dependencies { - mineralogyDevelopmentMappings "de.oceanlabs.mcp:mcp_stable:${project.mapping_version}@zip" -} -if (runtimeSources.values().every { it.isFile() }) { - Map mappedMembers = [:] - new java.util.zip.ZipFile(configurations.mineralogyDevelopmentMappings.singleFile).withCloseable { mappings -> - ['fields.csv', 'methods.csv'].each { name -> - mappings.getInputStream(mappings.getEntry(name)).withCloseable { input -> - input.getText('UTF-8').readLines().drop(1).each { row -> - String[] columns = row.split(',', 3) - mappedMembers[columns[0]] = columns[1] - } - } - } - } - List rules = [] - runtimeSources.each { mod, source -> - new java.util.jar.JarFile(source).withCloseable { archive -> - String declared = archive.manifest?.mainAttributes?.getValue('FMLAT') - declared?.tokenize()?.each { name -> - def entry = archive.getJarEntry('META-INF/' + name) - if (entry == null) throw new GradleException("Missing ${mod} access rules: ${name}") - archive.getInputStream(entry).withCloseable { input -> - String mapped = input.getText('UTF-8').replaceAll(/\b(?:func|field)_\d+_[A-Za-z_]+\b/) { member -> - if (!mappedMembers.containsKey(member)) throw new GradleException("Unmapped optional access rule: ${member}") - mappedMembers[member] - } - rules.add(mapped) - } - } - } - } - File accessRules = file('.gradle/mineralogy-runtime/access-transformers.cfg') - accessRules.parentFile.mkdirs() - accessRules.setText(rules.join('\n') + '\n', 'UTF-8') - minecraft.accessTransformers.from(accessRules) -} -def developmentLogging = file('gradle/development-log4j2.xml').toURI().toString() - - -def installRuntimeMods = { - runtimeSources.each { mod, source -> +def prepare = { + runtimeSources.each { name, source -> if (!source.isFile()) { - throw new GradleException("Missing optional ${mod} jar: ${source}. Set -P${mod}RuntimeJar=.") + throw new GradleException('Missing optional ' + name + ' jar. Set -P' + name + 'RuntimeJar=.') } - String expected = runtimeLock.getProperty(mod + '.sha256') - String actual = MessageDigest.getInstance('SHA-256').digest(Files.readAllBytes(source.toPath())) - .encodeHex().toString() - if (!(expected ==~ /(?i)[a-f0-9]{64}/) || !actual.equalsIgnoreCase(expected)) { - throw new GradleException("The optional ${mod} jar does not match gradle/mineralogy-runtime.properties") + String actual = MessageDigest.getInstance('SHA-256').digest(source.bytes).encodeHex().toString() + String expected = lock.getProperty(name + '.sha256') + if (!actual.equalsIgnoreCase(expected)) { + throw new GradleException('The optional ' + name + ' jar does not match gradle/mineralogy-runtime.properties') } } File mods = new File(gameDirectory.asFile, 'mods') - Set managedNames = runtimeSources.values()*.name as Set + Set expected = runtimeSources.values()*.name as Set fileTree(mods).matching { include '*.jar' }.each { candidate -> String name = candidate.name.toLowerCase(Locale.ROOT) - if ((name.startsWith('mineralogy-') || name.startsWith('orespawn-')) && - !managedNames.contains(candidate.name)) { - throw new GradleException("Remove the older optional mod from this profile first: ${candidate}") + if ((name.startsWith('mineralogy-') || name.startsWith('orespawn-')) && !expected.contains(candidate.name)) { + throw new GradleException('Remove the older optional mod from this profile first: ' + candidate) } - if (name.contains('integration-test') || name.contains('-probe')) { - throw new GradleException("The ordinary Mineralogy profile must not contain test mods: ${candidate}") + if (name.contains('integration-test') || name.contains('native-checks')) { + throw new GradleException('The Mineralogy play profile must not contain test mods: ' + candidate) } } - // Copy only the declared jars. Never sync or clear a player's mods, config or saves. + project.stageTerrainContentJars(gameDirectory.asFile) copy { - from runtimeSources.values(), extraMods + from runtimeSources.values() into mods } } -def prepareMineralogyDevelopmentRun = tasks.register('prepareMineralogyDevelopmentRun') { +def prepareMineralogy = tasks.register('prepareMineralogyDevelopmentRun') { group = 'ide' - description = 'Checks and installs the optional Mineralogy/OreSpawn jars in the separate game folder.' - doLast { installRuntimeMods() } + dependsOn 'verifyContentDependencies' + doLast { prepare() } } -def prepareAvailableMineralogyDevelopmentRun = tasks.register('prepareAvailableMineralogyDevelopmentRun') { +def prepareAvailableMineralogy = tasks.register('prepareAvailableMineralogyDevelopmentRun') { group = 'ide' - description = 'Prepares the Eclipse profile when the optional development jars are available.' + dependsOn 'verifyContentDependencies' onlyIf { boolean available = runtimeSources.values().every { it.isFile() } if (!available) { - logger.lifecycle('Mineralogy profiles generated; supply the optional jars before using them. See docs/BUILDING.md.') + logger.lifecycle('Mineralogy runs generated; add the pinned optional jars before using them.') } available } - doLast { installRuntimeMods() } + doLast { prepare() } } -// Reuse the target's normal launcher, mappings, assets and merged production output. -tasks.register('runMineralogyClient', JavaExec) { - group = 'forgegradle runs' - description = 'Runs a separate client with Mineralogy and OreSpawn installed.' - dependsOn prepareMineralogyDevelopmentRun - workingDir gameDirectory - systemProperty 'log4j.configurationFile', developmentLogging - configureVerification(delegate, 'runClient') -} -tasks.register('runMineralogyServer', JavaExec) { - group = 'forgegradle runs' - description = 'Runs a separate server with Mineralogy and OreSpawn installed.' - dependsOn prepareMineralogyDevelopmentRun - workingDir gameDirectory - systemProperty 'log4j.configurationFile', developmentLogging - args '--nogui' - configureVerification(delegate, 'runServer') -} - -def generateMineralogyEclipseRuns = tasks.register('generateMineralogyEclipseRuns') { - group = 'ide' - dependsOn tasks.named('isolateEclipseProductionRuns') - doLast { - ['Client', 'Server'].each { side -> - File source = file("run${side}.launch") - if (!source.isFile()) throw new GradleException('Generate the ordinary Eclipse launches first') - def xml = new groovy.xml.XmlParser(false, false).parse(source) - xml.stringAttribute.find { - it.@key == 'org.eclipse.jdt.launching.WORKING_DIRECTORY' - }.@value = gameDirectory.asFile.absolutePath - def vmArguments = xml.stringAttribute.find { - it.@key == 'org.eclipse.jdt.launching.VM_ARGUMENTS' - } - vmArguments.@value = vmArguments.@value.toString() + ' -Dlog4j.configurationFile=' + developmentLogging - file("runMineralogy${side}.launch").setText(groovy.xml.XmlUtil.serialize(xml).replace('\r\n', '\n'), 'UTF-8') - } +runs { + mineralogyClient { + runType 'client' + workingDirectory gameDirectory + modSource project.sourceSets.main } -} -tasks.named('genEclipseRuns') { - finalizedBy prepareAvailableMineralogyDevelopmentRun, generateMineralogyEclipseRuns -} - -tasks.named('verifyEclipseProductionClasspath') { - doLast { - ['runMineralogyClient.launch', 'runMineralogyServer.launch'].each { name -> - File launch = file(name) - def xml = new groovy.xml.XmlSlurper().parse(launch) - String workingDirectory = xml.stringAttribute.find { - it.@key.text() == 'org.eclipse.jdt.launching.WORKING_DIRECTORY' - }.@value.text() - String contents = launch.getText('UTF-8').replace('\\', '/') - if (file(workingDirectory).canonicalFile != gameDirectory.asFile.canonicalFile || - !contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE') || - contents.contains('bin/test') || contents.contains('src/test') || contents.contains('junit-')) { - throw new GradleException("Mineralogy launch is not isolated from ordinary runs and tests: ${name}") - } - } + mineralogyServer { + runType 'server' + workingDirectory gameDirectory + modSource project.sourceSets.main + argument '--nogui' } } -tasks.register('verifyMineralogyDevelopmentProfile') { - group = 'verification' - dependsOn prepareMineralogyDevelopmentRun, tasks.named('verifyEclipseProductionClasspath') - doLast { - File mods = new File(gameDirectory.asFile, 'mods') - Set expected = (runtimeSources.values() + extraMods.files)*.name as Set - Set actual = fileTree(mods).matching { include '*.jar' }.files*.name as Set - if (actual != expected) { - throw new GradleException("The reference Mineralogy profile has unexpected jars: ${actual - expected}") - } - } -} - -// Verification uses another directory. Probes never enter the manual Eclipse profile. -def probeId = project.ext.mineralogyProfileProbeId -def verificationDirectory = layout.buildDirectory.dir('mineralogy-integration-run') -def prepareMineralogyIntegrationTest = tasks.register('prepareMineralogyIntegrationTest') { - group = 'verification' - dependsOn prepareMineralogyDevelopmentRun, tasks.named('integrationTestModJar') - doLast { - File directory = verificationDirectory.get().asFile - File expected = new File(layout.buildDirectory.get().asFile, 'mineralogy-integration-run') - if (directory.canonicalFile != expected.canonicalFile) { - throw new GradleException('Refusing to clear a Mineralogy verification directory outside the build output') - } - project.delete(directory) - copy { - from new File(gameDirectory.asFile, 'mods') - from tasks.named('integrationTestModJar', Jar).flatMap { it.archiveFile } - into new File(directory, 'mods') - } - new File(directory, 'eula.txt').setText('eula=true\n', 'UTF-8') - ['banned-players.json', 'banned-ips.json', 'ops.json', 'whitelist.json'].each { name -> - new File(directory, name).setText('[]\n', 'UTF-8') - } - new File(directory, 'server.properties').setText('''\ -level-name=world -level-seed=4815162342 -level-type=default -online-mode=false -server-ip=127.0.0.1 -server-port=0 -generate-structures=false -spawn-animals=false -spawn-monsters=false -spawn-protection=0 -max-tick-time=120000 -'''.replace('level-type=default', - probeId == 'skysterrainsmootherbop' ? 'level-type=BIOMESOP' : 'level-type=default'), 'UTF-8') - } -} - -def fresh = tasks.register('integrationMineralogyFreshProcess', JavaExec) { - group = 'verification' - dependsOn prepareMineralogyIntegrationTest - workingDir verificationDirectory - systemProperty probeId + '.integrationPhase', 'fresh' - systemProperty 'log4j.configurationFile', developmentLogging - systemProperty 'skysterrainsmoother.requireMineralogy', 'true' - systemProperty 'skysterrainsmoother.testMineralogyShapes', 'false' - configureVerification(delegate, 'runServer') -} -def reload = tasks.register('integrationMineralogyReloadProcess', JavaExec) { - group = 'verification' - dependsOn fresh - workingDir verificationDirectory - systemProperty probeId + '.integrationPhase', 'reload' - systemProperty 'log4j.configurationFile', developmentLogging - systemProperty 'skysterrainsmoother.requireMineralogy', 'true' - systemProperty 'skysterrainsmoother.testMineralogyShapes', 'false' - configureVerification(delegate, 'runServer') -} - -tasks.register('integrationMineralogyTest') { - group = 'verification' - dependsOn reload - doLast { - File directory = verificationDirectory.get().asFile - File marker = new File(directory, "world/${probeId}-integration.properties") - Properties evidence = new Properties() - if (!marker.isFile()) throw new GradleException('Mineralogy integration marker is missing') - marker.withInputStream { evidence.load(it) } - if (evidence.getProperty('fresh_complete') != 'true' || evidence.getProperty('reload_complete') != 'true') { - throw new GradleException('Mineralogy fresh/reload verification did not complete') - } - if (!fileTree(new File(directory, 'crash-reports')).isEmpty()) { - throw new GradleException('Mineralogy verification produced a crash report') - } - } -} - -tasks.register('integrationMineralogyClientProcess', JavaExec) { - group = 'verification' - dependsOn prepareMineralogyIntegrationTest - workingDir verificationDirectory - systemProperty probeId + '.integrationPhase', 'client' - systemProperty 'log4j.configurationFile', developmentLogging - systemProperty 'skysterrainsmoother.requireMineralogy', 'true' - systemProperty 'skysterrainsmoother.testMineralogyShapes', 'false' - configureVerification(delegate, 'runClient') - doLast { - String marker = probeId == 'skysterrainsmoother' ? - 'terrain-smoother-client-pass.txt' : 'bop-terrain-client-pass.txt' - if (!new File(verificationDirectory.get().asFile, marker).isFile()) { - throw new GradleException('The Mineralogy client did not complete resource verification') - } +tasks.configureEach { + if (name == 'runMineralogyClient' || name == 'runMineralogyServer') { + dependsOn prepareMineralogy } } +tasks.named('eclipse') { dependsOn prepareAvailableMineralogy } diff --git a/gradle/mineralogy-runtime.properties b/gradle/mineralogy-runtime.properties index 1b673d9..01b8df6 100644 --- a/gradle/mineralogy-runtime.properties +++ b/gradle/mineralogy-runtime.properties @@ -1,5 +1,4 @@ -# Optional development runtime only. These jars are not bundled or required by the mod. -mineralogy.path=../../../MinecraftMineralogy/MinecraftMineralogy 112/MinecraftMineralogy/build/libs/Mineralogy-6.0.1.112021.jar -mineralogy.sha256=558AD3403486F3B5CF575494BDF0A36B9F5967EA8D58E94BDBC2C2B3B6AF5D22 -orespawn.path=../../../MinecraftMineralogy/OS112/OreSpawn/build/libs/OreSpawn-4.1.2.112021.jar -orespawn.sha256=50EE5C3453290AB5ECA6A87BD1FEC6F9A5A8DCC1D6F7632B9BFED9B991EA182D +mineralogy.path=../../../MinecraftMineralogy/MinecraftMineralogy 1211NEO/MinecraftMineralogy/build/libs/Mineralogy-6.1.4.121012.jar +mineralogy.sha256=E7A29C978CE751735F0842B89D4ADFC3E5B4A34188C3F41B5B2AE24A94BDCFFD +orespawn.path=../../../MinecraftMineralogy/OS1211NEO/OreSpawn/build/libs/OreSpawn-4.2.0.121012.jar +orespawn.sha256=587873724C20CCD0167E6B016C700BA6F0F46B9F8E0F3B742E05A7ABAB9E5ECD diff --git a/gradle/recipe-advancements.gradle b/gradle/recipe-advancements.gradle index 51bf68d..847d1b2 100644 --- a/gradle/recipe-advancements.gradle +++ b/gradle/recipe-advancements.gradle @@ -28,15 +28,10 @@ def generateRecipeAdvancements = tasks.register('generateRecipeAdvancements') { } criteria.has_the_recipe = [trigger: 'minecraft:recipe_unlocked', conditions: [recipe: recipe]] requirements.add('has_the_recipe') - // A shared parent makes 1.12 recurse through thousands of learned recipes on login. - // Independent hidden unlocks keep the same recipe-book behaviour without that recursion. + // Independent unlocks avoid a long recipe visibility chain on login. Map advancement = [rewards: [recipes: [recipe]], criteria: criteria, requirements: [requirements]] - if (project.mod_id != 'skysgrassslabs') { - // Optional catalogues and recovery recipes must not leave phantom unlocks. - advancement.conditions = [[type: 'skysbuildingpieces:recipe_exists', recipe: recipe]] - } - File target = new File(output, "assets/${owner}/advancements/recipes/${path}.json") + File target = new File(output, "data/${owner}/advancement/recipes/${path}.json") target.parentFile.mkdirs() target.setText(JsonOutput.prettyPrint(JsonOutput.toJson(advancement)) + '\n', 'UTF-8') } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0d4a951..f8e1ee3 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2c68b41..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip -distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index cccdd3d..adff685 --- a/gradlew +++ b/gradlew @@ -1,78 +1,128 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,35 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle index 6576119..4733227 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,12 @@ -plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' } +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + rootProject.name = 'SkysTerrainSmoother' diff --git a/src/bootstrapTest/defaultconfigs/config/fml.toml b/src/bootstrapTest/defaultconfigs/config/fml.toml new file mode 100644 index 0000000..f126de7 --- /dev/null +++ b/src/bootstrapTest/defaultconfigs/config/fml.toml @@ -0,0 +1,32 @@ +#Disables File Watcher. Used to automatically update config if its file has been modified. +disableConfigWatcher = false +#Should we control the window. Disabling this disables new GL features and can be bad for mods that rely on them. +earlyWindowControl = true +#Max threads for early initialization parallelism, -1 is based on processor count +maxThreads = -1 +#Enable NeoForge global version checking +versionCheck = true +#Default config path for servers +defaultConfigPath = "defaultconfigs" +#Disables Optimized DFU client-side - already disabled on servers +disableOptimizedDFU = true +#Early window provider +earlyWindowProvider = "fmlearlywindow" +#Early window width +earlyWindowWidth = 854 +#Early window height +earlyWindowHeight = 480 +#Early window framebuffer scale +earlyWindowFBScale = 1 +#Early window starts maximized +earlyWindowMaximized = false +#Skip specific GL versions, may help with buggy graphics card drivers +earlyWindowSkipGLVersions = [] +#Squir? +earlyWindowSquir = false +#Define dependency overrides below +#Dependency overrides can be used to forcibly remove a dependency constraint from a mod or to force a mod to load AFTER another mod +#Using dependency overrides can cause issues. Use at your own risk. +#Example dependency override for the mod with the id 'targetMod': dependency constraints (incompatibility clauses or restrictive version ranges) against mod 'dep1' are removed, and the mod will now load after the mod 'dep2' +#dependencyOverrides.targetMod = ["-dep1", "+dep2"] +dependencyOverrides = {} diff --git a/src/bootstrapTest/defaultconfigs/config/neoforge-client.toml b/src/bootstrapTest/defaultconfigs/config/neoforge-client.toml new file mode 100644 index 0000000..d3f2e54 --- /dev/null +++ b/src/bootstrapTest/defaultconfigs/config/neoforge-client.toml @@ -0,0 +1,8 @@ +#EXPERIMENTAL: Enable the NeoForge block rendering pipeline - fixes the lighting of custom models. +experimentalForgeLightPipelineEnabled = false +#When enabled, NeoForge will show any warnings that occurred during loading. +showLoadWarnings = true +#Set to true to use a combined DEPTH_STENCIL attachment instead of two separate ones. +useCombinedDepthStencilAttachment = false +#A config option mainly for developers. Logs out configuration values that do not have translations when running a client in a development environment. +logUntranslatedConfigurationWarnings = true diff --git a/src/bootstrapTest/defaultconfigs/config/neoforge-common.toml b/src/bootstrapTest/defaultconfigs/config/neoforge-common.toml new file mode 100644 index 0000000..9e1ac4c --- /dev/null +++ b/src/bootstrapTest/defaultconfigs/config/neoforge-common.toml @@ -0,0 +1,8 @@ +#A config option mainly for developers. Logs out modded item tags that do not have translations when running on integrated server. Format desired is tag.item.. for the translation key. Defaults to SILENCED. +#Allowed Values: SILENCED, DEV_SHORT, DEV_VERBOSE, PROD_SHORT, PROD_VERBOSE +logUntranslatedItemTagWarnings = "SILENCED" +#A config option mainly for developers. Logs out modded tags that are using the 'forge' namespace when running on integrated server. Defaults to DEV_SHORT. +#Allowed Values: SILENCED, DEV_SHORT, DEV_VERBOSE, PROD_SHORT, PROD_VERBOSE +logLegacyTagWarnings = "DEV_SHORT" +#Set this to true to enable showing debug information about attributes on an item when advanced tooltips is on. +attributeAdvancedTooltipDebugInfo = true diff --git a/src/bootstrapTest/defaultconfigs/config/neoforge-server.toml b/src/bootstrapTest/defaultconfigs/config/neoforge-server.toml new file mode 100644 index 0000000..99d6fcc --- /dev/null +++ b/src/bootstrapTest/defaultconfigs/config/neoforge-server.toml @@ -0,0 +1,10 @@ +#Set this to true to remove any BlockEntity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. +removeErroringBlockEntities = false +#Set this to true to remove any Entity (Note: Does not include BlockEntities) that throws an error in its tick method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. +removeErroringEntities = false +#Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticeable differences in mechanics so default is vanilla behavior. Default: false. +fullBoundingBoxLadders = false +#The permission handler used by the server. Defaults to neoforge:default_handler if no such handler with that name is registered. +permissionHandler = "neoforge:default_handler" +#Set this to true to enable advertising the dedicated server to local LAN clients so that it shows up in the Multiplayer screen automatically. +advertiseDedicatedServerToLan = true diff --git a/src/bootstrapTest/java/zone/moddev/mc/skysterrainsmoother/probe/NativeBootstrap.java b/src/bootstrapTest/java/zone/moddev/mc/skysterrainsmoother/probe/NativeBootstrap.java new file mode 100644 index 0000000..9aba7bb --- /dev/null +++ b/src/bootstrapTest/java/zone/moddev/mc/skysterrainsmoother/probe/NativeBootstrap.java @@ -0,0 +1,135 @@ +package zone.moddev.mc.skysterrainsmoother.probe; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Properties; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.bus.api.IEventBus; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.api.TerrainSmoothingApi; +import zone.moddev.mc.skysterrainsmoother.content.SandContent; +import zone.moddev.mc.skysterrainsmoother.content.SandLegacyRecovery; +import zone.moddev.mc.skysterrainsmoother.content.SandSlabBlock; +import zone.moddev.mc.skysterrainsmoother.internal.MaterialCatalogue; + +/** Small native lifecycle and falling-state checks, isolated from production launches. */ +@Mod("terrainbootstrap") +public final class NativeBootstrap { + private boolean setupComplete; + + public NativeBootstrap(IEventBus modBus) { + modBus.addListener(this::setup); + NeoForge.EVENT_BUS.addListener(this::started); + } + + private void setup(FMLCommonSetupEvent event) { + require(TerrainSmoothingApi.hasMaterial(ResourceLocation.parse("minecraft:stone")), + "Built-ins are available from a contributor's native setup callback"); + TerrainSmoothingApi.addBiomeRule((material, biome) -> true); + setupComplete = true; + } + + private void started(ServerStartedEvent event) { + ServerLevel world = event.getServer().getLevel(Level.OVERWORLD); + require(setupComplete && MaterialCatalogue.instance().frozen(), "Native setup completed and froze"); + require(GrassSlabsApi.hasSmoothingOwner(), "Grass generation ownership was claimed"); + boolean lateRejected = false; + try { + TerrainSmoothingApi.addBiomeRule((material, biome) -> true); + } catch (IllegalStateException expected) { + lateRejected = true; + } + require(lateRejected, "Late registration rejected"); + int checks = 3; + for (int metadata = 0; metadata < 4; metadata++) { + BlockState state = SandContent.legacyState(metadata); + Block block = (metadata & 2) == 0 ? SandContent.slab : SandContent.redSlab; + require(state.getBlock() == block && state.getValue(SandSlabBlock.TYPE) + == ((metadata & 1) == 0 ? SlabType.TOP : SlabType.BOTTOM), "Legacy sand orientation"); + require(!state.getValue(SandSlabBlock.WATERLOGGED), "Legacy sand begins dry"); + require(block.getStateDefinition().getPossibleStates().size() == 4, "No double sand state"); + CompoundTag oldStack = new CompoundTag(); + oldStack.putString("id", "skysterrainsmoother:sand_slab"); + oldStack.putByte("Count", (byte) 17); + oldStack.putShort("Damage", (short) metadata); + CompoundTag custom = new CompoundTag(); + custom.putString("Name", "Kept sand"); + oldStack.put("tag", custom); + SandLegacyRecovery.recoverItems(oldStack); + require(oldStack.getString("id").equals(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(block).toString()) + && oldStack.getByte("Count") == 17 && custom.equals(oldStack.getCompound("tag")), + "Raw sand stack retains count and custom data"); + CompoundTag once = oldStack.copy(); + SandLegacyRecovery.recoverItems(oldStack); + require(once.equals(oldStack), "Sand item recovery is idempotent"); + checks += 5; + checks += falling(world, state, metadata, 220); + checks += falling(world, state, metadata, -56); + } + File root = event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); + Properties receipt = new Properties(); + receipt.setProperty("checks", Integer.toString(checks)); + receipt.setProperty("materials", Integer.toString(MaterialCatalogue.instance().size())); + try (FileOutputStream output = new FileOutputStream(new File(root, "native-terrain.properties"))) { + receipt.store(output, "Native Terrain Smoother checks"); + } catch (java.io.IOException failure) { + throw new IllegalStateException("Cannot write native check receipt", failure); + } + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_NATIVE_BOOTSTRAP_PASS checks=" + checks); + event.getServer().halt(false); + } + + private static int falling(ServerLevel world, BlockState state, int metadata, int sourceY) { + BlockPos source = new BlockPos(80 + metadata * 4, sourceY, 80); + world.getChunk(source); + for (int height = sourceY - 5; height <= sourceY + 3; height++) { + world.setBlock(new BlockPos(source.getX(), height, source.getZ()), Blocks.AIR.defaultBlockState(), 2); + } + world.setBlock(source.below(5), Blocks.STONE.defaultBlockState(), 2); + world.setBlock(source, state, 2); + ((SandSlabBlock) state.getBlock()).tick(state, world, source, net.minecraft.util.RandomSource.create(1)); + java.util.List entities = world.getEntitiesOfClass(FallingBlockEntity.class, + new AABB(source).inflate(1)); + require(entities.size() == 1, "Native falling entity created"); + FallingBlockEntity falling = entities.get(0); + for (int tick = 0; tick < 100 && falling.isAlive(); tick++) { + falling.tick(); + } + require(!falling.isAlive() && world.getBlockState(source.below(4)).equals(state), + "Falling preserves native sand colour and orientation"); + world.setBlock(source, state.setValue(SandSlabBlock.WATERLOGGED, true), 2); + ((SandSlabBlock) state.getBlock()).tick(world.getBlockState(source), world, source, net.minecraft.util.RandomSource.create(1)); + entities = world.getEntitiesOfClass(FallingBlockEntity.class, new AABB(source).inflate(1)); + entities.removeIf(entity -> !entity.isAlive()); + require(entities.size() == 1, "Wet sand begins falling"); + FallingBlockEntity wet = entities.get(0); + require(!wet.getBlockState().getValue(SandSlabBlock.WATERLOGGED), "Falling sand carries no source water"); + wet.tick(); + require(world.getFluidState(source).getType() == Fluids.WATER, "Water remains at the source"); + wet.discard(); + return 5; + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +} diff --git a/src/bootstrapTest/resources/META-INF/neoforge.mods.toml b/src/bootstrapTest/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..40ca115 --- /dev/null +++ b/src/bootstrapTest/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,14 @@ +modLoader="javafml" +loaderVersion="[2,)" +license="LGPL-2.1-only" +[[mods]] +modId="terrainbootstrap" +version="1.0" +displayName="Terrain Smoother Native Checks" +description="Build-only native lifecycle checks." +[[dependencies.terrainbootstrap]] +modId="skysterrainsmoother" +type="required" +versionRange="[0.1.0.120062,)" +ordering="AFTER" +side="BOTH" diff --git a/src/bootstrapTest/resources/pack.mcmeta b/src/bootstrapTest/resources/pack.mcmeta new file mode 100644 index 0000000..1728810 --- /dev/null +++ b/src/bootstrapTest/resources/pack.mcmeta @@ -0,0 +1,10 @@ +{ + "pack": { + "pack_format": 48, + "supported_formats": { + "min_inclusive": 34, + "max_inclusive": 48 + }, + "description": "Build-only runtime checks" + } +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java index 86bf1cf..f75f557 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java @@ -1,18 +1,18 @@ package zone.moddev.mc.skysterrainsmoother.probe; import java.util.*; -import net.minecraft.block.state.IBlockState; -import net.minecraft.init.Blocks; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; -import net.minecraft.world.chunk.Chunk; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.world.gen.structure.MapGenStructureData; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.gameevent.TickEvent; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.event.terraingen.DecorateBiomeEvent; -import net.minecraftforge.event.terraingen.PopulateChunkEvent; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.block.RedStoneOreBlock; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.tick.LevelTickEvent; import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; import zone.moddev.mc.skysterrainsmoother.api.TerrainMaterial; @@ -26,18 +26,18 @@ final class CaveProbe { private CaveProbe() { } - static int run(WorldServer world) { + static int run(ServerLevel world) { BlockPos floor = new BlockPos(3000, 70, 3000); - Chunk owner = world.getChunk(floor); + LevelChunk owner = world.getChunkAt(floor); clear(world, floor, 69, 255); fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(!world.isAirBlock(floor.up()), "cave floor below a roof receives a matching slab"); + RuntimeProbe.require(!world.isEmptyBlock(floor.above()), "cave floor below a roof receives a matching slab"); int checks = 1; - TerrainMaterial stone = MaterialCatalogue.instance().source(Blocks.STONE.getDefaultState()); + TerrainMaterial stone = MaterialCatalogue.instance().source(Blocks.STONE.defaultBlockState()); for (int level = 1; level <= 3; level++) { for (int cardinal = 0; cardinal < 16; cardinal++) { @@ -46,22 +46,22 @@ static int run(WorldServer world) { for (int directionIndex = 0; directionIndex < 4; directionIndex++) { if ((cardinal & (1 << directionIndex)) != 0) { - world.setBlockState(floor.add(DX[directionIndex], 1, DZ[directionIndex]), - Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.offset(DX[directionIndex], 1, DZ[directionIndex]), + Blocks.STONE.defaultBlockState(), 2); } if ((diagonal & (1 << directionIndex)) != 0) { - world.setBlockState(floor.add(CX[directionIndex], 1, CZ[directionIndex]), - Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.offset(CX[directionIndex], 1, CZ[directionIndex]), + Blocks.STONE.defaultBlockState(), 2); } } - IBlockState expected = MaterialCatalogue.instance().result(stone, + BlockState expected = MaterialCatalogue.instance().result(stone, Footprint.mask(cardinal, diagonal, level), level); SkysTerrainSmoother.engine.smooth(world, owner, level); RuntimeProbe.require( - world.getBlockState(floor.up()) - .equals(expected == null ? Blocks.AIR.getDefaultState() : expected), + world.getBlockState(floor.above()) + .equals(expected == null ? Blocks.AIR.defaultBlockState() : expected), "roofed footprint " + level + "/" + cardinal + "/" + diagonal); checks++; } @@ -70,58 +70,61 @@ static int run(WorldServer world) { // The same column can contain independent floors at multiple depths. fixture(world, floor, 6); - fixture(world, floor.up(16), 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); - world.setBlockState(floor.up(16).east().up(), Blocks.STONE.getDefaultState(), 2); + fixture(world, floor.above(16), 6); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); + world.setBlock(floor.above(16).east().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 3); - RuntimeProbe.require(!world.isAirBlock(floor.up()) && !world.isAirBlock(floor.up(17)), + RuntimeProbe.require(!world.isEmptyBlock(floor.above()) && !world.isEmptyBlock(floor.above(17)), "stacked cave floors are both smoothed"); checks++; for (int air = 1; air <= 4; air++) { fixture(world, floor, air + 1); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.isAirBlock(floor.up()) == (air < 3), + RuntimeProbe.require(world.isEmptyBlock(floor.above()) == (air < 3), "low passage remains walkable: air height " + air); checks++; } - for (IBlockState obstacle : new IBlockState[] { Blocks.WATER.getDefaultState(), Blocks.LAVA.getDefaultState(), - Blocks.GLASS.getDefaultState(), Blocks.CHEST.getDefaultState() }) { + for (BlockState obstacle : new BlockState[] { Blocks.WATER.defaultBlockState(), Blocks.LAVA.defaultBlockState(), + Blocks.GLASS.defaultBlockState(), Blocks.CHEST.defaultBlockState() }) { for (int height = 1; height <= 3; height++) { fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); - world.setBlockState(floor.up(height), obstacle, 2); + RuntimeProbe.require(world.isEmptyBlock(floor.above()) && world.isEmptyBlock(floor.above(2)) + && world.isEmptyBlock(floor.above(3)), "Cave fixture starts with clear headroom"); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); + world.setBlock(floor.above(height), obstacle, 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); RuntimeProbe.require( - height == 1 ? world.getBlockState(floor.up()).equals(obstacle) : world.isAirBlock(floor.up()), - "fluid, occupied target or blocked headroom is protected"); + height == 1 ? world.getBlockState(floor.above()).equals(obstacle) : world.isEmptyBlock(floor.above()), + "fluid, occupied target or blocked headroom is protected: " + obstacle + " / " + height + + " / actual=" + world.getBlockState(floor.above())); checks++; } } fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); - world.setBlockState(floor.north().up(2), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); + world.setBlock(floor.north().above(2), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.isAirBlock(floor.up()), "solid cave wall is not a floor transition"); + RuntimeProbe.require(world.isEmptyBlock(floor.above()), "solid cave wall is not a floor transition"); checks++; // An open column must still see a higher floor hidden under its neighbour's roof. fixture(world, floor, 6); - world.setBlockToAir(floor.up(6)); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + ProbeWorld.clear(world, floor.above(6)); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(!world.isAirBlock(floor.up()), "cave entrance and outdoor floor join consistently"); + RuntimeProbe.require(!world.isEmptyBlock(floor.above()), "cave entrance and outdoor floor join consistently"); checks++; - for (IBlockState source : new IBlockState[] { Blocks.COAL_ORE.getDefaultState(), - Blocks.GRAVEL.getDefaultState(), Blocks.PLANKS.getDefaultState(), Blocks.MONSTER_EGG.getDefaultState(), - Blocks.CHEST.getDefaultState() }) { + for (BlockState source : new BlockState[] { Blocks.COAL_ORE.defaultBlockState(), + Blocks.GRAVEL.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState(), Blocks.INFESTED_STONE.defaultBlockState(), + Blocks.CHEST.defaultBlockState() }) { fixture(world, floor, 6); - world.setBlockState(floor, source, 2); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor, source, 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.isAirBlock(floor.up()) && world.getBlockState(floor).equals(source), + RuntimeProbe.require(world.isEmptyBlock(floor.above()) && world.getBlockState(floor).equals(source), "unregistered provider terrain, ores and tile sources remain intact"); checks++; } @@ -133,12 +136,12 @@ static int run(WorldServer world) { || material.mineralogy && !SkysTerrainSmoother.mineralogy) { continue; } - for (IBlockState source : material.sources) { + for (BlockState source : material.sources) { fixture(world, floor, 6); - world.setBlockState(floor, source, 2); - world.setBlockState(floor.north().up(), source, 2); + world.setBlock(floor, source, 2); + world.setBlock(floor.north().above(), source, 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.getBlockState(floor.up()).equals(material.slab), + RuntimeProbe.require(world.getBlockState(floor.above()).equals(material.slab), "cave uses the exact registered material: " + material.id); RuntimeProbe .require( @@ -146,7 +149,7 @@ static int run(WorldServer world) { .equals(material.supportAfterPlacement == null ? source : material.supportAfterPlacement), "native support identity is preserved or dirtified"); - RuntimeProbe.require(world.getBlockState(floor.up(6)).getBlock() == Blocks.BRICK_BLOCK, + RuntimeProbe.require(world.getBlockState(floor.above(6)).getBlock() == Blocks.BRICKS, "cave ceiling remains intact"); checks += 3; @@ -157,87 +160,72 @@ static int run(WorldServer world) { } // Structure bounds and add-on vetoes apply to underground floors as well. - MapGenStructureData data = (MapGenStructureData) world.getPerWorldStorage() - .getOrLoadData(MapGenStructureData.class, "Mineshaft"); - - if (data == null) { - data = new MapGenStructureData("Mineshaft"); - world.getPerWorldStorage().setData("Mineshaft", data); - } - - String key = MapGenStructureData.formatChunkCoords(owner.x, owner.z); - NBTTagCompound previous = data.getTagCompound().hasKey(key) ? data.getTagCompound().getCompoundTag(key).copy() - : null; - + var previousStarts = new java.util.HashMap<>(owner.getAllStarts()); + BoundingBox protectedBounds = new BoundingBox(floor.getX() - 2, 69, floor.getZ() - 2, + floor.getX() + 2, 80, floor.getZ() + 2); + var mineshaft = world.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.STRUCTURE) + .getOrThrow(net.minecraft.world.level.levelgen.structure.BuiltinStructures.MINESHAFT); + StructureStart protectedStart = protectedStart(owner, mineshaft, protectedBounds); + owner.setStartForStructure(mineshaft, protectedStart); try { - NBTTagCompound bounds = new NBTTagCompound(); - - bounds.setIntArray("BB", - new int[] { floor.getX() - 2, 69, floor.getZ() - 2, floor.getX() + 2, 80, floor.getZ() + 2 }); - data.writeInstance(bounds, owner.x, owner.z); fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.isAirBlock(floor.up()), "mineshaft structure bounds are protected underground"); + RuntimeProbe.require(world.isEmptyBlock(floor.above()), "Native mineshaft bounds protect underground floors"); checks++; } finally { - if (previous == null) { - data.getTagCompound().removeTag(key); - } else { - data.getTagCompound().setTag(key, previous); - } + owner.setAllStarts(previousStarts); } BlockPos veto = new BlockPos(floor.getX(), 240, floor.getZ()); fixture(world, veto, 6); - world.setBlockState(veto.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(veto.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.isAirBlock(veto.up()), "underground add-on area veto"); + RuntimeProbe.require(world.isEmptyBlock(veto.above()), "underground add-on area veto"); checks++; checks += lateDecoration(world, floor, owner); checks += oreDecoration(world, floor, owner); checks += manyFloors(world); - System.out.println("TERRAIN_SMOOTHER_CAVE_MATERIALS_PASS mineralogy=" + mineralogy); - System.out.println("TERRAIN_SMOOTHER_CAVE_PASS checks=" + checks); + checks += nativeHeightRange(world); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CAVE_MATERIALS_PASS mineralogy=" + mineralogy); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CAVE_PASS checks=" + checks); return checks; } - private static int lateDecoration(WorldServer world, BlockPos floor, Chunk owner) { + private static int lateDecoration(ServerLevel world, BlockPos floor, LevelChunk owner) { int checks = 0; for (int kind = 0; kind < 4; kind++) { fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - MinecraftForge.EVENT_BUS.post( - new DecorateBiomeEvent.Post(world, new Random(1), new BlockPos(owner.x << 4, 0, owner.z << 4))); - RuntimeProbe.require(!world.isAirBlock(floor.up()), + ProbeWorld.repair(world, new BlockPos(owner.getPos().x << 4, 0, owner.getPos().z << 4)); + RuntimeProbe.require(!world.isEmptyBlock(floor.above()), "decorated cave cap retained until all later generators finish"); - MinecraftForge.EVENT_BUS.post(new PopulateChunkEvent.Post(world.getChunkProvider().chunkGenerator, world, - new Random(1), owner.x, owner.z, false)); + ProbeWorld.queue(world, owner.getPos().x, owner.getPos().z); if (kind == 0) { - world.setBlockState(floor, Blocks.MONSTER_EGG.getDefaultState(), 2); + world.setBlock(floor, Blocks.INFESTED_STONE.defaultBlockState(), 2); } if (kind == 1) { - world.setBlockState(floor, Blocks.SAND.getDefaultState(), 2); + world.setBlock(floor, Blocks.SAND.defaultBlockState(), 2); } if (kind == 2) { - world.setBlockState(floor.up(2), Blocks.BRICK_BLOCK.getDefaultState(), 2); + world.setBlock(floor.above(2), Blocks.BRICKS.defaultBlockState(), 2); } if (kind == 3) { - world.setBlockState(floor, Blocks.SAND.getDefaultState(), 2); - world.setBlockState(floor.up(), Blocks.GLASS.getDefaultState(), 2); + world.setBlock(floor, Blocks.SAND.defaultBlockState(), 2); + world.setBlock(floor.above(), Blocks.GLASS.defaultBlockState(), 2); } - MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(Side.SERVER, TickEvent.Phase.END, world)); + NeoForge.EVENT_BUS.post(new LevelTickEvent.Post(() -> true, world)); RuntimeProbe.require( kind == 1 - ? world.getBlockState(floor.up()) + ? world.getBlockState(floor.above()) .getBlock() == zone.moddev.mc.skysterrainsmoother.content.SandContent.slab - : kind == 3 ? world.getBlockState(floor.up()).getBlock() == Blocks.GLASS - : world.isAirBlock(floor.up()), + : kind == 3 ? world.getBlockState(floor.above()).getBlock() == Blocks.GLASS + : world.isEmptyBlock(floor.above()), "late infested stone, sand, low ceiling and provider-owned replacement are handled without excavation"); checks += 2; } @@ -245,27 +233,25 @@ private static int lateDecoration(WorldServer world, BlockPos floor, Chunk owner return checks; } - private static int oreDecoration(WorldServer world, BlockPos floor, Chunk owner) { + private static int oreDecoration(ServerLevel world, BlockPos floor, LevelChunk owner) { int checks = 0; - for (IBlockState ore : new IBlockState[] { Blocks.COAL_ORE.getDefaultState(), Blocks.IRON_ORE.getDefaultState(), - Blocks.REDSTONE_ORE.getDefaultState(), Blocks.LIT_REDSTONE_ORE.getDefaultState(), - RuntimeProbe.probeOre.getDefaultState() }) { + for (BlockState ore : new BlockState[] { Blocks.COAL_ORE.defaultBlockState(), Blocks.IRON_ORE.defaultBlockState(), + Blocks.REDSTONE_ORE.defaultBlockState(), Blocks.REDSTONE_ORE.defaultBlockState().setValue(RedStoneOreBlock.LIT, true), + RuntimeProbe.probeOre.defaultBlockState() }) { for (int level = 1; level <= 3; level++) { fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, level); - IBlockState cap = world.getBlockState(floor.up()); + BlockState cap = world.getBlockState(floor.above()); RuntimeProbe.require(cap.getBlock() != Blocks.AIR, "ore regression starts with a generated piece"); - world.setBlockState(floor, ore, 2); - MinecraftForge.EVENT_BUS.post( - new DecorateBiomeEvent.Post(world, new Random(1), new BlockPos(owner.x << 4, 0, owner.z << 4))); - MinecraftForge.EVENT_BUS.post(new PopulateChunkEvent.Post(world.getChunkProvider().chunkGenerator, - world, new Random(1), owner.x, owner.z, false)); - MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(Side.SERVER, TickEvent.Phase.END, world)); - RuntimeProbe.require(world.getBlockState(floor.up()).equals(cap), + world.setBlock(floor, ore, 2); + ProbeWorld.repair(world, new BlockPos(owner.getPos().x << 4, 0, owner.getPos().z << 4)); + ProbeWorld.queue(world, owner.getPos().x, owner.getPos().z); + NeoForge.EVENT_BUS.post(new LevelTickEvent.Post(() -> true, world)); + RuntimeProbe.require(world.getBlockState(floor.above()).equals(cap), "ore under a generated piece stays hidden: " + ore + " / level " + level); RuntimeProbe.require(world.getBlockState(floor).equals(ore), "ore support is never overwritten"); checks += 3; @@ -273,77 +259,137 @@ private static int oreDecoration(WorldServer world, BlockPos floor, Chunk owner) } fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - world.setBlockState(floor, Blocks.COAL_ORE.getDefaultState(), 2); - world.setBlockState(floor.up(2), Blocks.BRICK_BLOCK.getDefaultState(), 2); - MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(Side.SERVER, TickEvent.Phase.END, world)); - RuntimeProbe.require(world.isAirBlock(floor.up()) && world.getBlockState(floor).getBlock() == Blocks.COAL_ORE, + world.setBlock(floor, Blocks.COAL_ORE.defaultBlockState(), 2); + world.setBlock(floor.above(2), Blocks.BRICKS.defaultBlockState(), 2); + NeoForge.EVENT_BUS.post(new LevelTickEvent.Post(() -> true, world)); + RuntimeProbe.require(world.isEmptyBlock(floor.above()) && world.getBlockState(floor).getBlock() == Blocks.COAL_ORE, "ore preservation does not override cave headroom protection"); checks++; fixture(world, floor, 6); - world.setBlockState(floor.north().up(), Blocks.COAL_ORE.getDefaultState(), 2); - world.setBlockState(floor.north().up(2), - MaterialCatalogue.instance().source(Blocks.STONE.getDefaultState()).slab, 2); + world.setBlock(floor.north().above(), Blocks.COAL_ORE.defaultBlockState(), 2); + world.setBlock(floor.north().above(2), + MaterialCatalogue.instance().source(Blocks.STONE.defaultBlockState()).slab, 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(!world.isAirBlock(floor.up()), + RuntimeProbe.require(!world.isEmptyBlock(floor.above()), "neighbouring retained ore cap still represents a higher logical floor"); checks++; - System.out.println("TERRAIN_SMOOTHER_ORE_SUPPORT_PASS checks=" + checks); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_ORE_SUPPORT_PASS checks=" + checks); + return checks; + } + + private static StructureStart protectedStart(LevelChunk owner, + net.minecraft.world.level.levelgen.structure.Structure structure, BoundingBox bounds) { + net.minecraft.world.level.levelgen.structure.StructurePiece piece = + new net.minecraft.world.level.levelgen.structure.StructurePiece( + net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType.MINE_SHAFT_ROOM, + 0, bounds) { + @Override + protected void addAdditionalSaveData( + net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext context, + net.minecraft.nbt.CompoundTag tag) { + } + + @Override + public void postProcess(net.minecraft.world.level.WorldGenLevel world, + net.minecraft.world.level.StructureManager structures, + net.minecraft.world.level.chunk.ChunkGenerator generator, + net.minecraft.util.RandomSource random, BoundingBox clip, + net.minecraft.world.level.ChunkPos chunk, BlockPos position) { + } + }; + return new StructureStart(structure, + owner.getPos(), 0, + new net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer(java.util.List.of(piece))); + } + + private static int nativeHeightRange(ServerLevel world) { + RuntimeProbe.require(world.getMinBuildHeight() == -64 && world.getMaxBuildHeight() == 320, + "The native Overworld exposes the complete 1.18 height range"); + BlockPos centre = new BlockPos(3020, 0, 3020); + LevelChunk owner = world.getChunkAt(centre); + TerrainMaterial stone = MaterialCatalogue.instance().source(Blocks.STONE.defaultBlockState()); + int[] floors = { -56, -24, 48, 264, 312 }; + int checks = 0; + for (int level = 1; level <= 3; level++) { + clear(world, centre, world.getMinBuildHeight(), world.getMaxBuildHeight() - 1); + for (int floorY : floors) { + BlockPos floor = centre.atY(floorY); + fixture(world, floor, 6); + world.setBlock(floor.north().above(), Blocks.STONE.defaultBlockState(), 2); + } + ProbeWorld.rebuildHeights(owner); + int loadedBefore = world.getChunkSource().getLoadedChunksCount(); + SkysTerrainSmoother.engine.smooth(world, owner, level); + BlockState expected = MaterialCatalogue.instance().result(stone, Footprint.mask(1, 0, level), level); + for (int floorY : floors) { + BlockPos floor = centre.atY(floorY); + RuntimeProbe.require(world.getBlockState(floor.above()).equals(expected), + "Matching cave transition at native height " + floorY + " / level " + level); + RuntimeProbe.require(world.getBlockState(floor.above(6)).is(Blocks.BRICKS), + "Height-range smoothing keeps the cave roof intact"); + checks++; + } + RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loadedBefore, + "Height-range smoothing does not load neighbouring chunks"); + } + SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_HEIGHT_RANGE_PASS checks={} min=-64 max=320 levels=3", checks); return checks; } - private static int manyFloors(WorldServer world) { - Chunk owner = world.getChunk(200, 200); - net.minecraft.world.chunk.storage.ExtendedBlockStorage[] sections = owner.getBlockStorageArray(); + private static int manyFloors(ServerLevel world) { + LevelChunk owner = world.getChunk(200, 200); + net.minecraft.world.level.chunk.LevelChunkSection[] sections = owner.getSections(); - for (int section = 0; section < 16; section++) { - sections[section] = new net.minecraft.world.chunk.storage.ExtendedBlockStorage(section * 16, true); + for (int section = 0; section < sections.length; section++) { + sections[section] = ProbeWorld.emptySection(world); } for (int blockZ = 0; blockZ < 16; blockZ++) { for (int blockX = 0; blockX < 16; blockX++) { for (int blockY = 32; blockY < 232; blockY += 8) { int floorY = blockY + ((blockX + blockZ) & 1); - sections[floorY >> 4].set(blockX, floorY & 15, blockZ, Blocks.STONE.getDefaultState()); + sections[owner.getSectionIndex(floorY)].setBlockState(blockX, floorY & 15, blockZ, Blocks.STONE.defaultBlockState()); } - sections[15].set(blockX, 246 & 15, blockZ, Blocks.BRICK_BLOCK.getDefaultState()); - owner.getHeightMap()[blockX | blockZ << 4] = 247; + sections[owner.getSectionIndex(246)].setBlockState(blockX, 246 & 15, blockZ, Blocks.BRICKS.defaultBlockState()); + } } - int countBefore = world.getChunkProvider().getLoadedChunkCount(); + int countBefore = world.getChunkSource().getLoadedChunksCount(); + ProbeWorld.rebuildHeights(owner); int placed = SkysTerrainSmoother.engine.smooth(world, owner, 1); RuntimeProbe.require(placed > 256, "decision buffer covers multiple floors across all 256 columns"); - RuntimeProbe.require(world.getChunkProvider().getLoadedChunkCount() == countBefore, + RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == countBefore, "cave scan never loads a halo chunk"); int second = SkysTerrainSmoother.engine.smooth(world, owner, 1); RuntimeProbe.require(second == 0, "repeated floor scan never stacks new caps"); - System.out.println("TERRAIN_SMOOTHER_CAVE_BUFFER_PASS placed=" + placed); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CAVE_BUFFER_PASS placed=" + placed); return 3; } - private static void clear(WorldServer world, BlockPos floor, int minY, int maxY) { + private static void clear(ServerLevel world, BlockPos floor, int minY, int maxY) { for (int blockZ = -2; blockZ <= 2; blockZ++) { for (int blockX = -2; blockX <= 2; blockX++) { for (int blockY = minY; blockY <= maxY; blockY++) { - world.setBlockToAir(new BlockPos(floor.getX() + blockX, blockY, floor.getZ() + blockZ)); + ProbeWorld.clear(world, new BlockPos(floor.getX() + blockX, blockY, floor.getZ() + blockZ)); } } } } - private static void fixture(WorldServer world, BlockPos floor, int roofOffset) { + private static void fixture(ServerLevel world, BlockPos floor, int roofOffset) { clear(world, floor, floor.getY(), floor.getY() + 8); for (int blockZ = -2; blockZ <= 2; blockZ++) { for (int blockX = -2; blockX <= 2; blockX++) { - world.setBlockState(floor.add(blockX, 0, blockZ), Blocks.STONE.getDefaultState(), 2); - world.setBlockState(floor.add(blockX, roofOffset, blockZ), Blocks.BRICK_BLOCK.getDefaultState(), 2); + world.setBlock(floor.offset(blockX, 0, blockZ), Blocks.STONE.defaultBlockState(), 2); + world.setBlock(floor.offset(blockX, roofOffset, blockZ), Blocks.BRICKS.defaultBlockState(), 2); } } } diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java index 9656a95..e8a57a6 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java @@ -2,105 +2,188 @@ import java.io.*; import net.minecraft.client.Minecraft; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; public final class ClientProbe extends ProbeProxy { private boolean ran; private boolean integrated; + private boolean resourcesChecked; + private int settledWorldTicks; + private long leaveAfter; @Override public void init() { - net.minecraftforge.client.model.ModelLoader.setCustomModelResourceLocation( - net.minecraft.item.Item.getItemFromBlock(RuntimeProbe.probeOre), 0, - new net.minecraft.client.renderer.block.model.ModelResourceLocation("minecraft:stone", "inventory")); - net.minecraftforge.client.model.ModelLoader.setCustomStateMapper(RuntimeProbe.probeOre, - new net.minecraft.client.renderer.block.statemap.StateMapperBase() { - @Override - protected net.minecraft.client.renderer.block.model.ModelResourceLocation getModelResourceLocation( - net.minecraft.block.state.IBlockState state) { - return new net.minecraft.client.renderer.block.model.ModelResourceLocation("minecraft:stone", - "normal"); - } - }); - MinecraftForge.EVENT_BUS.register(this); + NeoForge.EVENT_BUS.register(this); } @SubscribeEvent - public void tick(TickEvent.ClientTickEvent event) { - Minecraft minecraft = Minecraft.getMinecraft(); + public void tick(ClientTickEvent.Post event) { + Minecraft minecraft = Minecraft.getInstance(); + + org.lwjgl.glfw.GLFW.glfwHideWindow(minecraft.getWindow().getWindow()); - if (event.phase != TickEvent.Phase.END) { + if (leaveAfter != 0) { + if (System.nanoTime() >= leaveAfter) { + RuntimeProbe.require(minecraft.isPaused() && minecraft.level != null, + "The disposable world is paused before leaving"); + // Leave through the same save path as the pause menu. + minecraft.level.disconnect(); + minecraft.disconnect(new net.minecraft.client.gui.screens.GenericMessageScreen( + net.minecraft.network.chat.Component.translatable("menu.savingLevel"))); + RuntimeProbe.require(minecraft.level == null, "The client left the disposable world"); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info( + "TERRAIN_SMOOTHER_CLIENT_DISCONNECT_PASS"); + minecraft.stop(); + } return; } + if (ran) { - if (integrated && ClientStatus.complete) { - System.out.println("TERRAIN_SMOOTHER_CLIENT_WORLD_PASS"); - minecraft.shutdown(); + if (integrated) confirmDisposableWorld(minecraft); + if (integrated && ClientStatus.complete && minecraft.level != null && minecraft.player != null + && minecraft.getConnection() != null + && !minecraft.getConnection().getRecipeManager().getRecipes().isEmpty()) { + if (++settledWorldTicks < 20) return; + RecipePreviewModels.check(minecraft); + checkBiomeTints(minecraft); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CLIENT_WORLD_PASS"); integrated = false; + // Pause world ticks long enough for the local connection to finish sending chunk data. + minecraft.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)); + leaveAfter = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(5); } return; } - if (minecraft.currentScreen == null) { + if (minecraft.screen == null || minecraft.getOverlay() != null) { return; } - ran = true; - try { - MineralogyProbe.run(); - FineGrassModelProbe.run(minecraft); - ConfigScreenProbe.run(minecraft); - checkModel(minecraft, RuntimeProbe.probeOre.getDefaultState()); - - for (net.minecraft.block.state.IBlockState state : zone.moddev.mc.skysgrassslabs.init.ModBlocks.GRASS_SLAB - .getBlockState().getValidStates()) { - checkModel(minecraft, state); - } - for (zone.moddev.mc.skysbuildingpieces.content.PieceBlock block : zone.moddev.mc.skysbuildingpieces.content.Pieces.BLOCKS - .values()) { - for (net.minecraft.block.state.IBlockState state : block.getBlockState().getValidStates()) { - if (state.getValue(zone.moddev.mc.skysbuildingpieces.content.PieceBlock.META) - / block.palette.shape.states >= block.palette.materials.size()) { - continue; - } + if (!resourcesChecked) { + MineralogyProbe.run(); + FineGrassModelProbe.run(minecraft); + checkModel(minecraft, RuntimeProbe.probeOre.defaultBlockState()); + for (net.minecraft.world.level.block.Block block : new net.minecraft.world.level.block.Block[] { + zone.moddev.mc.skysterrainsmoother.content.SandContent.slab, + zone.moddev.mc.skysterrainsmoother.content.SandContent.redSlab }) { + for (net.minecraft.world.level.block.state.BlockState state : block.getStateDefinition().getPossibleStates()) { + checkModel(minecraft, state); + } + } + for (net.minecraft.world.level.block.state.BlockState state : zone.moddev.mc.skysgrassslabs.init.ModBlocks.GRASS_SLAB.get() + .getStateDefinition().getPossibleStates()) { checkModel(minecraft, state); } + for (zone.moddev.mc.skysbuildingpieces.content.PieceBlock block : zone.moddev.mc.skysbuildingpieces.content.Pieces.BLOCKS + .values()) { + for (net.minecraft.world.level.block.state.BlockState state : block.getStateDefinition().getPossibleStates()) { + checkModel(minecraft, state); + } + } + resourcesChecked = true; + ConfigScreenProbe.begin(minecraft); + return; } - try (Writer writer = new FileWriter(new File(minecraft.gameDir, "terrain-smoother-client-pass.txt"))) { + if (!ConfigScreenProbe.tick(minecraft)) return; + ran = true; + try (Writer writer = new FileWriter(new File(minecraft.gameDirectory, "terrain-smoother-client-pass.txt"))) { writer.write("TERRAIN_SMOOTHER_CLIENT_PASS\n"); } - System.out.println("TERRAIN_SMOOTHER_CLIENT_PASS"); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CLIENT_PASS"); } catch (IOException failure) { throw new IllegalStateException(failure); } if (System.getProperty("skysterrainsmoother.integrationPhase", "").startsWith("client-")) { integrated = true; - minecraft.launchIntegratedServer("terrain-smoother-world", "Terrain Smoother Test", - new net.minecraft.world.WorldSettings(4815162342L, net.minecraft.world.GameType.CREATIVE, true, - false, net.minecraft.world.WorldType.DEFAULT)); + if (new File(minecraft.gameDirectory, "saves/terrain-smoother-world/level.dat").isFile()) { + minecraft.createWorldOpenFlows().openWorld("terrain-smoother-world", + () -> minecraft.setScreen(new net.minecraft.client.gui.screens.TitleScreen())); + } else { + net.minecraft.world.level.LevelSettings settings = new net.minecraft.world.level.LevelSettings( + "Terrain Smoother Test", net.minecraft.world.level.GameType.CREATIVE, false, + net.minecraft.world.Difficulty.NORMAL, true, new net.minecraft.world.level.GameRules(), + net.minecraft.world.level.WorldDataConfiguration.DEFAULT); + minecraft.createWorldOpenFlows().createFreshLevel("terrain-smoother-world", settings, + new net.minecraft.world.level.levelgen.WorldOptions(4815162342L, false, false), + net.minecraft.world.level.levelgen.presets.WorldPresets::createNormalWorldDimensions, minecraft.screen); + } + } else { + minecraft.stop(); + } + } + + private static void confirmDisposableWorld(Minecraft client) { + if (!new File(client.gameDirectory, "terrain-smoother-client-pass.txt").isFile() + || !System.getProperty("skysterrainsmoother.integrationPhase", "").startsWith("client-")) { + return; + } + net.minecraft.client.gui.screens.Screen screen = client.screen; + if (screen == null || !(screen.getTitle().getContents() + instanceof net.minecraft.network.chat.contents.TranslatableContents title) + || !title.getKey().equals("selectWorld.backupQuestion.experimental")) { + return; + } + net.minecraft.network.chat.Component proceed; + if (screen instanceof net.minecraft.client.gui.screens.BackupConfirmScreen) { + proceed = net.minecraft.network.chat.Component.translatable("selectWorld.backupJoinSkipButton"); + } else if (screen instanceof net.minecraft.client.gui.screens.ConfirmScreen) { + proceed = net.minecraft.network.chat.CommonComponents.GUI_PROCEED; } else { - minecraft.shutdown(); + return; + } + for (net.minecraft.client.gui.components.events.GuiEventListener child : screen.children()) { + if (child instanceof net.minecraft.client.gui.components.Button) { + net.minecraft.client.gui.components.Button button = + (net.minecraft.client.gui.components.Button) child; + if (button.getMessage().equals(proceed)) { + // Only the marked probe's disposable world reaches this confirmation. + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_NATIVE_EXPERIMENTAL_WORLD_CONFIRMATION"); + button.onPress(); + return; + } + } + } + throw new AssertionError("The native experimental-world confirmation has no proceed action"); + } + + private static void checkBiomeTints(Minecraft client) { + net.minecraft.core.BlockPos sample = client.player.blockPosition(); + int expected = client.getBlockColors().getColor(net.minecraft.world.level.block.Blocks.GRASS_BLOCK.defaultBlockState(), + client.level, sample, 0); + int count = 0; + for (zone.moddev.mc.skysbuildingpieces.content.PieceBlock block : + zone.moddev.mc.skysbuildingpieces.content.Pieces.BLOCKS.values()) { + if (block.palette.group.equals("grass")) { + RuntimeProbe.require(client.getBlockColors().getColor(block.defaultBlockState(), client.level, sample, 0) + == expected, "Fine grass uses the actual client biome tint"); + count++; + } } + RuntimeProbe.require(count > 0, "Grass pieces reached the colour checks"); + RuntimeProbe.require(client.getBlockColors().getColor( + zone.moddev.mc.skysgrassslabs.init.ModBlocks.GRASS_SLAB.get().defaultBlockState(), client.level, sample, 0) + == expected, "Grass slabs retain the actual client biome tint"); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CLIENT_TINT_PASS palettes=" + count); } - private static void checkModel(Minecraft minecraft, net.minecraft.block.state.IBlockState state) { - net.minecraft.client.renderer.block.model.IBakedModel model = minecraft.getBlockRendererDispatcher() - .getModelForState(state); + private static void checkModel(Minecraft minecraft, net.minecraft.world.level.block.state.BlockState state) { + net.minecraft.client.resources.model.BakedModel model = minecraft.getBlockRenderer() + .getBlockModel(state); - RuntimeProbe.require(model != minecraft.getBlockRendererDispatcher().getBlockModelShapes().getModelManager() + RuntimeProbe.require(model != minecraft.getBlockRenderer().getBlockModelShaper().getModelManager() .getMissingModel(), "missing model " + state); - for (net.minecraft.util.EnumFacing face : new net.minecraft.util.EnumFacing[] { null, - net.minecraft.util.EnumFacing.DOWN, net.minecraft.util.EnumFacing.UP, - net.minecraft.util.EnumFacing.NORTH, net.minecraft.util.EnumFacing.SOUTH, - net.minecraft.util.EnumFacing.WEST, net.minecraft.util.EnumFacing.EAST }) { - for (net.minecraft.client.renderer.block.model.BakedQuad quad : model.getQuads(state, face, 0)) { - RuntimeProbe.require(!quad.getSprite().getIconName().contains("missingno"), "missing texture " + state); + for (net.minecraft.core.Direction face : new net.minecraft.core.Direction[] { null, + net.minecraft.core.Direction.DOWN, net.minecraft.core.Direction.UP, + net.minecraft.core.Direction.NORTH, net.minecraft.core.Direction.SOUTH, + net.minecraft.core.Direction.WEST, net.minecraft.core.Direction.EAST }) { + for (net.minecraft.client.renderer.block.model.BakedQuad quad : model.getQuads(state, face, net.minecraft.util.RandomSource.create(0))) { + RuntimeProbe.require(!quad.getSprite().contents().name().toString().contains("missingno"), "missing texture " + state); } } } diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ConfigScreenProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ConfigScreenProbe.java index 0cdc543..e3c464e 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ConfigScreenProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ConfigScreenProbe.java @@ -1,91 +1,209 @@ package zone.moddev.mc.skysterrainsmoother.probe; +import java.io.File; +import java.util.ArrayList; +import java.util.List; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.resources.I18n; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.fml.client.FMLClientHandler; -import net.minecraftforge.fml.client.IModGuiFactory; -import net.minecraftforge.fml.client.config.GuiConfig; -import net.minecraftforge.fml.client.event.ConfigChangedEvent; -import net.minecraftforge.fml.common.Loader; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.resources.language.I18n; import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; -import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; import zone.moddev.mc.skysterrainsmoother.config.TerrainSmootherConfig; -/** Opens the actual registered screens inside the disposable client, without taking desktop control. */ +/** Opens, renders and saves the native settings screens, preserving the running session. */ final class ConfigScreenProbe { - static void run(Minecraft minecraft) { - ModMenuProbe.run(minecraft); - for (String modId : new String[] { "skysgrassslabs", "skysterrainsmoother" }) { - IModGuiFactory factory = FMLClientHandler.instance() - .getGuiFactoryFor(Loader.instance().getIndexedModList().get(modId)); - RuntimeProbe.require(factory != null, "Mods list has a config factory for " + modId); - RuntimeProbe.require(factory.hasConfigGui(), "Config button is enabled for " + modId); - GuiScreen screen = factory.createConfigGui(minecraft.currentScreen); - net.minecraft.client.gui.ScaledResolution resolution = new net.minecraft.client.gui.ScaledResolution(minecraft); - screen.setWorldAndResolution(minecraft, resolution.getScaledWidth(), resolution.getScaledHeight()); - GuiConfig configScreen = (GuiConfig) screen; - int expectedEntries = modId.equals("skysgrassslabs") ? 2 : 3; - RuntimeProbe.require(configScreen.configElements.size() == expectedEntries - && configScreen.entryList.listEntries.size() == expectedEntries - && configScreen.allRequireMcRestart, "all settings have widgets and require restart"); - for (net.minecraftforge.fml.client.config.IConfigElement element : configScreen.configElements) { - RuntimeProbe.require(!I18n.format(element.getLanguageKey()).equals(element.getLanguageKey()), - "setting has a readable translated label: " + element.getName()); + private static int screenshotNumber; + private static Screen parent; + private static Screen screen; + private static int modIndex; + private static int choiceIndex; + private static int stage; + private static int settleTicks; + private static int tooltips; + private static long deadline; + private static boolean activeGrass; + private static boolean activeTerrain; + private static int activeLevel; + private static List original; + private static String[] keys; + + static void begin(Minecraft minecraft) { + parent = minecraft.screen; + activeGrass = SkysGrassSlabsConfig.generateGrassSlabs(); + activeTerrain = TerrainSmootherConfig.enabled(); + activeLevel = TerrainSmootherConfig.level(); + modIndex = 0; + tooltips = 0; + stage = 0; + } + + static boolean tick(Minecraft minecraft) { + if (modIndex == 2) { + minecraft.setScreen(parent); + RuntimeProbe.require(tooltips == 6, "All six settings were exercised"); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("SKY_CONFIG_SCREENS_PASS factories=2 widgets=6 restart_safe=true"); + return true; + } + String modId = modIndex == 0 ? "skysgrassslabs" : "skysterrainsmoother"; + if (stage == 0) { + screen = ModMenuProbe.open(minecraft, modId, parent); + keys = modIndex == 0 ? new String[] { "generateGrassSlabs", "forceReplaceBuildingBricksSlabs", + "forceReplaceGrassSlabsModContent" } + : new String[] { "enabled", "smoothingLevel", "enableMineralogy" }; + RuntimeProbe.require(buttons(screen).size() == keys.length + 1, "Each setting has its own control"); + original = new ArrayList<>(); + for (int index = 0; index < keys.length; index++) { + original.add(buttons(screen).get(index).getMessage().getString()); + } + choiceIndex = 0; + stage = 1; + } else if (stage == 1) { + Button button = buttons(screen).get(choiceIndex); + String key = "config." + modId + "." + keys[choiceIndex]; + RuntimeProbe.require(!I18n.get(key).equals(key) && !I18n.get(key + ".tooltip").equals(key + ".tooltip"), + "Readable label and tooltip for " + key); + render(minecraft, screen, button.getX() + 5, button.getY() + 5); + screenshot(minecraft, modId + "-" + keys[choiceIndex] + "-tooltip.png"); + button.onPress(); + RuntimeProbe.require(!button.getMessage().getString().equals(original.get(choiceIndex)), "The pending choice changes"); + tooltips++; + if (++choiceIndex == keys.length) { + buttons(screen).get(keys.length).onPress(); + deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(10); + settleTicks = 0; + stage = 2; + } + } else if (stage == 2) { + // Native config saves and file watches are asynchronous. Reopening on + // a later frame also follows the same path a player uses. + RuntimeProbe.require(System.nanoTime() < deadline, "Saved config choices became readable for " + modId); + Screen pending = newConfigScreen(modId); + minecraft.setScreen(pending); + boolean changed = true; + for (int index = 0; index < keys.length; index++) { + changed &= !buttons(pending).get(index).getMessage().getString().equals(original.get(index)); + } + if (!changed || !configuredValuesMatchDisk(modId)) { + settleTicks = 0; + return false; + } + if (++settleTicks < 20) return false; + screen = ModMenuProbe.open(minecraft, modId, parent); + for (int index = 0; index < keys.length; index++) { + Button button = buttons(screen).get(index); + RuntimeProbe.require(!button.getMessage().getString().equals(original.get(index)), "Saved choices are reloaded: " + keys[index]); + for (int attempt = 0; attempt < 3 && !button.getMessage().getString().equals(original.get(index)); attempt++) { + button.onPress(); + } + RuntimeProbe.require(button.getMessage().getString().equals(original.get(index)), "Restore the disposable configuration"); + } + buttons(screen).get(keys.length).onPress(); + settleTicks = 0; + stage = 3; + } else { + RuntimeProbe.require(System.nanoTime() < deadline, "Original config choices restored for " + modId); + Screen restored = newConfigScreen(modId); + minecraft.setScreen(restored); + for (int index = 0; index < keys.length; index++) { + if (!buttons(restored).get(index).getMessage().getString().equals(original.get(index))) { + settleTicks = 0; + return false; + } + } + if (!configuredValuesMatchDisk(modId)) { + settleTicks = 0; + return false; } - minecraft.getFramebuffer().bindFramebuffer(true); - minecraft.entityRenderer.setupOverlayRendering(); - screen.drawScreen(0, 0, 0); - net.minecraft.util.ScreenShotHelper.saveScreenshot(minecraft.gameDir, modId + "-config.png", - minecraft.displayWidth, minecraft.displayHeight, minecraft.getFramebuffer()); - RuntimeProbe.require(new java.io.File(minecraft.gameDir, "screenshots/" + modId + "-config.png").isFile(), - "config screen screenshot saved"); - screen.onGuiClosed(); + if (++settleTicks < 20) return false; + RuntimeProbe.require(SkysGrassSlabsConfig.generateGrassSlabs() == activeGrass + && TerrainSmootherConfig.enabled() == activeTerrain && TerrainSmootherConfig.level() == activeLevel, + "Saving choices leaves this session unchanged"); + modIndex++; + stage = 0; } + return false; + } - Configuration grass = (Configuration) grassConfiguration("configuration"); - Configuration terrain = TerrainSmootherConfig.configuration(); - boolean activeGrass = SkysGrassSlabsConfig.generateGrassSlabs(); - int activeLevel = SkysTerrainSmoother.level; + private static Screen newConfigScreen(String modId) { + return modId.equals("skysgrassslabs") + ? new zone.moddev.mc.skysgrassslabs.client.GrassSlabsConfigScreen(parent) + : new zone.moddev.mc.skysterrainsmoother.client.TerrainSmootherConfigScreen(parent); + } - grass.get("worldgen", "generateGrassSlabs", true).set(!activeGrass); - int pendingLevel = activeLevel == 3 ? 1 : 3; - terrain.getCategory("worldgen").get("smoothingLevel").set(pendingLevel); - MinecraftForge.EVENT_BUS.post(new ConfigChangedEvent.OnConfigChangedEvent("unrelated", null, false, true)); - RuntimeProbe.require(read(grass).get("worldgen", "generateGrassSlabs", true).getBoolean() == activeGrass - && read(terrain).get("worldgen", "smoothingLevel", 1).getInt() == activeLevel, - "other mods cannot save these settings"); - MinecraftForge.EVENT_BUS.post(new ConfigChangedEvent.OnConfigChangedEvent("skysgrassslabs", null, false, true)); - MinecraftForge.EVENT_BUS.post(new ConfigChangedEvent.OnConfigChangedEvent("skysterrainsmoother", null, false, true)); - // Forge's save() does not clear every dirty flag. Read the files back instead. - RuntimeProbe.require(read(grass).get("worldgen", "generateGrassSlabs", true).getBoolean() != activeGrass - && read(terrain).get("worldgen", "smoothingLevel", 1).getInt() == pendingLevel, - "actual GUI event handlers saved both files"); - RuntimeProbe.require(SkysGrassSlabsConfig.generateGrassSlabs() == activeGrass - && SkysTerrainSmoother.level == activeLevel, "pending GUI choices leave active settings unchanged"); + private static boolean configuredValuesMatchDisk(String modId) { + String[] values = modId.equals("skysgrassslabs") + ? new String[] { + Boolean.toString(SkysGrassSlabsConfig.configuredGrassGeneration()), + Boolean.toString(SkysGrassSlabsConfig.configuredLegacyReplacement()), + Boolean.toString(SkysGrassSlabsConfig.configuredGrassSlabsReplacement()) + } + : new String[] { + Boolean.toString(TerrainSmootherConfig.configuredEnabled()), + Integer.toString(TerrainSmootherConfig.configuredLevel()), + Boolean.toString(TerrainSmootherConfig.configuredMineralogy()) + }; + try { + String contents = java.nio.file.Files.readString(java.nio.file.Paths.get( + "config", modId + "-common.toml"), java.nio.charset.StandardCharsets.UTF_8); + for (int index = 0; index < keys.length; index++) { + java.util.regex.Matcher value = java.util.regex.Pattern.compile( + "(?m)^\\h*" + java.util.regex.Pattern.quote(keys[index]) + + "\\h*=\\h*(true|false|[1-3])\\h*$").matcher(contents); + if (!value.find() || !value.group(1).equals(values[index]) || value.find()) { + return false; + } + } + return true; + } catch (java.io.IOException failure) { + throw new IllegalStateException("Cannot read the saved test configuration for " + modId, failure); + } + } + + static void render(Minecraft minecraft, Screen screen, int mouseX, int mouseY) { + minecraft.getMainRenderTarget().bindWrite(true); + com.mojang.blaze3d.platform.Window window = minecraft.getWindow(); - grass.get("worldgen", "generateGrassSlabs", true).set(activeGrass); - terrain.getCategory("worldgen").get("smoothingLevel").set(activeLevel); - grassConfiguration("saveChanges"); - TerrainSmootherConfig.saveChanges(); - System.out.println("SKY_CONFIG_SCREENS_PASS factories=2 widgets=5 restart_safe=true"); + com.mojang.blaze3d.systems.RenderSystem.clear(256, Minecraft.ON_OSX); + float farPlane = net.neoforged.neoforge.client.ClientHooks.getGuiFarPlane(); + com.mojang.blaze3d.systems.RenderSystem.setProjectionMatrix( + new org.joml.Matrix4f().setOrtho(0, (float) (window.getWidth() / window.getGuiScale()), + (float) (window.getHeight() / window.getGuiScale()), 0, 1000, farPlane), + com.mojang.blaze3d.vertex.VertexSorting.ORTHOGRAPHIC_Z); + org.joml.Matrix4fStack modelView = + com.mojang.blaze3d.systems.RenderSystem.getModelViewStack(); + modelView.identity(); + modelView.translate(0, 0, 1000 - farPlane); + com.mojang.blaze3d.systems.RenderSystem.applyModelViewMatrix(); + com.mojang.blaze3d.platform.Lighting.setupFor3DItems(); + var graphics = new net.minecraft.client.gui.GuiGraphics(minecraft, minecraft.renderBuffers().bufferSource()); + screen.render(graphics, mouseX, mouseY, 0); + graphics.flush(); } - private static Configuration read(Configuration source) { - Configuration written = new Configuration(source.getConfigFile()); - written.load(); - return written; + private static List