From c31d1ce35170ac2c4d924132bf78b8ea0b33ddcc Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sun, 1 Nov 2026 01:32:47 +0000 Subject: [PATCH 01/26] Port Terrain Smoother to Minecraft 1.14.4 --- .gitattributes | 4 + .github/workflows/ci.yml | 149 +- .github/workflows/codeql-analysis.yml | 41 +- .github/workflows/deploy-release.yml | 8 +- .github/workflows/release-on-tag.yml | 6 +- .github/workflows/validate-gradle-build.yml | 4 +- .gitignore | 4 + CHANGELOG.md | 8 + README.md | 11 +- build.gradle | 426 +- docs/API.md | 9 +- docs/BUILDING.md | 33 +- docs/CONFIGURATION.md | 2 +- docs/WORLD-UPGRADES.md | 12 + gradle.properties | 16 +- gradle/content-dependencies.properties | 15 +- gradle/mineralogy-runs.gradle | 143 +- gradle/mineralogy-runtime.properties | 14 +- gradle/recipe-advancements.gradle | 9 +- .../defaultconfigs/config/forge-client.toml | 22 + .../defaultconfigs/forge-server.toml | 30 + .../probe/NativeBootstrap.java | 135 + .../resources/META-INF/mods.toml | 14 + src/bootstrapTest/resources/pack.mcmeta | 1 + .../skysterrainsmoother/probe/CaveProbe.java | 134 +- .../probe/ClientProbe.java | 109 +- .../probe/ConfigScreenProbe.java | 211 +- .../probe/FamilyForwardChecks.java | 167 + .../probe/FineGrassModelProbe.java | 41 +- .../probe/FineGrassProbe.java | 70 +- .../probe/FoliageProbe.java | 45 +- .../probe/LateTerrainProbe.java | 76 +- .../probe/MineralogyProbe.java | 18 +- .../probe/ModMenuProbe.java | 141 +- .../probe/OpportunityProbe.java | 72 +- .../skysterrainsmoother/probe/ProbeWorld.java | 73 + .../probe/RecipePreviewModels.java | 74 + .../probe/RuntimeProbe.java | 569 +- .../skysterrainsmoother/probe/SandProbe.java | 113 + .../probe/TransitionProbe.java | 72 +- .../resources/META-INF/mods.toml | 14 + .../blockstates/ore_support.json | 7 +- .../terrainsmootherprobe/lang/en_us.json | 1 + .../terrainsmootherprobe/lang/en_us.lang | 1 - .../models/item/ore_support.json | 4 +- .../data/forge/tags/blocks/ores.json | 1 + .../fixtures/snowy_stone_terrace.json | 2 +- src/integrationTest/resources/mcmod.info | 1 - src/integrationTest/resources/pack.mcmeta | 1 + .../SkysTerrainSmoother.java | 95 +- .../api/TerrainMaterial.java | 26 +- .../api/TerrainSmoothingApi.java | 20 +- .../client/ClientProxy.java | 24 +- .../client/CommonProxy.java | 6 - .../client/TerrainSmootherConfigScreen.java | 86 +- .../client/TerrainSmootherGuiFactory.java | 43 +- .../config/TerrainSmootherConfig.java | 175 +- .../content/SandContent.java | 47 +- .../content/SandLegacyRecovery.java | 71 + .../content/SandSlabBlock.java | 174 +- .../content/SandSlabItem.java | 113 +- .../internal/BuiltInMaterials.java | 163 +- .../internal/EmptyAccess.java | 46 +- .../internal/MaterialCatalogue.java | 96 +- .../internal/SmoothingEngine.java | 463 +- .../internal/SmoothingFoliage.java | 90 +- .../internal/StructureProtection.java | 80 +- .../internal/TerrainSmoothingFeature.java | 73 + src/main/recipe-book/recipes.json | 12 +- src/main/resources/META-INF/mods.toml | 36 + .../blockstates/red_sand_slab.json | 10 + .../blockstates/sand_slab.json | 14 +- .../skysterrainsmoother/lang/de_at.json | 10 + .../skysterrainsmoother/lang/de_at.lang | 8 - .../skysterrainsmoother/lang/de_au.json | 10 + .../skysterrainsmoother/lang/de_au.lang | 8 - .../skysterrainsmoother/lang/de_de.json | 10 + .../skysterrainsmoother/lang/de_de.lang | 8 - .../skysterrainsmoother/lang/en_ca.json | 10 + .../skysterrainsmoother/lang/en_ca.lang | 8 - .../skysterrainsmoother/lang/en_en.json | 10 + .../skysterrainsmoother/lang/en_en.lang | 8 - .../skysterrainsmoother/lang/en_gb.json | 10 + .../skysterrainsmoother/lang/en_gb.lang | 8 - .../skysterrainsmoother/lang/en_pt.json | 10 + .../skysterrainsmoother/lang/en_pt.lang | 8 - .../skysterrainsmoother/lang/en_us.json | 10 + .../skysterrainsmoother/lang/en_us.lang | 8 - .../skysterrainsmoother/lang/es_es.json | 10 + .../skysterrainsmoother/lang/es_es.lang | 8 - .../skysterrainsmoother/lang/es_mx.json | 10 + .../skysterrainsmoother/lang/es_mx.lang | 8 - .../skysterrainsmoother/lang/fr_ca.json | 10 + .../skysterrainsmoother/lang/fr_ca.lang | 8 - .../skysterrainsmoother/lang/fr_fr.json | 10 + .../skysterrainsmoother/lang/fr_fr.lang | 8 - .../skysterrainsmoother/lang/ja_jp.json | 10 + .../skysterrainsmoother/lang/ja_jp.lang | 8 - .../skysterrainsmoother/lang/ko_kr.json | 10 + .../skysterrainsmoother/lang/ko_kr.lang | 8 - .../skysterrainsmoother/lang/pt_br.json | 10 + .../skysterrainsmoother/lang/pt_br.lang | 8 - .../skysterrainsmoother/lang/pt_pt.json | 10 + .../skysterrainsmoother/lang/pt_pt.lang | 8 - .../skysterrainsmoother/lang/ru_ru.json | 10 + .../skysterrainsmoother/lang/ru_ru.lang | 8 - .../skysterrainsmoother/lang/zh_cn.json | 10 + .../skysterrainsmoother/lang/zh_cn.lang | 8 - .../models/block/red_sand_slab.json | 8 +- .../models/block/red_sand_slab_top.json | 8 +- .../models/block/sand_slab.json | 8 +- .../models/block/sand_slab_top.json | 8 +- .../loot_tables/blocks/red_sand_slab.json | 19 + .../loot_tables/blocks/sand_slab.json | 19 + .../recipes/red_sand_from_slabs.json | 15 + .../recipes/red_sand_slab.json | 16 + .../recipes/sand_from_slabs.json | 15 + .../recipes/sand_slab.json | 16 + src/main/resources/mcmod.info | 1 - src/main/resources/pack.mcmeta | 7 +- .../mc/skysterrainsmoother/CatalogueTest.java | 104 +- .../EclipseDependencyContractTest.java | 2 +- .../ForwardFixtureTest.java | 86 + .../MinecraftTestBootstrap.java | 34 + .../MineralogyLaunchContractTest.java | 9 +- .../ProjectContractTest.java | 193 +- .../RecipeAdvancementContractTest.java | 2 +- .../mc/skysterrainsmoother/SandStateTest.java | 99 + .../config/ConfigScreenContractTest.java | 176 +- .../fixtures/forward-1.12.2-family/README.md | 12 + .../fixtures/forward-1.12.2-family/SHA256SUMS | 1 + .../source-artifacts.json | 25 + .../source-family-fixture.json | 58036 ++++++++++++++++ .../source-file-sha256.json | 26 + .../fixtures/forward-1.12.2-family/world.zip | Bin 0 -> 293374 bytes tools/native_client_log.py | 126 + tools/run_packaged_checks.py | 233 + tools/test_native_client_log.py | 104 + tools/test_native_server_log.py | 96 + 139 files changed, 62679 insertions(+), 2245 deletions(-) create mode 100644 src/bootstrapTest/defaultconfigs/config/forge-client.toml create mode 100644 src/bootstrapTest/defaultconfigs/defaultconfigs/forge-server.toml create mode 100644 src/bootstrapTest/java/zone/moddev/mc/skysterrainsmoother/probe/NativeBootstrap.java create mode 100644 src/bootstrapTest/resources/META-INF/mods.toml create mode 100644 src/bootstrapTest/resources/pack.mcmeta create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FamilyForwardChecks.java create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ProbeWorld.java create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RecipePreviewModels.java create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SandProbe.java create mode 100644 src/integrationTest/resources/META-INF/mods.toml create mode 100644 src/integrationTest/resources/assets/terrainsmootherprobe/lang/en_us.json delete mode 100644 src/integrationTest/resources/assets/terrainsmootherprobe/lang/en_us.lang create mode 100644 src/integrationTest/resources/data/forge/tags/blocks/ores.json delete mode 100644 src/integrationTest/resources/mcmod.info create mode 100644 src/integrationTest/resources/pack.mcmeta delete mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/client/CommonProxy.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandLegacyRecovery.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/internal/TerrainSmoothingFeature.java create mode 100644 src/main/resources/META-INF/mods.toml create mode 100644 src/main/resources/assets/skysterrainsmoother/blockstates/red_sand_slab.json create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/de_at.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/de_at.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/de_au.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/de_au.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/de_de.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/de_de.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_ca.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_ca.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_en.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_en.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_gb.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_gb.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_pt.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_pt.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_us.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/en_us.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/es_es.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/es_es.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/es_mx.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/es_mx.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/fr_ca.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/fr_fr.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/ja_jp.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/ko_kr.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/pt_br.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/pt_br.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/pt_pt.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/ru_ru.lang create mode 100644 src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json delete mode 100644 src/main/resources/assets/skysterrainsmoother/lang/zh_cn.lang create mode 100644 src/main/resources/data/skysterrainsmoother/loot_tables/blocks/red_sand_slab.json create mode 100644 src/main/resources/data/skysterrainsmoother/loot_tables/blocks/sand_slab.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipes/red_sand_from_slabs.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipes/red_sand_slab.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipes/sand_from_slabs.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipes/sand_slab.json delete mode 100644 src/main/resources/mcmod.info create mode 100644 src/test/java/zone/moddev/mc/skysterrainsmoother/ForwardFixtureTest.java create mode 100644 src/test/java/zone/moddev/mc/skysterrainsmoother/MinecraftTestBootstrap.java create mode 100644 src/test/java/zone/moddev/mc/skysterrainsmoother/SandStateTest.java create mode 100644 src/test/resources/fixtures/forward-1.12.2-family/README.md create mode 100644 src/test/resources/fixtures/forward-1.12.2-family/SHA256SUMS create mode 100644 src/test/resources/fixtures/forward-1.12.2-family/source-artifacts.json create mode 100644 src/test/resources/fixtures/forward-1.12.2-family/source-family-fixture.json create mode 100644 src/test/resources/fixtures/forward-1.12.2-family/source-file-sha256.json create mode 100644 src/test/resources/fixtures/forward-1.12.2-family/world.zip create mode 100644 tools/native_client_log.py create mode 100644 tools/run_packaged_checks.py create mode 100644 tools/test_native_client_log.py create mode 100644 tools/test_native_server_log.py diff --git a/.gitattributes b/.gitattributes index 83eed3b..8c49ba7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,7 @@ *.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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0738b69..1824459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,22 +1,113 @@ -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: + cold-forge-bootstrap: + name: Cold Forge bootstrap + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: 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) + (cd dependencies/pieces && ./gradlew assemble --no-daemon --stacktrace) + + + - name: Bootstrap Forge 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 -PgrassSlabsDirectory=dependencies/grass -PbuildingPiecesDirectory=dependencies/pieces + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Verify same-cache bootstrap offline + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/skysterrainsmoother-cold-gradle + run: | + set -euo pipefail + gradle_args=( + classes test -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,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + build: name: Build, test, and audit runs-on: ubuntu-latest @@ -26,27 +117,49 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Read target build metadata + id: target + shell: bash + run: | + 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 ForgeGradle Mavenizer Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: '25.0.3+9.0.LTS' + + - name: Install target Java toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: ${{ steps.target.outputs.java_setup_version }} - name: Pin production Java + if: steps.target.outputs.java_version == '8' 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" - - name: Install Java 17 for Gradle + - name: Install Java for Gradle uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: ${{ steps.target.outputs.gradle_java_version }} - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Read pinned dependency sources id: content run: | @@ -73,8 +186,9 @@ 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: Make the wrapper executable run: chmod +x ./gradlew @@ -86,19 +200,18 @@ jobs: - 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: 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/SkysTerrainSmoother-0.1.0.114041.jar + build/libs/SkysTerrainSmoother-0.1.0.114041-sources.jar + build/libs/SkysTerrainSmoother-0.1.0.114041-javadoc.jar build/release/SHA256SUMS CHANGELOG.md @@ -106,7 +219,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..182f918 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,49 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Read target build metadata + id: target + shell: bash + run: | + 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 ForgeGradle Mavenizer Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install target Java toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: ${{ steps.target.outputs.java_setup_version }} - name: Pin production Java + if: steps.target.outputs.java_version == '8' 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" - - name: Install Java 17 for Gradle + - name: Install Java for Gradle uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: ${{ steps.target.outputs.gradle_java_version }} - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Read pinned dependency sources id: content run: | @@ -73,8 +95,9 @@ 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: 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..e12f77f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 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..00ea675 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. @@ -48,14 +48,15 @@ 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.14.4**, use **Forge 28.2.26** and **Java 8**, 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.114041**. +- Sky's Building Pieces **0.3.8.114041**. +- Sky's Terrain Smoother **0.1.0.114041**. Install all three main jars on both the client and server. +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..5c2da85 100644 --- a/build.gradle +++ b/build.gradle @@ -59,8 +59,8 @@ tasks.withType(JavaCompile).configureEach { } } -def grassSlabsDirectory = file(providers.gradleProperty('grassSlabsDirectory').getOrElse('../..' + '/SkysGrassSlabs112/SkysGrassSlabs')) -def buildingPiecesDirectory = file(providers.gradleProperty('buildingPiecesDirectory').getOrElse('../..' + '/SkysBuildingPieces112/SkysBuildingPieces')) +def grassSlabsDirectory = file(providers.gradleProperty('grassSlabsDirectory').getOrElse('../..' + '/SkysGrassSlabs114/SkysGrassSlabs')) +def buildingPiecesDirectory = file(providers.gradleProperty('buildingPiecesDirectory').getOrElse('../..' + '/SkysBuildingPieces114/SkysBuildingPieces')) def contentLock=new Properties() file('gradle/content-dependencies.properties').withInputStream { contentLock.load(it) } @@ -83,24 +83,48 @@ if (localPiecesVersion.isPresent() && 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') { +def verifyContentDependencies = tasks.register('verifyContentDependencies') { 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.' : '' + 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 ${expectedVersion}, found ${metadata.mod_version})." + localHelp) + " (expected ${selected.version}, found ${metadata.mod_version}).") } - 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) + 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-dev/${selected.artifact}-${selected.version}-deobf.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) + } + } } } } @@ -130,20 +154,21 @@ tasks.withType(JavaExec).configureEach { tasks.withType(Javadoc).configureEach { if (pinnedJava8.isPresent()) javadocTool = javaToolchains.javadocToolFor(exactJava8Spec) - failOnError = false + failOnError = true options.encoding = 'UTF-8' options.addStringOption('Xdoclint:none', '-quiet') options.addBooleanOption('notimestamp', true) } tasks.withType(AbstractArchiveTask).configureEach { + includeEmptyDirs = false preserveFileTimestamps = false reproducibleFileOrder = true } 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([ @@ -157,42 +182,78 @@ def normalizeArchiveLineEndings = { details -> } def developmentRunDirectory = providers.gradleProperty('skysTerrainSmootherRunDirectory').getOrElse('run') + +def prepareContentDevelopmentRun = tasks.register('prepareContentDevelopmentRun') { + group = 'ide' + dependsOn verifyContentDependencies + doLast { + File mods = new File(file(developmentRunDirectory), '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) + } + } + copy { + from grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar + into mods + } + } +} + minecraft { mappings channel: project.mapping_channel, version: project.mapping_version runs { configureEach { + mainClass = 'net.minecraftforge.userdev.LaunchTesting' workingDir.convention layout.projectDirectory.dir(developmentRunDirectory) - environment 'MC_VERSION', project.minecraft_version.toString() + systemProperty 'forge.logging.markers', 'REGISTRIES' systemProperty 'forge.logging.console.level', 'debug' - systemProperty 'fml.coreMods.load', 'zone.moddev.mc.skysbuildingpieces.legacy.LoaderPlugin' + mods { skysterrainsmoother { source sourceSets.main } } + environment 'MOD_CLASSES', + "${mod_id}%%${buildDir}/forge-run-mod-classes/main${File.pathSeparator}" + + "${mod_id}%%${buildDir}/forge-run-mod-classes/main" + if (name.toLowerCase(Locale.ROOT).contains('client') && + providers.gradleProperty('forgeClientAssets').isPresent()) { + environment 'assetIndex', '1.14' + environment 'assetDirectory', providers.gradleProperty('forgeClientAssets').get() + if (providers.gradleProperty('forgeClientNatives').isPresent()) { + environment 'nativesDirectory', providers.gradleProperty('forgeClientNatives').get() + } + } } register('client') register('server') { args '--nogui' } } } -// 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 } +def forgeRunModClassesDirectory = file("${buildDir}/forge-run-mod-classes/main") +def prepareForgeRunModClasses = tasks.register('prepareForgeRunModClasses', Sync) { + dependsOn tasks.named('classes'), prepareContentDevelopmentRun + from sourceSets.main.output.classesDirs from sourceSets.main.output.resourcesDir - into forge12DevelopmentOutput + into forgeRunModClassesDirectory } -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 configureForge28Run = { JavaExec runTask, String launchTarget -> + runTask.dependsOn prepareForgeRunModClasses + runTask.environment 'target', launchTarget + runTask.environment 'MCP_MAPPINGS', "${mapping_channel}_${mapping_version}" + runTask.environment 'MCP_VERSION', mcp_version + runTask.environment 'FORGE_VERSION', forge_version + runTask.environment 'FORGE_GROUP', 'net.minecraftforge' + runTask.environment 'MC_VERSION', minecraft_version + runTask.environment 'MOD_CLASSES', "${mod_id}%%${forgeRunModClassesDirectory}${File.pathSeparator}" + + "${mod_id}%%${forgeRunModClassesDirectory}" +} +tasks.withType(JavaExec).configureEach { JavaExec runTask -> + Map targets = [runClient: 'fmluserdevclient', runServer: 'fmluserdevserver'] + String launchTarget = targets.get(runTask.name) + if (launchTarget != null) { + configureForge28Run(runTask, launchTarget) } } @@ -226,14 +287,9 @@ def configureFromForgeRun = { JavaExec process, String runTaskName -> 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.environment('MOD_CLASSES', + "${mod_id}%%${forgeRunModClassesDirectory}${File.pathSeparator}" + + "${mod_id}%%${forgeRunModClassesDirectory}") run.exec() } finally { runOptions.workingDir.set(originalConfiguredWorkingDir) @@ -278,7 +334,6 @@ 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' @@ -290,7 +345,7 @@ def compileIntegrationTestMod = tasks.register('compileIntegrationTestMod', Java group = 'verification' dependsOn tasks.named('classes') source fileTree('src/integrationTest/java') - classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) + classpath = files(sourceSets.main.output, sourceSets.main.runtimeClasspath) destinationDirectory = integrationTestClasses options.encoding = 'UTF-8' } @@ -324,11 +379,19 @@ def prepareIntegrationTest = tasks.register('prepareIntegrationTest') { dependsOn integrationTestModJar doLast { File runDirectory = integrationRunDirectory.get().asFile - project.delete(runDirectory) + if (runDirectory.canonicalFile != new File(buildDir, 'integration-test-run').canonicalFile) { + throw new GradleException('Integration checks must stay inside the build directory') + } + if (new File(runDirectory, 'logs/latest.log').isFile()) { + throw new GradleException('Archive the previous runtime evidence before preparing another run') + } + runDirectory.mkdirs() + copy { from 'src/bootstrapTest/defaultconfigs'; into runDirectory } File mods = new File(runDirectory, 'mods') mods.mkdirs() project.copy { from integrationTestModJar.flatMap { it.archiveFile } + from grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar into mods } new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') @@ -357,7 +420,6 @@ def createIntegrationProcess = { String phase, Object dependency -> 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, @@ -370,6 +432,15 @@ def createIntegrationProcess = { String phase, Object dependency -> if (values.getProperty("${phase.toLowerCase(Locale.ROOT)}_complete") != 'true') { throw new GradleException("${phase} integration did not complete: ${marker}") } + project.services.get(org.gradle.process.ExecOperations).exec { + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), + 'tools/native_client_log.py', '--server', '--directory', + integrationRunDirectory.get().asFile.absolutePath, '--phase', phase.toLowerCase(Locale.ROOT) + } + copy { + from new File(integrationRunDirectory.get().asFile, 'logs') + into new File(integrationRunDirectory.get().asFile, 'phase-logs/' + phase.toLowerCase(Locale.ROOT)) + } } } } @@ -400,15 +471,35 @@ tasks.register('integrationTest') { } } +def integrationClientDirectory = layout.buildDirectory.dir('integration-client-run') +def prepareIntegrationClient = tasks.register('prepareIntegrationClient') { + dependsOn integrationTestModJar, verifyContentDependencies + doLast { + File target = integrationClientDirectory.get().asFile + if (target.canonicalFile != new File(buildDir, 'integration-client-run').canonicalFile) { + throw new GradleException('Client checks must stay inside the build directory') + } + if (new File(target, 'logs/latest.log').isFile()) { + throw new GradleException('Archive the previous client evidence before preparing another run') + } + target.mkdirs() + copy { from 'src/bootstrapTest/defaultconfigs'; into target } + copy { + from integrationTestModJar.flatMap { it.archiveFile }, grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar + into new File(target, 'mods') + } + new File(target, 'options.txt').text = 'fullscreen:false\noverrideWidth:854\noverrideHeight:480\nmaxFps:30\npauseOnLostFocus:false\nrenderDistance:6\n' + } +} + tasks.register('integrationClientProcess', JavaExec) { group = 'verification' - dependsOn prepareIntegrationTest - workingDir integrationRunDirectory + dependsOn prepareIntegrationClient + workingDir integrationClientDirectory 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()) + if (!new File(integrationClientDirectory.get().asFile, 'terrain-smoother-client-pass.txt').isFile()) throw new GradleException('Client did not complete resource baking') } } @@ -419,18 +510,11 @@ tasks.named('check') { 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', 'forge_version_range', + 'minecraft_version_range'].collectEntries { key -> [(key): project.property(key)] } + inputs.properties(metadata) + filesMatching('META-INF/mods.toml') { expand metadata } filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) } @@ -486,12 +570,12 @@ 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' || + if (project.minecraft_version != '1.14.4' || project.forge_version != '28.2.26' || + project.mapping_channel != 'snapshot' || project.mapping_version != '20190719-1.14.3' || project.loader_name != 'forge' || project.loader_code != '1' || project.java_version != '8' || project.java_toolchain_version != '8.0.502+7' || project.mod_id != 'skysterrainsmoother') { - throw new GradleException('Unexpected 1.12.2 release metadata') + throw new GradleException('Unexpected 1.14.4 release metadata') } } } @@ -510,7 +594,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/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}") } @@ -689,8 +773,20 @@ tasks.register('isolateEclipseProductionRuns') { 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()) + // Eclipse needs a literal version, not ForgeGradle's unresolved token. + contents = contents.replace( + 'key="MC_VERSION" value="${MC_VERSION}"', + "key=\"MC_VERSION\" value=\"${project.minecraft_version}\"") boolean production = name in ['runClient.launch', 'runServer.launch'] + if (production) { + // Eclipse compiles into bin/main; use that same merged output + // for both of Forge 28's discovery passes, including fresh edits. + String roots = "${mod_id}%%${projectDir}/bin/main${File.pathSeparator}" + + "${mod_id}%%${projectDir}/bin/main" + contents = contents.replaceAll(/key="MOD_CLASSES" value="[^"]*"/) { + 'key="MOD_CLASSES" value="' + roots + '"' + } + } if (production && !contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) { contents = contents.replace('', ' ' + @@ -714,6 +810,7 @@ tasks.register('isolateEclipseProductionRuns') { } tasks.named('genEclipseRuns') { + dependsOn prepareContentDevelopmentRun finalizedBy configureEclipseBuildship, 'isolateEclipseProductionRuns' } @@ -739,7 +836,7 @@ tasks.register('verifyEclipseProductionClasspath') { if (overrides != expectedOverrides) { throw new GradleException('Eclipse Buildship retained an incorrect Building Pieces version override') } - File metadata = file('bin/main/mcmod.info') + File metadata = file('bin/main/META-INF/mods.toml') def eclipseClasspath = new groovy.xml.XmlSlurper().parse(file('.classpath')) def piecesLibraries = eclipseClasspath.classpathentry.findAll { it.@kind == 'lib' && it.@path.text().tokenize('/\\').last().startsWith('SkysBuildingPieces-') @@ -758,9 +855,19 @@ tasks.register('verifyEclipseProductionClasspath') { resourceFolders.count { it == 'build/resources/main' } != 1) { throw new GradleException('Eclipse must copy processed resources, not overwrite them with templates') } - 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') + String nativeMetadata = metadata.isFile() ? metadata.getText('UTF-8') : '' + if (!nativeMetadata.contains("version=\"${project.mod_version}\"") || + !nativeMetadata.contains("versionRange=\"${project.minecraft_version_range}\"")) { + throw new GradleException('Eclipse output contains incorrect mods.toml') + } + ['runClient.launch', 'runServer.launch', 'runTestClient.launch', 'runTestServer.launch'].each { name -> + String contents = file(name).getText('UTF-8') + if (contents.contains('${MC_VERSION}')) { + throw new GradleException("${name} retains ForgeGradle's unresolved MC_VERSION token") + } + if (!contents.contains("key=\"MC_VERSION\" value=\"${project.minecraft_version}\"")) { + throw new GradleException("${name} does not define literal MC_VERSION=${project.minecraft_version}") + } } ['runClient.launch', 'runServer.launch'].each { name -> String contents = file(name).getText('UTF-8').replace('\\', '/') @@ -768,6 +875,15 @@ tasks.register('verifyEclipseProductionClasspath') { contents.contains('src/test') || contents.contains('bin/test') || contents.contains('junit-')) { throw new GradleException("${name} exposes test code or dependencies") } + def xml = new groovy.xml.XmlParser(false, false).parse(file(name)) + String modClasses = xml.mapAttribute.find { + it.@key == 'org.eclipse.debug.core.environmentVariables' + }.mapEntry.find { it.@key == 'MOD_CLASSES' }.@value + List roots = modClasses.split(java.util.regex.Pattern.quote(File.pathSeparator)).toList() + if (roots.size() != 2 || roots[0] != roots[1] || + roots[0].replace('\\', '/') != "${mod_id}%%${projectDir}/bin/main".replace('\\', '/')) { + throw new GradleException("${name} must repeat the merged production output in MOD_CLASSES") + } } } } @@ -777,31 +893,169 @@ tasks.named('check') { } def clientFresh = tasks.register('integrationClientFreshProcess', JavaExec) { - dependsOn prepareIntegrationTest - workingDir integrationRunDirectory + dependsOn prepareIntegrationClient + workingDir integrationClientDirectory 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') + File marker=new File(integrationClientDirectory.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') + project.services.get(org.gradle.process.ExecOperations).exec { + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), + 'tools/native_client_log.py', '--directory', integrationClientDirectory.get().asFile.absolutePath, + '--phase', 'client-fresh' + } + copy { from new File(integrationClientDirectory.get().asFile, 'logs'); into new File(integrationClientDirectory.get().asFile, 'phase-logs/fresh') } } } tasks.register('integrationClientReloadProcess', JavaExec) { dependsOn clientFresh - workingDir integrationRunDirectory + workingDir integrationClientDirectory 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') + File marker=new File(integrationClientDirectory.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') + project.services.get(org.gradle.process.ExecOperations).exec { + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), + 'tools/native_client_log.py', '--directory', integrationClientDirectory.get().asFile.absolutePath, + '--phase', 'client-reload' + } + copy { from new File(integrationClientDirectory.get().asFile, 'logs'); into new File(integrationClientDirectory.get().asFile, 'phase-logs/reload') } } } ext.configureMineralogyVerificationRun = configureFromForgeRun ext.mineralogyProfileProbeId = 'skysterrainsmoother' -ext.mineralogyProfileAdditionalMods = files() +ext.mineralogyProfileAdditionalMods = files(grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar) apply from: 'gradle/mineralogy-runs.gradle' apply from: 'gradle/recipe-advancements.gradle' + +// Keep early native checks separate while the full gameplay suite uses its own probe. +def nativeClasses = layout.buildDirectory.dir('native-bootstrap/classes') +def nativeCompile = tasks.register('compileNativeBootstrapProbe', JavaCompile) { + dependsOn tasks.named('classes') + source fileTree('src/bootstrapTest/java') + classpath = files(sourceSets.main.output, sourceSets.main.runtimeClasspath) + destinationDirectory = nativeClasses +} +def nativeJar = tasks.register('nativeBootstrapProbeJar', Jar) { + dependsOn nativeCompile + archiveFileName = 'terrain-native-checks.jar' + destinationDirectory = layout.buildDirectory.dir('native-bootstrap') + from nativeClasses + from 'src/bootstrapTest/resources' +} +def nativeDirectory = layout.buildDirectory.dir('native-bootstrap-run') +def nativePrepare = tasks.register('prepareNativeBootstrapTest') { + dependsOn nativeJar, verifyContentDependencies + doLast { + File target = nativeDirectory.get().asFile + if (target.canonicalFile != new File(buildDir, 'native-bootstrap-run').canonicalFile) + throw new GradleException('Native checks must stay inside build output') + if (new File(target, 'logs/latest.log').isFile()) + throw new GradleException('Archive the previous native runtime evidence before preparing another run') + target.mkdirs() + copy { from nativeJar.flatMap { it.archiveFile }, grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar; into new File(target, 'mods') } + new File(target, 'eula.txt').text = 'eula=true\n' + ['banned-players.json', 'banned-ips.json', 'ops.json', 'whitelist.json'].each { new File(target, it).text = '[]\n' } + new File(target, 'server.properties').text = 'level-name=world\nlevel-seed=4815162342\nonline-mode=false\nserver-ip=127.0.0.1\nserver-port=0\ngenerate-structures=false\nmax-tick-time=120000\n' + } +} +tasks.register('nativeBootstrapTest', JavaExec) { + dependsOn nativePrepare + workingDir nativeDirectory + configureFromForgeRun(delegate, 'runServer') + doLast { + File log = new File(nativeDirectory.get().asFile, 'logs/latest.log') + String result = log.isFile() ? log.getText('UTF-8') : '' + if (!result.contains('TERRAIN_NATIVE_BOOTSTRAP_PASS') || !result.contains('Done (') || + result =~ /(?m)\[[^\r\n]*\/(?:ERROR|FATAL)\]/ || result.contains('[STDERR/') || + !fileTree(new File(nativeDirectory.get().asFile, 'crash-reports')).isEmpty()) + throw new GradleException('Native Terrain Smoother checks failed; inspect the full logs') + } +} + +tasks.register('verifyNativeLogPolicy', Exec) { + group = 'verification' + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), + '-m', 'unittest', 'discover', '-s', 'tools', '-p', 'test_native_*_log.py' +} +tasks.named('check') { dependsOn tasks.named('verifyNativeLogPolicy') } + +// Recipe advancements are already in the processed resource tree. A second +// Eclipse source entry would copy the same resources into bin/main twice. +eclipse.classpath.file.whenMerged { classpath -> + classpath.entries.removeAll { entry -> + entry.kind == 'src' && entry.path == 'build/generated/recipe-advancements' + } +} + +def familyForwardDirectory = layout.buildDirectory.dir('family-forward-run') +def prepareFamilyForward = tasks.register('prepareFamilyForwardFixture') { + dependsOn integrationTestModJar, verifyContentDependencies + doLast { + File target = familyForwardDirectory.get().asFile + if (new File(target, 'logs/latest.log').isFile()) { + throw new GradleException('Archive the previous forward-upgrade evidence before preparing another run') + } + File fixture = file('src/test/resources/fixtures/forward-1.12.2-family') + File archive = new File(fixture, 'world.zip') + String expected = new File(fixture, 'SHA256SUMS').getText('UTF-8').trim().split(/\s+/)[0] + String actual = MessageDigest.getInstance('SHA-256').digest(Files.readAllBytes(archive.toPath())) + .encodeHex().toString().toUpperCase(Locale.ROOT) + if (actual != expected) throw new GradleException('The sealed source world changed') + copy { from zipTree(archive); into target } + copy { from new File(fixture, 'source-family-fixture.json'); into target } + copy { from 'src/bootstrapTest/defaultconfigs'; into target } + copy { + from integrationTestModJar.flatMap { it.archiveFile }, grassSlabsDevelopmentJar, buildingPiecesDevelopmentJar + into new File(target, 'mods') + } + new File(target, 'eula.txt').setText('eula=true\n', 'UTF-8') + ['banned-players.json', 'banned-ips.json', 'ops.json', 'whitelist.json'].each { name -> + new File(target, name).setText('[]\n', 'UTF-8') + } + new File(target, 'server.properties').setText('level-name=world\nonline-mode=false\n' + + 'server-ip=127.0.0.1\nserver-port=0\nview-distance=6\nmax-tick-time=120000\n', 'UTF-8') + } +} +def forwardFresh = tasks.register('integrationForwardFreshProcess', JavaExec) { + dependsOn prepareFamilyForward + workingDir familyForwardDirectory + systemProperty 'skysterrainsmoother.forwardFixture', 'true' + systemProperty 'skysterrainsmoother.integrationPhase', 'fresh' + configureFromForgeRun(delegate, 'runServer') +} +def forwardReload = tasks.register('integrationForwardReloadProcess', JavaExec) { + dependsOn forwardFresh + workingDir familyForwardDirectory + systemProperty 'skysterrainsmoother.forwardFixture', 'true' + systemProperty 'skysterrainsmoother.integrationPhase', 'reload' + configureFromForgeRun(delegate, 'runServer') +} +[forwardFresh: 'fresh', forwardReload: 'reload'].each { taskName, phase -> + def task = taskName == 'forwardFresh' ? forwardFresh : forwardReload + task.configure { + doLast { + File target = familyForwardDirectory.get().asFile + File marker = new File(target, 'world/family-forward-' + phase + '.properties') + if (!marker.isFile() || !marker.getText('UTF-8').contains('complete=true')) { + throw new GradleException('Forward upgrade checks did not complete: ' + phase) + } + project.services.get(org.gradle.process.ExecOperations).exec { + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), + 'tools/native_client_log.py', '--server', '--directory', target.absolutePath, '--phase', phase + } + copy { from new File(target, 'logs'); into new File(target, 'phase-logs/' + phase) } + } + } +} +tasks.register('integrationForwardTest') { + group = 'verification' + description = 'Upgrades and reloads a sealed native 1.12 family world.' + dependsOn forwardReload +} + +tasks.named('check') { dependsOn tasks.named('integrationForwardTest') } diff --git a/docs/API.md b/docs/API.md index f759c24..5ab87e3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,15 +1,18 @@ # 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/client setup or IMC callbacks. Declare ordering after +`skysterrainsmoother`. Registration closes at load completion. Forge 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( diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 3a6a6e2..e43b914 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -2,13 +2,14 @@ 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. +Minecraft 1.14.4 with ForgeGradle 7.0.34. Build the exact Grass Slabs and Building Pieces source commits listed in -`gradle/content-dependencies.properties` with their `jar` tasks first. +`gradle/content-dependencies.properties` with their `assemble` tasks first. The build checks both their source commits +and main/development jar checksums. -The source lock records released Grass Slabs 1.1.2.112021 and Building Pieces -0.3.8.112021. +The source lock records locally qualified Grass Slabs 1.2.0.114041 and Building Pieces +0.3.8.114041. The default layout uses sibling checkouts. For another layout, pass `-PgrassSlabsDirectory=` and @@ -30,7 +31,9 @@ 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) +use the two pinned content jars and exclude unit tests and build-only runtime probes. +Generation prepares those jars in the development mods folder without clearing +its configuration or worlds. See [the release guide](RELEASING.md) for publication. ## Testing with Mineralogy @@ -45,13 +48,14 @@ 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. -The expected release jars and their checksums are listed in +The expected main and mapped development 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. +modified. Development runs use the exact mapped jars; packaged games use the unchanged +main jars. If a sibling build has moved on, keep the qualified jars under `.gradle/qualification-dependencies/` with their original filenames. @@ -72,8 +76,15 @@ resources there. Neither task opens the manual profile's saved worlds. 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. +All four sand recipes retain their original IDs. -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. +Recipe unlocks have no shared advancement parent. This avoids a long recursive +visibility chain when loading completed progress. Inventory and crafting-table +discovery keep the same recipe-book behaviour. + + +`integrationForwardTest` upgrades a sealed 1.12.2 save and reloads the result. +It checks placed pieces, chest stacks, dropped items, an item frame and the saved +player's main, ender and nested inventories. The source archive stays unchanged. +Archive a previous disposable run before starting that task again; its logs and +world are kept for inspection. 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/WORLD-UPGRADES.md b/docs/WORLD-UPGRADES.md index 47d9acf..6012cf2 100644 --- a/docs/WORLD-UPGRADES.md +++ b/docs/WORLD-UPGRADES.md @@ -16,3 +16,15 @@ 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 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..0901a01 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,23 +4,27 @@ 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 +minecraft_version=1.14.4 +minecraft_version_range=[1.14.4] +forge_version=28.2.26 +forge_version_range=[28,) +loader_version_range=[28,) +mapping_channel=snapshot +mapping_version=20190719-1.14.3 +mcp_version=20190829.143755 loader_name=forge loader_code=1 java_version=8 java_toolchain_version=8.0.502+7 gradle_java_version=17 +gradle_java_toolchain_version=17.0.1+12 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.114041 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..2322c72 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,6 +1,13 @@ +# Qualified local candidates; publication remains separate. grass.repository=MinecraftModDevelopmentMods/SkysGrassSlabs -grass.commit=b2636d8a67efa3f257a8a69293ce513e979010fd -grass.version=1.1.2.112021 +grass.kind=local-candidate +grass.commit=a87c91b22d583b2358480735cb4694679fb1866b +grass.version=1.2.0.114041 +grass.sha256=019321AB9EBEE72B9E9086B53DAD3486FF68B067AFB0E83B8E29661B625763E0 +grass.development.sha256=C49B434404C30BFD44B18E2E9F8D9DC773EB4581FC1953423CACA1FC8E96B135 pieces.repository=MinecraftModDevelopmentMods/SkysBuildingPieces -pieces.commit=af3ce9b73d215a2f9e27a11324da56a752e4edac -pieces.version=0.3.8.112021 +pieces.kind=local-candidate +pieces.commit=f07a38b22ffef00c03c8e83e48377714a13a88cf +pieces.version=0.3.8.114041 +pieces.sha256=8A0A1DEA2A4A6C069519C7B1BCF28331186FF2DF6284DB72C2FFDB8178FC84D4 +pieces.development.sha256=EF13A7DF57DCC76E0B693766337384AAFE3BDFFB88B10314ACD788E3B8F1A75C diff --git a/gradle/mineralogy-runs.gradle b/gradle/mineralogy-runs.gradle index ede971a..4aefd3c 100644 --- a/gradle/mineralogy-runs.gradle +++ b/gradle/mineralogy-runs.gradle @@ -5,7 +5,7 @@ import java.nio.file.Files 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')) + File siblingJar = file(runtimeLock.getProperty(mod + '.development.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. @@ -18,54 +18,25 @@ def gameDirectory = layout.projectDirectory.dir( 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) - } +// Exact mapped reference jars already carry the target's native access rules. +// Leave their matching production artifacts unchanged for packaged checks. +ext.nativeOptionalDevelopmentJars = runtimeSources +ext.verifyNativeOptionalRuntime = tasks.register('verifyNativeOptionalRuntime') { + group = 'verification' + doLast { + ['mineralogy', 'orespawn'].each { mod -> + [[file(runtimeLock.getProperty(mod + '.path')), runtimeLock.getProperty(mod + '.sha256')], + [runtimeSources[mod], runtimeLock.getProperty(mod + '.development.sha256')]].each { pair -> + File source = pair[0] + if (!source.isFile()) throw new GradleException('Missing optional reference jar: ' + source) + String actual = MessageDigest.getInstance('SHA-256').digest(Files.readAllBytes(source.toPath())) + .encodeHex().toString().toUpperCase(Locale.ROOT) + if (actual != pair[1]) throw new GradleException('Optional reference hash changed: ' + source.name) } } } - 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() @@ -74,7 +45,7 @@ def installRuntimeMods = { if (!source.isFile()) { throw new GradleException("Missing optional ${mod} jar: ${source}. Set -P${mod}RuntimeJar=.") } - String expected = runtimeLock.getProperty(mod + '.sha256') + String expected = runtimeLock.getProperty(mod + '.development.sha256') String actual = MessageDigest.getInstance('SHA-256').digest(Files.readAllBytes(source.toPath())) .encodeHex().toString() if (!(expected ==~ /(?i)[a-f0-9]{64}/) || !actual.equalsIgnoreCase(expected)) { @@ -205,7 +176,11 @@ def prepareMineralogyIntegrationTest = tasks.register('prepareMineralogyIntegrat if (directory.canonicalFile != expected.canonicalFile) { throw new GradleException('Refusing to clear a Mineralogy verification directory outside the build output') } - project.delete(directory) + if (new File(directory, 'logs/latest.log').isFile()) { + throw new GradleException('Archive the previous optional runtime evidence before preparing another run') + } + directory.mkdirs() + copy { from 'src/bootstrapTest/defaultconfigs'; into directory } copy { from new File(gameDirectory.asFile, 'mods') from tasks.named('integrationTestModJar', Jar).flatMap { it.archiveFile } @@ -226,65 +201,91 @@ generate-structures=false spawn-animals=false spawn-monsters=false spawn-protection=0 +view-distance=6 max-tick-time=120000 '''.replace('level-type=default', probeId == 'skysterrainsmootherbop' ? 'level-type=BIOMESOP' : 'level-type=default'), 'UTF-8') } } +def auditOptionalPhase = { File directory, String phase, boolean server -> + File marker = new File(directory, (server ? 'world/' : 'saves/terrain-smoother-world/') + + probeId + '-integration.properties') + Properties evidence = new Properties() + if (!marker.isFile()) throw new GradleException('Optional runtime marker is missing: ' + phase) + marker.withInputStream { evidence.load(it) } + String completed = phase.endsWith('reload') ? 'reload_complete' : 'fresh_complete' + if (evidence.getProperty(completed) != 'true') { + throw new GradleException('Optional runtime did not complete: ' + phase) + } + List arguments = [providers.gradleProperty('pythonExecutable').getOrElse('python'), + 'tools/native_client_log.py', '--directory', directory.absolutePath, '--phase', phase] + if (server) arguments.add('--server') + project.services.get(org.gradle.process.ExecOperations).exec { commandLine arguments } + copy { from new File(directory, 'logs'); into new File(directory, 'phase-logs/' + phase) } +} + 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') + doLast { auditOptionalPhase(verificationDirectory.get().asFile, 'fresh', true) } } 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') + doLast { auditOptionalPhase(verificationDirectory.get().asFile, 'reload', true) } } tasks.register('integrationMineralogyTest') { group = 'verification' dependsOn reload +} + +def optionalClientDirectory = layout.buildDirectory.dir('mineralogy-client-run') +def prepareOptionalClient = tasks.register('prepareMineralogyIntegrationClient') { + dependsOn prepareMineralogyDevelopmentRun, tasks.named('integrationTestModJar') 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') + File directory = optionalClientDirectory.get().asFile + if (new File(directory, 'logs/latest.log').isFile()) { + throw new GradleException('Archive the previous optional client evidence before preparing another run') } - if (!fileTree(new File(directory, 'crash-reports')).isEmpty()) { - throw new GradleException('Mineralogy verification produced a crash report') + copy { from 'src/bootstrapTest/defaultconfigs'; into directory } + copy { + from new File(gameDirectory.asFile, 'mods') + from tasks.named('integrationTestModJar', Jar).flatMap { it.archiveFile } + into new File(directory, 'mods') } + new File(directory, 'options.txt').setText( + 'fullscreen:false\nmaxFps:30\nrenderDistance:6\n', 'UTF-8') } } - -tasks.register('integrationMineralogyClientProcess', JavaExec) { +def optionalClientFresh = tasks.register('integrationMineralogyClientProcess', JavaExec) { group = 'verification' - dependsOn prepareMineralogyIntegrationTest - workingDir verificationDirectory - systemProperty probeId + '.integrationPhase', 'client' - systemProperty 'log4j.configurationFile', developmentLogging + dependsOn prepareOptionalClient + workingDir optionalClientDirectory + systemProperty probeId + '.integrationPhase', 'client-fresh' 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') - } - } + doLast { auditOptionalPhase(optionalClientDirectory.get().asFile, 'client-fresh', false) } +} +tasks.register('integrationMineralogyClientReloadProcess', JavaExec) { + group = 'verification' + dependsOn optionalClientFresh + workingDir optionalClientDirectory + systemProperty probeId + '.integrationPhase', 'client-reload' + systemProperty 'skysterrainsmoother.requireMineralogy', 'true' + systemProperty 'skysterrainsmoother.testMineralogyShapes', 'false' + configureVerification(delegate, 'runClient') + doLast { auditOptionalPhase(optionalClientDirectory.get().asFile, 'client-reload', false) } } diff --git a/gradle/mineralogy-runtime.properties b/gradle/mineralogy-runtime.properties index 1b673d9..f868e49 100644 --- a/gradle/mineralogy-runtime.properties +++ b/gradle/mineralogy-runtime.properties @@ -1,5 +1,9 @@ -# 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 +# Optional local reference jars; none are bundled or required by this mod. +mineralogy.path=../../../MinecraftMineralogy/MinecraftMineralogy 114-new/MinecraftMineralogy/build/libs/Mineralogy-6.0.1.114041.jar +mineralogy.sha256=75CE58BCDCB845407DE55F476FDB06211E2F8DBE6FCB0AC0F7FBDB5B7841F7BA +mineralogy.development.path=../../../MinecraftMineralogy/MinecraftMineralogy 114-new/MinecraftMineralogy/build/libs-dev/Mineralogy-6.0.1.114041-deobf.jar +mineralogy.development.sha256=FBB03D6B980C8786AC218D2C2615A6E15E87EE55DE7577D29905116D023782CE +orespawn.path=../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar +orespawn.sha256=219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294 +orespawn.development.path=../../../MinecraftMineralogy/OS114/OreSpawn/build/libs-dev/OreSpawn-4.1.1.114041-deobf.jar +orespawn.development.sha256=1807C3A43F6E854F4EC5741F75A52750B733CD81DC588CF0F666C6C3A489FBEF diff --git a/gradle/recipe-advancements.gradle b/gradle/recipe-advancements.gradle index 51bf68d..2984f24 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}/advancements/recipes/${path}.json") target.parentFile.mkdirs() target.setText(JsonOutput.prettyPrint(JsonOutput.toJson(advancement)) + '\n', 'UTF-8') } diff --git a/src/bootstrapTest/defaultconfigs/config/forge-client.toml b/src/bootstrapTest/defaultconfigs/config/forge-client.toml new file mode 100644 index 0000000..452bdef --- /dev/null +++ b/src/bootstrapTest/defaultconfigs/config/forge-client.toml @@ -0,0 +1,22 @@ + +#Client only settings, mostly things related to rendering +[client] + #Toggle off to make missing model text in the gui fit inside the slot. + zoomInMissingModelTextInGui = false + #Enable uploading cloud geometry to the GPU for faster rendering. + forgeCloudsEnabled = true + #Disable culling of hidden faces next to stairs and slabs. Causes extra rendering, but may fix some resource packs that exploit this vanilla mechanic. + disableStairSlabCulling = false + #Enable forge to queue all chunk updates to the Chunk Update thread. + #May increase FPS significantly, but may also cause weird rendering lag. + #Not recommended for computers without a significant number of cores available. + alwaysSetupTerrainOffThread = false + #Enable the forge block rendering pipeline - fixes the lighting of custom models. + forgeLightPipelineEnabled = true + #When enabled, makes specific reload tasks such as language changing quicker to run. + selectiveResourceReloadEnabled = true + #When enabled, forge will show any warnings that occurred during loading + showLoadWarnings = true + #Allow item rendering to detect emissive quads and draw them properly. This allows glowing blocks to look the same in item form, but incurs a very slight performance hit. + allowEmissiveItems = true + diff --git a/src/bootstrapTest/defaultconfigs/defaultconfigs/forge-server.toml b/src/bootstrapTest/defaultconfigs/defaultconfigs/forge-server.toml new file mode 100644 index 0000000..0183ad7 --- /dev/null +++ b/src/bootstrapTest/defaultconfigs/defaultconfigs/forge-server.toml @@ -0,0 +1,30 @@ + +#Server configuration settings +[server] + #The time in ticks the server will wait when a dimension was queued to unload. This can be useful when rapidly loading and unloading dimensions, like e.g. throwing items through a nether portal a few time per second. + #Range: > 0 + dimensionUnloadQueueDelay = 0 + #Base zombie summoning spawn chance. Allows changing the bonus zombie summoning mechanic. + #Range: 0.0 ~ 1.0 + zombieBaseSummonChance = 0.1 + #Chance that a zombie (or subclass) is a baby. Allows changing the zombie spawning mechanic. + #Range: 0.0 ~ 1.0 + zombieBabyChance = 0.05 + #Set this to true to remove any Entity 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. + 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 + #Fix advancement loading to use a proper topological sort. This may have visibility side-effects and can thus be turned off if needed for data-pack compatibility. + fixAdvancementLoading = true + #Log cascading chunk generation issues during terrain population. + logCascadingWorldGeneration = true + #Vanilla will treat crafting recipess using empty tags as air, and allow you to craft with nothing in that slot. This changes empty tags to use BARRIER as the item. To prevent crafting with air. + treatEmptyTagsAsAir = false + #Controls the number threshold at which Packet51 is preferred over Packet52, default and minimum 64, maximum 1024 + #Range: 64 ~ 1024 + clumpingThreshold = 64 + #Set this to true to remove any TileEntity 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. + removeErroringTileEntities = false + #Fix vanilla issues that cause worldgen cascading. This DOES change vanilla worldgen so DO NOT report bugs related to world differences if this flag is on. + fixVanillaCascading = false + 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..ac03e0a --- /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.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.entity.item.FallingBlockEntity; +import net.minecraft.fluid.Fluids; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.state.properties.SlabType; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.server.FMLServerStartedEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +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() { + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup); + MinecraftForge.EVENT_BUS.addListener(this::started); + } + + private void setup(FMLCommonSetupEvent event) { + require(TerrainSmoothingApi.hasMaterial(new ResourceLocation("minecraft:stone")), + "Built-ins are available from a contributor's native setup callback"); + TerrainSmoothingApi.addBiomeRule((material, biome) -> true); + setupComplete = true; + } + + private void started(FMLServerStartedEvent event) { + ServerWorld world = event.getServer().getWorld(DimensionType.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.get(SandSlabBlock.TYPE) + == ((metadata & 1) == 0 ? SlabType.TOP : SlabType.BOTTOM), "Legacy sand orientation"); + require(!state.get(SandSlabBlock.WATERLOGGED), "Legacy sand begins dry"); + require(block.getStateContainer().getValidStates().size() == 4, "No double sand state"); + CompoundNBT oldStack = new CompoundNBT(); + oldStack.putString("id", "skysterrainsmoother:sand_slab"); + oldStack.putByte("Count", (byte) 17); + oldStack.putShort("Damage", (short) metadata); + CompoundNBT custom = new CompoundNBT(); + custom.putString("Name", "Kept sand"); + oldStack.put("tag", custom); + SandLegacyRecovery.recoverItems(oldStack); + require(oldStack.getString("id").equals(block.getRegistryName().toString()) + && oldStack.getByte("Count") == 17 && custom.equals(oldStack.getCompound("tag")), + "Raw sand stack retains count and custom data"); + CompoundNBT once = oldStack.copy(); + SandLegacyRecovery.recoverItems(oldStack); + require(once.equals(oldStack), "Sand item recovery is idempotent"); + checks += 5; + checks += falling(world, state, metadata); + } + File root = event.getServer().getActiveAnvilConverter() + .getFile(event.getServer().getFolderName(), "level.dat").getParentFile(); + 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); + } + System.out.println("TERRAIN_NATIVE_BOOTSTRAP_PASS checks=" + checks); + event.getServer().initiateShutdown(false); + } + + private static int falling(ServerWorld world, BlockState state, int metadata) { + BlockPos source = new BlockPos(80 + metadata * 4, 220, 80); + world.getChunk(source); + for (int height = 215; height <= 223; height++) { + world.setBlockState(new BlockPos(source.getX(), height, source.getZ()), Blocks.AIR.getDefaultState(), 2); + } + world.setBlockState(source.down(5), Blocks.STONE.getDefaultState(), 2); + world.setBlockState(source, state, 2); + ((SandSlabBlock) state.getBlock()).tick(state, world, source, new java.util.Random(1)); + java.util.List entities = world.getEntitiesWithinAABB(FallingBlockEntity.class, + new AxisAlignedBB(source).grow(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.down(4)).equals(state), + "Falling preserves native sand colour and orientation"); + world.setBlockState(source, state.with(SandSlabBlock.WATERLOGGED, true), 2); + ((SandSlabBlock) state.getBlock()).tick(world.getBlockState(source), world, source, new java.util.Random(1)); + entities = world.getEntitiesWithinAABB(FallingBlockEntity.class, new AxisAlignedBB(source).grow(1)); + entities.removeIf(entity -> !entity.isAlive()); + require(entities.size() == 1, "Wet sand begins falling"); + FallingBlockEntity wet = entities.get(0); + require(!wet.getBlockState().get(SandSlabBlock.WATERLOGGED), "Falling sand carries no source water"); + wet.tick(); + require(world.getFluidState(source).getFluid() == Fluids.WATER, "Water remains at the source"); + wet.remove(); + return 5; + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +} diff --git a/src/bootstrapTest/resources/META-INF/mods.toml b/src/bootstrapTest/resources/META-INF/mods.toml new file mode 100644 index 0000000..f2013a8 --- /dev/null +++ b/src/bootstrapTest/resources/META-INF/mods.toml @@ -0,0 +1,14 @@ +modLoader="javafml" +loaderVersion="[28,)" +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" +mandatory=true +versionRange="[0.1.0.114041,)" +ordering="AFTER" +side="BOTH" diff --git a/src/bootstrapTest/resources/pack.mcmeta b/src/bootstrapTest/resources/pack.mcmeta new file mode 100644 index 0000000..690ef39 --- /dev/null +++ b/src/bootstrapTest/resources/pack.mcmeta @@ -0,0 +1 @@ +{"pack":{"pack_format":4,"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..05003ec 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,19 @@ package zone.moddev.mc.skysterrainsmoother.probe; import java.util.*; -import net.minecraft.block.state.IBlockState; -import net.minecraft.init.Blocks; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraft.world.chunk.Chunk; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.world.gen.structure.MapGenStructureData; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.gen.feature.structure.StructureStart; +import net.minecraft.world.gen.feature.Feature; +import net.minecraft.util.math.MutableBoundingBox; +import net.minecraft.block.RedstoneOreBlock; 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.minecraftforge.event.TickEvent; +import net.minecraftforge.fml.LogicalSide; import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; import zone.moddev.mc.skysterrainsmoother.api.TerrainMaterial; @@ -26,9 +27,9 @@ final class CaveProbe { private CaveProbe() { } - static int run(WorldServer world) { + static int run(ServerWorld world) { BlockPos floor = new BlockPos(3000, 70, 3000); - Chunk owner = world.getChunk(floor); + Chunk owner = world.getChunkAt(floor); clear(world, floor, 69, 255); fixture(world, floor, 6); @@ -55,7 +56,7 @@ static int run(WorldServer world) { } } - IBlockState expected = MaterialCatalogue.instance().result(stone, + BlockState expected = MaterialCatalogue.instance().result(stone, Footprint.mask(cardinal, diagonal, level), level); SkysTerrainSmoother.engine.smooth(world, owner, level); @@ -86,16 +87,19 @@ static int run(WorldServer world) { "low passage remains walkable: air height " + air); checks++; } - for (IBlockState obstacle : new IBlockState[] { Blocks.WATER.getDefaultState(), Blocks.LAVA.getDefaultState(), + for (BlockState obstacle : new BlockState[] { Blocks.WATER.getDefaultState(), Blocks.LAVA.getDefaultState(), Blocks.GLASS.getDefaultState(), Blocks.CHEST.getDefaultState() }) { for (int height = 1; height <= 3; height++) { fixture(world, floor, 6); + RuntimeProbe.require(world.isAirBlock(floor.up()) && world.isAirBlock(floor.up(2)) + && world.isAirBlock(floor.up(3)), "Cave fixture starts with clear headroom"); world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); world.setBlockState(floor.up(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"); + "fluid, occupied target or blocked headroom is protected: " + obstacle + " / " + height + + " / actual=" + world.getBlockState(floor.up())); checks++; } } @@ -108,14 +112,14 @@ static int run(WorldServer world) { 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)); + ProbeWorld.clear(world, floor.up(6)); world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, 1); RuntimeProbe.require(!world.isAirBlock(floor.up()), "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(), + for (BlockState source : new BlockState[] { Blocks.COAL_ORE.getDefaultState(), + Blocks.GRAVEL.getDefaultState(), Blocks.OAK_PLANKS.getDefaultState(), Blocks.INFESTED_STONE.getDefaultState(), Blocks.CHEST.getDefaultState() }) { fixture(world, floor, 6); world.setBlockState(floor, source, 2); @@ -133,7 +137,7 @@ 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); @@ -146,7 +150,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.up(6)).getBlock() == Blocks.BRICKS, "cave ceiling remains intact"); checks += 3; @@ -157,34 +161,33 @@ 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; + StructureStart previous = owner.getStructureStarts().get("Mineshaft"); + StructureStart protectedStart = new StructureStart(Feature.MINESHAFT, owner.getPos().x, owner.getPos().z, + world.getBiome(floor), new MutableBoundingBox(floor.getX() - 2, 69, floor.getZ() - 2, + floor.getX() + 2, 80, floor.getZ() + 2), 0, 1) { + @Override + public void init(net.minecraft.world.gen.ChunkGenerator generator, + net.minecraft.world.gen.feature.template.TemplateManager templates, int chunkX, int chunkZ, + net.minecraft.world.biome.Biome biome) { + } + @Override + public boolean isValid() { + return true; + } + }; + owner.getStructureStarts().put("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); SkysTerrainSmoother.engine.smooth(world, owner, 1); - RuntimeProbe.require(world.isAirBlock(floor.up()), "mineshaft structure bounds are protected underground"); + RuntimeProbe.require(world.isAirBlock(floor.up()), "Native mineshaft bounds protect underground floors"); checks++; } finally { if (previous == null) { - data.getTagCompound().removeTag(key); + owner.getStructureStarts().remove("Mineshaft"); } else { - data.getTagCompound().setTag(key, previous); + owner.getStructureStarts().put("Mineshaft", previous); } } @@ -203,35 +206,33 @@ static int run(WorldServer world) { return checks; } - private static int lateDecoration(WorldServer world, BlockPos floor, Chunk owner) { + private static int lateDecoration(ServerWorld world, BlockPos floor, Chunk owner) { int checks = 0; for (int kind = 0; kind < 4; kind++) { fixture(world, floor, 6); world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 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))); + ProbeWorld.repair(world, new BlockPos(owner.getPos().x << 4, 0, owner.getPos().z << 4)); RuntimeProbe.require(!world.isAirBlock(floor.up()), "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.setBlockState(floor, Blocks.INFESTED_STONE.getDefaultState(), 2); } if (kind == 1) { world.setBlockState(floor, Blocks.SAND.getDefaultState(), 2); } if (kind == 2) { - world.setBlockState(floor.up(2), Blocks.BRICK_BLOCK.getDefaultState(), 2); + world.setBlockState(floor.up(2), Blocks.BRICKS.getDefaultState(), 2); } if (kind == 3) { world.setBlockState(floor, Blocks.SAND.getDefaultState(), 2); world.setBlockState(floor.up(), Blocks.GLASS.getDefaultState(), 2); } - MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(Side.SERVER, TickEvent.Phase.END, world)); + MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(LogicalSide.SERVER, TickEvent.Phase.END, world)); RuntimeProbe.require( kind == 1 ? world.getBlockState(floor.up()) @@ -245,26 +246,24 @@ 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(ServerWorld world, BlockPos floor, Chunk 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(), + for (BlockState ore : new BlockState[] { Blocks.COAL_ORE.getDefaultState(), Blocks.IRON_ORE.getDefaultState(), + Blocks.REDSTONE_ORE.getDefaultState(), Blocks.REDSTONE_ORE.getDefaultState().with(RedstoneOreBlock.LIT, true), RuntimeProbe.probeOre.getDefaultState() }) { for (int level = 1; level <= 3; level++) { fixture(world, floor, 6); world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 2); SkysTerrainSmoother.engine.smooth(world, owner, level); - IBlockState cap = world.getBlockState(floor.up()); + BlockState cap = world.getBlockState(floor.up()); 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)); + ProbeWorld.repair(world, new BlockPos(owner.getPos().x << 4, 0, owner.getPos().z << 4)); + ProbeWorld.queue(world, owner.getPos().x, owner.getPos().z); + MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(LogicalSide.SERVER, TickEvent.Phase.END, world)); RuntimeProbe.require(world.getBlockState(floor.up()).equals(cap), "ore under a generated piece stays hidden: " + ore + " / level " + level); RuntimeProbe.require(world.getBlockState(floor).equals(ore), "ore support is never overwritten"); @@ -276,8 +275,8 @@ private static int oreDecoration(WorldServer world, BlockPos floor, Chunk owner) world.setBlockState(floor.north().up(), Blocks.STONE.getDefaultState(), 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)); + world.setBlockState(floor.up(2), Blocks.BRICKS.getDefaultState(), 2); + MinecraftForge.EVENT_BUS.post(new TickEvent.WorldTickEvent(LogicalSide.SERVER, TickEvent.Phase.END, world)); RuntimeProbe.require(world.isAirBlock(floor.up()) && world.getBlockState(floor).getBlock() == Blocks.COAL_ORE, "ore preservation does not override cave headroom protection"); checks++; @@ -293,27 +292,28 @@ private static int oreDecoration(WorldServer world, BlockPos floor, Chunk owner) return checks; } - private static int manyFloors(WorldServer world) { + private static int manyFloors(ServerWorld world) { Chunk owner = world.getChunk(200, 200); - net.minecraft.world.chunk.storage.ExtendedBlockStorage[] sections = owner.getBlockStorageArray(); + net.minecraft.world.chunk.ChunkSection[] sections = owner.getSections(); for (int section = 0; section < 16; section++) { - sections[section] = new net.minecraft.world.chunk.storage.ExtendedBlockStorage(section * 16, true); + sections[section] = new net.minecraft.world.chunk.ChunkSection(section * 16); } 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[floorY >> 4].setBlockState(blockX, floorY & 15, blockZ, Blocks.STONE.getDefaultState()); } - sections[15].set(blockX, 246 & 15, blockZ, Blocks.BRICK_BLOCK.getDefaultState()); - owner.getHeightMap()[blockX | blockZ << 4] = 247; + sections[15].setBlockState(blockX, 246 & 15, blockZ, Blocks.BRICKS.getDefaultState()); + } } int countBefore = world.getChunkProvider().getLoadedChunkCount(); + ProbeWorld.rebuildHeights(owner); int placed = SkysTerrainSmoother.engine.smooth(world, owner, 1); RuntimeProbe.require(placed > 256, "decision buffer covers multiple floors across all 256 columns"); @@ -327,23 +327,23 @@ private static int manyFloors(WorldServer world) { return 3; } - private static void clear(WorldServer world, BlockPos floor, int minY, int maxY) { + private static void clear(ServerWorld 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(ServerWorld 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.setBlockState(floor.add(blockX, roofOffset, blockZ), Blocks.BRICKS.getDefaultState(), 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..9b4a163 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java @@ -3,39 +3,36 @@ 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.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.event.TickEvent; public final class ClientProbe extends ProbeProxy { private boolean ran; private boolean integrated; + private boolean resourcesChecked; + private int settledWorldTicks; @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); } @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { - Minecraft minecraft = Minecraft.getMinecraft(); + Minecraft minecraft = Minecraft.getInstance(); + + org.lwjgl.glfw.GLFW.glfwHideWindow(minecraft.mainWindow.getHandle()); if (event.phase != TickEvent.Phase.END) { return; } if (ran) { - if (integrated && ClientStatus.complete) { + if (integrated && ClientStatus.complete && minecraft.world != null && minecraft.player != null + && minecraft.getConnection() != null + && !minecraft.getConnection().getRecipeManager().getRecipes().isEmpty()) { + if (++settledWorldTicks < 20) return; + RecipePreviewModels.check(minecraft); + checkBiomeTints(minecraft); System.out.println("TERRAIN_SMOOTHER_CLIENT_WORLD_PASS"); minecraft.shutdown(); integrated = false; @@ -43,33 +40,39 @@ public void tick(TickEvent.ClientTickEvent event) { return; } - if (minecraft.currentScreen == null) { + if (minecraft.currentScreen == null || minecraft.getLoadingGui() != null) { return; } - ran = true; - try { - MineralogyProbe.run(); - FineGrassModelProbe.run(minecraft); - ConfigScreenProbe.run(minecraft); - checkModel(minecraft, RuntimeProbe.probeOre.getDefaultState()); + if (!resourcesChecked) { + MineralogyProbe.run(); + FineGrassModelProbe.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; + for (net.minecraft.block.Block block : new net.minecraft.block.Block[] { + zone.moddev.mc.skysterrainsmoother.content.SandContent.slab, + zone.moddev.mc.skysterrainsmoother.content.SandContent.redSlab }) { + for (net.minecraft.block.BlockState state : block.getStateContainer().getValidStates()) { + checkModel(minecraft, state); } - + } + for (net.minecraft.block.BlockState state : zone.moddev.mc.skysgrassslabs.init.ModBlocks.GRASS_SLAB + .getStateContainer().getValidStates()) { checkModel(minecraft, state); } + for (zone.moddev.mc.skysbuildingpieces.content.PieceBlock block : zone.moddev.mc.skysbuildingpieces.content.Pieces.BLOCKS + .values()) { + for (net.minecraft.block.BlockState state : block.getStateContainer().getValidStates()) { + checkModel(minecraft, state); + } + } + resourcesChecked = true; + ConfigScreenProbe.begin(minecraft); + return; } + if (!ConfigScreenProbe.tick(minecraft)) return; + ran = true; try (Writer writer = new FileWriter(new File(minecraft.gameDir, "terrain-smoother-client-pass.txt"))) { writer.write("TERRAIN_SMOOTHER_CLIENT_PASS\n"); } @@ -81,26 +84,46 @@ public void tick(TickEvent.ClientTickEvent event) { 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, + new net.minecraft.world.WorldSettings(4815162342L, net.minecraft.world.GameType.CREATIVE, false, false, net.minecraft.world.WorldType.DEFAULT)); } else { minecraft.shutdown(); } } - private static void checkModel(Minecraft minecraft, net.minecraft.block.state.IBlockState state) { - net.minecraft.client.renderer.block.model.IBakedModel model = minecraft.getBlockRendererDispatcher() + private static void checkBiomeTints(Minecraft client) { + net.minecraft.util.math.BlockPos sample = new net.minecraft.util.math.BlockPos(client.player); + int expected = client.getBlockColors().getColor(net.minecraft.block.Blocks.GRASS_BLOCK.getDefaultState(), + client.world, 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.getDefaultState(), client.world, 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.getDefaultState(), client.world, sample, 0) + == expected, "Grass slabs retain the actual client biome tint"); + System.out.println("TERRAIN_SMOOTHER_CLIENT_TINT_PASS palettes=" + count); + } + + private static void checkModel(Minecraft minecraft, net.minecraft.block.BlockState state) { + net.minecraft.client.renderer.model.IBakedModel model = minecraft.getBlockRendererDispatcher() .getModelForState(state); RuntimeProbe.require(model != minecraft.getBlockRendererDispatcher().getBlockModelShapes().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.util.Direction face : new net.minecraft.util.Direction[] { null, + net.minecraft.util.Direction.DOWN, net.minecraft.util.Direction.UP, + net.minecraft.util.Direction.NORTH, net.minecraft.util.Direction.SOUTH, + net.minecraft.util.Direction.WEST, net.minecraft.util.Direction.EAST }) { + for (net.minecraft.client.renderer.model.BakedQuad quad : model.getQuads(state, face, new java.util.Random(0))) { + RuntimeProbe.require(!quad.getSprite().getName().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..f85ae62 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,162 @@ 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.gui.screen.Screen; +import net.minecraft.client.gui.widget.button.Button; 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 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.currentScreen; + activeGrass = SkysGrassSlabsConfig.generateGrassSlabs(); + activeTerrain = TerrainSmootherConfig.enabled(); + activeLevel = TerrainSmootherConfig.level(); + modIndex = 0; + tooltips = 0; + stage = 0; + } + + static boolean tick(Minecraft minecraft) { + if (modIndex == 2) { + minecraft.displayGuiScreen(parent); + RuntimeProbe.require(tooltips == 5, "All five settings were exercised"); + System.out.println("SKY_CONFIG_SCREENS_PASS factories=2 widgets=5 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" } + : 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()); + } + choiceIndex = 0; + stage = 1; + } else if (stage == 1) { + Button button = buttons(screen).get(choiceIndex); + String key = "config." + modId + "." + keys[choiceIndex]; + RuntimeProbe.require(!I18n.format(key).equals(key) && !I18n.format(key + ".tooltip").equals(key + ".tooltip"), + "Readable label and tooltip for " + key); + render(minecraft, screen, button.x + 5, button.y + 5); + screenshot(minecraft, modId + "-" + keys[choiceIndex] + "-tooltip.png"); + button.onPress(); + RuntimeProbe.require(!button.getMessage().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.displayGuiScreen(pending); + boolean changed = true; + for (int index = 0; index < keys.length; index++) { + changed &= !buttons(pending).get(index).getMessage().equals(original.get(index)); + } + if (!changed) { + 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 < 5) 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().equals(original.get(index)), "Saved choices are reloaded: " + keys[index]); + for (int attempt = 0; attempt < 3 && !button.getMessage().equals(original.get(index)); attempt++) { + button.onPress(); + } + RuntimeProbe.require(button.getMessage().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.displayGuiScreen(restored); + for (int index = 0; index < keys.length; index++) { + if (!buttons(restored).get(index).getMessage().equals(original.get(index))) { + settleTicks = 0; + return false; + } + } + if (++settleTicks < 5) 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; - - 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 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); - 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"); + static void render(Minecraft minecraft, Screen screen, int mouseX, int mouseY) { + minecraft.getFramebuffer().bindFramebuffer(true); + minecraft.mainWindow.loadGUIRenderMatrix(Minecraft.IS_RUNNING_ON_MAC); + com.mojang.blaze3d.platform.GlStateManager.clear(256, Minecraft.IS_RUNNING_ON_MAC); + screen.render(mouseX, mouseY, 0); } - private static Configuration read(Configuration source) { - Configuration written = new Configuration(source.getConfigFile()); - written.load(); - return written; + private static List