diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fc461d3..fc1a9f76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,78 @@ -name: CI +name: OreSpawn 1.10 CI -on: [push, pull_request] -#on: -# push: -# branches: [ master-1.12 ] -# pull_request: -# # The branches below must be a subset of the branches above -# branches: [ master-1.12 ] -# types: [opened, synchronize, reopened] +on: + push: + branches: + - master-1.10.2 + - 'feature/**' + pull_request: + branches: + - master-1.10.2 + +permissions: + contents: read + +concurrency: + group: orespawn-1.10-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build: + name: Build, test, and audit runs-on: ubuntu-latest - name: Build + timeout-minutes: 60 + steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install Java 8 toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install Java 17 for Gradle + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: microsoft + java-version: '17' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - name: Make the wrapper executable + run: chmod +x ./gradlew + + - name: Build, test, and audit release artifacts + run: >- + ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums + verifyEclipseProductionClasspath --no-daemon --stacktrace + + - name: Upload audited release candidate + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: OreSpawn-1.10.2-${{ github.sha }} + if-no-files-found: error + retention-days: 30 + path: | + build/libs/OreSpawn-4.0.8.110021.jar + build/libs/OreSpawn-4.0.8.110021-sources.jar + build/libs/OreSpawn-4.0.8.110021-javadoc.jar + build/release/SHA256SUMS + CHANGELOG.txt + + - name: Upload diagnostics on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - java-version: 8 - - run: chmod a+x gradlew - - run: ./gradlew --version --no-daemon - - run: ./gradlew setupCIWorkspace -S - - run: ./gradlew clean build -S + name: OreSpawn-1.10.2-diagnostics-${{ github.sha }} + if-no-files-found: ignore + retention-days: 14 + path: | + build/test-results/** + build/reports/** + build/*-run/logs/** + build/legacy-abi/**/run/logs/** + build/*-integration-run/**/*.properties + build/problems/** diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d5a02752..cce7f409 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,73 +1,55 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: [push, pull_request] -#on: -# push: -# branches: [ master-1.12 ] -# pull_request: -# # The branches below must be a subset of the branches above -# branches: [ master-1.12 ] -# types: [opened, synchronize, reopened] -# schedule: -# - cron: '43 7 * * 4' +name: CodeQL + +on: + push: + branches: + - master-1.10.2 + - 'feature/**' + pull_request: + branches: + - master-1.10.2 + schedule: + - cron: '43 7 * * 4' + +permissions: + actions: read + contents: read + security-events: write jobs: analyze: - name: Analyze + name: Analyze Java runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + timeout-minutes: 45 steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install Java 8 toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install Java 17 for Gradle + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: microsoft + java-version: '17' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + with: + languages: java-kotlin + + - name: Compile production code + run: | + chmod +x ./gradlew + ./gradlew clean classes --no-daemon --stacktrace + + - name: Analyze + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml new file mode 100644 index 00000000..0a34f3e7 --- /dev/null +++ b/.github/workflows/release-on-tag.yml @@ -0,0 +1,89 @@ +name: Start OreSpawn release from tag + +on: + push: + tags: + - '*.*.*.*' + +permissions: + actions: write + contents: read + +concurrency: + group: orespawn-release-starter-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + dispatch-release: + name: Validate tag and start guarded release + if: github.repository == 'MinecraftModDevelopmentMods/OreSpawn' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - name: Validate release tag, target metadata, and prior CI + id: validate + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + value() { sed -n "s/^$1=//p" gradle.properties; } + release_version="$(value mod_version)" + minecraft_version="$(value minecraft_version)" + loader_name="$(value loader_name)" + loader_code="$(value loader_code)" + + IFS=. read -r mc_major mc_minor mc_patch extra <<<"$minecraft_version" + if [[ -n "${extra:-}" || -z "${mc_major:-}" || -z "${mc_minor:-}" ]]; then + echo "Invalid minecraft_version=$minecraft_version" >&2 + exit 1 + fi + mc_patch="${mc_patch:-0}" + if [[ ! "$mc_major" =~ ^[0-9]+$ || ! "$mc_minor" =~ ^[0-9]+$ || ! "$mc_patch" =~ ^[0-9]+$ ]]; then + echo "Invalid minecraft_version=$minecraft_version" >&2 + exit 1 + fi + case "$loader_name:$loader_code" in + forge:1|neoforge:2) ;; + *) echo "Invalid loader metadata $loader_name/$loader_code" >&2; exit 1 ;; + esac + printf -v minor_padded '%02d' "$((10#$mc_minor))" + printf -v patch_padded '%02d' "$((10#$mc_patch))" + target_suffix="${mc_major}${minor_padded}${patch_padded}${loader_code}" + + if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.${target_suffix}$ ]]; then + echo "mod_version $release_version does not match $minecraft_version $loader_name target $target_suffix" >&2 + exit 1 + fi + if [[ "$GITHUB_REF_NAME" != "$release_version" ]]; then + echo "Release tag must equal mod_version $release_version; found $GITHUB_REF_NAME" >&2 + exit 1 + fi + + successful_ci="$(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')" + if [[ "$successful_ci" -lt 1 ]]; then + echo "The tagged commit has no successful Build, test, and audit check" >&2 + exit 1 + fi + echo "version=$release_version" >> "$GITHUB_OUTPUT" + + - name: Start default-branch release workflow + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ steps.validate.outputs.version }} + run: | + gh workflow run deploy-release.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref master-1.12 \ + -f mode=publish \ + -f release_ref="$GITHUB_REF_NAME" \ + -f curseforge_channel=release \ + -f confirm_version="$RELEASE_VERSION" diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index c9c52a54..00000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,30 +0,0 @@ -on: [push, pull_request] -#on: -# push: -# branches: -# - master-1.12 -# pull_request: -# types: [opened, synchronize, reopened] -# -name: SonarCloud -jobs: - sonarcloud: - name: SonarCloud Scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - # Disabling shallow clone is recommended for improving relevancy of reporting - fetch-depth: 0 - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -# SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - - name: SonarCloud Quality Gate check - uses: SonarSource/sonarqube-quality-gate-action@master - # Force to fail step after specific time - timeout-minutes: 5 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index 528f4b5a..81f7cd4e 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -1,11 +1,23 @@ name: Validate Gradle Wrapper -on: [push, pull_request] +on: + push: + branches: + - master-1.10.2 + - 'feature/**' + pull_request: + branches: + - master-1.10.2 + +permissions: + contents: read jobs: validation: - name: "Validation" + name: Validation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate wrapper integrity + uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 213c5dca..b19b787c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.8.110021 + +* Preserve all eight ore/fluid host, tag, and biome editor fields when saved values exceed Minecraft 1.10.2's default 32-character text limit. +* Prevent opening and saving an unchanged editor profile from silently replacing long identifiers or comma-separated lists with truncated values. +* Add reproducible ForgeGradle 7 builds and guarded Maven, CurseForge, and GitHub release automation. +* Keep API major 1, provider/global/world schemas 4/6/5, registry and profile compatibility, migrations, and world-generation semantics unchanged. + Version 4.0.6.110021 * Adopt target-qualified four-component versions so Minecraft and loader compatibility can be identified from the mod version. @@ -32,6 +39,7 @@ Version 4.0.6.110021 * Preserve vanilla springs in provider rocks through Forge's native terrain hooks. * Restore reliable 1.10.2 editor backgrounds, tooltip clearing, translated cycle-button captions, and safe OreSpawn world-settings navigation. +* Preserve long host-block, host-tag and biome-filter values when reopening the ore and fluid editors. * Package translations under Minecraft 1.10's required `ll_CC.lang` filename casing so editor captions load instead of displaying raw language keys. * Existing chunks are not rewritten; the correction applies while generating new chunks. diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index a3081bfe..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,128 +0,0 @@ -pipeline { - agent any - environment { - GRADLE_OPTS = '-Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dorg.gradle.warning.mode=all' -// JAVA_OPTS = '' - } - options { - ansiColor('xterm') - } - tools { -// git 'Git' - gradle 'Gradle 4.9' - jdk 'oraclejdk8' - } - stages { - stage('prebuild') { - steps { - sh 'rm -rf build/libs' - sh 'chmod +x gradlew' - sh 'java -version' - sh 'gradle -version' - sh './gradlew -version' - sh 'export' - } - } - stage('CIWorkspace') { - steps { - withGradle { - sh './gradlew clean setupCiWorkspace -S' - } - } - } - stage('build') { - steps { - withGradle { - sh './gradlew build -S' - } - } - } - stage('test') { - steps { - withGradle { - sh './gradlew test -S' - } - } - } - stage('publish') { - steps { - withCredentials([file(credentialsId: 'secret.json', variable: 'SECRET_FILE')]) { - withGradle { - sh './gradlew publish -S' - } - } - } - } - stage('CurseForge') { - steps { - withCredentials([file(credentialsId: 'secret.json', variable: 'SECRET_FILE')]) { - withGradle { - sh './gradlew -x publish curseforge -S' - } - } - } - } - stage('SonarQube') { - tools { - jdk "oraclejdk11" - } - environment { - scannerHome = tool 'SonarQube' - } - steps { -// withCredentials([file(credentialsId: 'secret.json', variable: 'SECRET_FILE')]) { -// withGradle { -// sh './gradlew sonarqube -S' -// } -// } - withSonarQubeEnv(installationName: 'SonarCloud', , envOnly: false) { - sh "${scannerHome}/bin/sonar-scanner -Dsonar.java.jdkHome=${JAVA_HOME}" - } - } - } - stage('postbuild') { - steps { - archiveArtifacts artifacts: 'build/libs/*.jar', followSymlinks: false - javadoc javadocDir: 'build/docs/javadoc', keepAll: false - fingerprint 'build/libs/*.zip' - junit allowEmptyResults: true, testResults: '**/build/test-results/junit-platform/*.xml' - jacoco classPattern: '**/build/classes/java', execPattern: '**/build/jacoco/**.exec', sourceInclusionPattern: '**/*.java', sourcePattern: '**/src/main/java' - findBuildScans() - recordIssues(tools: [java()]) - recordIssues(tools: [javaDoc()]) -// if (fileExists('')) { -// recordIssues(tools: [errorProne(pattern: 'ReportFilePattern', reportEncoding: 'UTF-8')]) -// } else { -// echo 'No ErrorProne report available' -// } - if (fileExists('**/build/reports/checkstyle/*.xml')) { - recordIssues(tools: [checkStyle(pattern: '**/build/reports/checkstyle/*.xml')]) - } else { - echo 'No CheckStyle report available' - } - if (fileExists('**/build/reports/pmd/*.xml')) { - recordIssues(tools: [pmdParser(pattern: '**/build/reports/pmd/*.xml')]) - } else { - echo 'No PMD report available' - } - if (fileExists('*/build/reports/findbugs/*.xml')) { - recordIssues(tools: [findBugs(pattern: '*/build/reports/findbugs/*.xml', useRankAsPriority: true)]) - } else { - echo 'No FindBugs report available' - } - } - when { expression { fileExists('**/build/reports/spotbugs/*.xml') } } - steps { - recordIssues(tools: [spotBugs(pattern: '**/build/reports/spotbugs/*.xml', useRankAsPriority: true)]) - } - when { expression { fileExists('**/build/test-results/junit-platform/*.xml') } } - steps { - recordIssues(tools: [junitParser(pattern: '**/build/test-results/junit-platform/*.xml')]) - } - when { expression { fileExists('**/sonar-report.json') } } - steps { - recordIssues(tools: [sonarQube(pattern: '**/sonar-report.json')]) - } - } - } -} diff --git a/README.md b/README.md index 6cc1877e..0ffe6d38 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.0.6.110021`: the OreSpawn 4.0.6 +This branch builds target-qualified version `4.0.8.110021`: the OreSpawn 4.0.8 feature set for Minecraft 1.10.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. @@ -104,12 +104,12 @@ exported to `config/orespawn-guide/` without overwriting existing files. ## Building -Use a Java 8 JDK from the repository root: +Run Gradle with JDK 17 from the repository root. The build uses a Java 8 +toolchain for compilation and every executable Minecraft/Forge gate: ```powershell -.\gradlew.bat clean build javadoc --no-daemon -.\gradlew.bat setupDecompWorkspace --no-daemon -.\gradlew.bat eclipse --no-daemon +.\gradlew.bat clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums --no-daemon +.\gradlew.bat genEclipseRuns verifyEclipseProductionClasspath --no-daemon ``` `build` runs the standard `check` lifecycle. In addition to the JUnit suite, @@ -121,14 +121,17 @@ survive, validates provider-rock vanilla springs and an external ore-pattern registration, then reopens and checks the exact saved world. The fixture is not included in OreSpawn's published jars. -Run both `setupDecompWorkspace` and `eclipse` after importing or refreshing this -ForgeGradle 2.2 project in Eclipse. This branch uses the Gradle 4.9 wrapper, -Forge 12.18.3.2511, the `stable_29` MCP mappings, and pack format 2. Published -jars are SRG-reobfuscated for the Forge 1.10 runtime. - -The publication contains five artifacts: the reobfuscated runtime jar, a -compiled `api` classifier with API 1 plus the supported OS1/OS3 facades, a -deobfuscated `deobf` development jar, sources, and Javadocs. +Import or refresh this ForgeGradle 7 project through Eclipse Buildship, then run +`genEclipseRuns`; do not run the removed ForgeGradle 2 +`setupDecompWorkspace` task or generate legacy Java-only Eclipse metadata. This +branch uses the Gradle 9.6.1 wrapper, ForgeGradle 7.0.34, Renamer 1.1.5, Forge +12.18.3.2511, the `stable` / `29-1.10.2` MCP mappings, and pack format 2. +Published jars are SRG-reobfuscated for the Forge 1.10 runtime. + +The publication contains exactly three artifacts: the reobfuscated runtime jar, +sources, and Javadocs. Callable compiled `api` and deobfuscated `deobf` jars are +retained for local development under `build/libs-dev`; they are excluded from +Maven, CurseForge, GitHub Releases, and the release checksum bundle. Machine-specific `AGENTS.md` and `agent-notes/` files are intentionally ignored. Public developer and AI integration guidance lives in `docs/` and is included diff --git a/build.gradle b/build.gradle index 849ba58f..d65111ea 100644 --- a/build.gradle +++ b/build.gradle @@ -1,86 +1,340 @@ -buildscript { - repositories { - maven { url = 'https://maven.minecraftforge.net/' } - mavenCentral() - } - dependencies { - classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT' - } +import groovy.json.JsonSlurper +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile +import org.apache.tools.ant.filters.FixCrLfFilter + +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'maven-publish' + id 'net.minecraftforge.renamer' version '1.1.5' + id 'net.minecraftforge.accesstransformers' version '2.0.0' + id 'net.minecraftforge.gradle' version '7.0.34' } -apply plugin: 'net.minecraftforge.gradle.forge' -apply plugin: 'eclipse' -apply plugin: 'maven-publish' +group = project.mod_group +version = project.mod_version +base.archivesName = 'OreSpawn' -eclipse { - classpath { - // ForgeGradle 2.2 launches use the whole Eclipse project classpath. - // Keep ordinary client/server runs production-only; the integration - // fixtures have dedicated Gradle-backed Eclipse launchers below. - sourceSets = [sourceSets.main] - file.whenMerged { classpath -> - classpath.entries.removeAll { entry -> - String path = entry.path == null ? '' : entry.path.toString() - path == 'src/test/java' || ['/junit-', '/opentest4j/', '/org.apiguardian/'].any { - path.contains(it) - } - } - } +def versionParts = project.mod_version.toString().tokenize('.') +if (versionParts.size() != 4 || !versionParts.every { it ==~ /\d+/ }) { + throw new GradleException("mod_version must use Major.Minor.Bug.Target numeric form: ${project.mod_version}") +} + +def minecraftVersionParts = project.minecraft_version.toString().tokenize('.') +if (minecraftVersionParts.size() < 2 || minecraftVersionParts.size() > 3 + || !minecraftVersionParts.every { it ==~ /\d+/ }) { + throw new GradleException("minecraft_version must use major.minor or major.minor.patch numeric form: " + + project.minecraft_version) +} +def minecraftPatch = minecraftVersionParts.size() == 3 ? minecraftVersionParts[2] : '0' +def expectedTargetVersion = "${minecraftVersionParts[0]}" \ + + "${minecraftVersionParts[1].padLeft(2, '0')}" \ + + "${minecraftPatch.padLeft(2, '0')}" \ + + project.loader_code +if (versionParts[3] != expectedTargetVersion) { + throw new GradleException("mod_version target ${versionParts[3]} does not match " + + "Minecraft ${project.minecraft_version} ${project.loader_name} target ${expectedTargetVersion}") +} + +ext.functional_version = versionParts[0..2].join('.') +ext.display_version = project.mod_version +ext.release_tag = project.mod_version + +java { + toolchain.languageVersion = JavaLanguageVersion.of(8) + withSourcesJar() + withJavadocJar() +} + +tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(8) } + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + options.encoding = 'UTF-8' + options.compilerArgs.addAll(['-Xmaxerrs', '1000']) +} + +tasks.named('compileTestJava', JavaCompile) { + options.compilerArgs.add('-proc:none') +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() + workingDir = project.projectDir +} + +tasks.withType(Javadoc).configureEach { + failOnError = false + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') + options.addBooleanOption('notimestamp', true) } -version = mod_version -group = mod_group_id -archivesBaseName = "OreSpawn-${minecraft_version}" -sourceCompatibility = targetCompatibility = '1.8' +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false + reproducibleFileOrder = true +} -println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}" +def archiveTextSuffixes = [ + '.cfg', '.css', '.html', '.info', '.java', '.js', '.json', '.lang', + '.mcmeta', '.md', '.properties', '.txt', '.xml' +] +def archiveTextPatterns = archiveTextSuffixes.collect { "**/*${it}".toString() } +archiveTextPatterns.addAll(['**/element-list', '**/package-list']) +def normalizeArchiveLineEndings = { details -> + details.filter(FixCrLfFilter, + eol: FixCrLfFilter.CrLf.newInstance('lf'), + eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) +} minecraft { - version = "${minecraft_version}-${forge_version}" - mappings = mapping_version - runDir = 'run' - makeObfSourceJar = false - accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg') - serverRunArgs += 'nogui' + mappings channel: project.mapping_channel, version: project.mapping_version + accessTransformer = 'META-INF/accesstransformer.cfg' + + runs { + configureEach { + workingDir.convention layout.projectDirectory.dir('run') + systemProperty 'forge.logging.console.level', 'debug' + } + register('client') + register('server') { + args '--nogui' + } + } +} + +// Forge 1.10 expects each development mod to expose classes and resources from +// one classpath root. ForgeGradle 7 splits those outputs, so build a separate +// merged launch root without contaminating either canonical source-set output. +def forge10DevelopmentOutput = layout.buildDirectory.dir('forge10-development/main') +def syncForge10DevelopmentResources = tasks.register('syncForge10DevelopmentResources', Sync) { + group = 'ide' + description = 'Merges processed resources into the Forge 1.10 development launch root.' + dependsOn tasks.named('classes') + from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory } + from sourceSets.main.output.resourcesDir + into forge10DevelopmentOutput +} +tasks.configureEach { + if (name == 'runClient' || name == 'runServer') { + dependsOn syncForge10DevelopmentResources + doFirst { + Set splitMainOutputs = [ + tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile, + sourceSets.main.output.resourcesDir + ] as Set + classpath = files(forge10DevelopmentOutput, classpath.filter { + !splitMainOutputs.contains(it) + }) + } + } } repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven + + exclusiveContent { + forRepository { + maven { + name = 'Sponge' + url = 'https://repo.spongepowered.org/repository/maven-public' + } + } + filter { includeGroupAndSubgroups('org.spongepowered') } + } + mavenCentral() + maven { + name = 'MinecraftLibraries' + url = 'https://libraries.minecraft.net/' + } +} + +def legacyFixtureRoot = file("${rootDir}/ci-fixtures") +def legacyFixtureArtifacts = new File(legacyFixtureRoot, 'artifacts') +def legacyFixtureWorlds = new File(legacyFixtureRoot, 'worlds') +def legacyMineralogyOracleJar = new File( + legacyFixtureArtifacts, 'Mineralogy-1.10.2-3.3.8.26.jar') +def legacyFixtureHashes = [ + 'artifacts/OreSpawn_1.10.2-1.1.0.jar': + '91345E4B825AA06E05F470066F467AABA0C7A2804A8DBCA4D3CD1905ED53607C', + 'artifacts/OreSpawn-1.10.2-3.2.2.104.jar': + 'D964148C20175E16CCEFAB02AE43A181A50DC08A1D636A77E59CFE8C8EA8D41A', + 'artifacts/BaseMetals_1.10.2-2.4.0.11.jar': + 'F919CEF794C73F02758BBD967346DBA0B53F6383153C7E29BD79F211CDA8C319', + 'artifacts/Mineralogy-1.10.2-3.3.8.26.jar': + '88A6237C9A0E2C8891718B68C373E741C78B8494F5E68D8093CA9339F3BC4D87', + 'worlds/sylvester-era-trio-source-v2.zip': + '11FC2FA44C43520AA9B21C29B43203EBFE0A84773D2BDCBB4F3C48C490559B07' +] + +task verifyLegacyFixtures { + group = 'verification' + description = 'Verifies the sealed ABI and migration corpus used only by isolated tests.' + inputs.files legacyFixtureHashes.keySet().collect { new File(legacyFixtureRoot, it) } + doLast { + legacyFixtureHashes.each { String relativePath, String expectedHash -> + File fixture = new File(legacyFixtureRoot, relativePath) + if (!fixture.isFile()) { + throw new GradleException("Missing sealed legacy fixture: ${fixture}") + } + MessageDigest digest = MessageDigest.getInstance('SHA-256') + fixture.withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + String actualHash = digest.digest().encodeHex().toString().toUpperCase() + if (actualHash != expectedHash) { + throw new GradleException("Legacy fixture hash mismatch for ${relativePath}: " + + "expected ${expectedHash}, found ${actualHash}") + } + } + } } dependencies { - testCompile 'org.junit.jupiter:junit-jupiter-api:5.10.2' - testCompile 'org.junit.jupiter:junit-jupiter-params:5.10.2' - testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.10.2' - testRuntime 'org.junit.platform:junit-platform-launcher:1.10.2' - testCompile files("${rootDir}/../migration-fixtures/sources/artifacts/Mineralogy-1.10.2-3.3.8.26.jar") + implementation minecraft.dependency( + "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}") + compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' + // Buildship must see this as a test-scoped dependency so Eclipse can + // compile the parity test. The Eclipse launch guard excludes all + // test-scoped dependencies from ordinary client/server runs. + testImplementation files(legacyMineralogyOracleJar) } -tasks.withType(JavaCompile) { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' - options.encoding = 'UTF-8' - options.compilerArgs += ['-Xmaxerrs', '1000'] +tasks.named('compileTestJava', JavaCompile) { + dependsOn verifyLegacyFixtures +} +tasks.named('test', Test) { + dependsOn verifyLegacyFixtures } -test { - useJUnitPlatform() +tasks.register('verifyLegacyOracleIsolation') { + group = 'verification' + description = 'Keeps the sealed Mineralogy oracle test-visible but production-invisible.' + dependsOn verifyLegacyFixtures + + doLast { + File oracle = legacyMineralogyOracleJar.canonicalFile + def canonicalFiles = { FileCollection classpath -> + classpath.files.collect { it.canonicalFile } as Set + } + Set testCompileFiles = canonicalFiles(sourceSets.test.compileClasspath) + Set testRuntimeFiles = canonicalFiles(sourceSets.test.runtimeClasspath) + if (!testCompileFiles.contains(oracle) || !testRuntimeFiles.contains(oracle)) { + throw new GradleException('The sealed Mineralogy oracle is missing from the test classpath model') + } + + Map productionClasspaths = [ + mainCompile: sourceSets.main.compileClasspath, + mainRuntime: sourceSets.main.runtimeClasspath + ] + productionClasspaths.each { String name, FileCollection classpath -> + if (canonicalFiles(classpath).contains(oracle)) { + throw new GradleException("The sealed Mineralogy oracle leaked into ${name}") + } + } + } +} +tasks.named('check') { + dependsOn tasks.named('verifyLegacyOracleIsolation') +} + +def java8Launcher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) +} +def configureFromForgeRun = { JavaExec process, String runTaskName -> + process.actions.clear() + process.dependsOn { + JavaExec run = tasks.getByName(runTaskName) as JavaExec + run.taskDependencies.getDependencies(run) + } + process.doLast { + JavaExec run = tasks.getByName(runTaskName) as JavaExec + String runConfigurationName = runTaskName.substring(3).uncapitalize() + def runOptions = minecraft.runs.getByName(runConfigurationName) + File originalConfiguredWorkingDir = runOptions.workingDir.get().asFile + File originalWorkingDir = run.workingDir + List originalArgs = new ArrayList<>(run.args) + List originalJvmArgs = new ArrayList<>(run.jvmArgs) + Map originalSystemProperties = new LinkedHashMap<>(run.systemProperties) + Map originalEnvironment = new LinkedHashMap<>(run.environment) + FileCollection originalClasspath = files(run.classpath.files) + String originalMinHeap = run.minHeapSize + String originalMaxHeap = run.maxHeapSize + String originalMainClass = run.mainClass.orNull + try { + File forgeConfig = new File(process.workingDir, 'config/forge.cfg') + if (!forgeConfig.isFile()) { + forgeConfig.parentFile.mkdirs() + forgeConfig.setText('''general { + B:disableVersionCheck=true +} + +version_checking { + B:Global=false +} +''', 'UTF-8') + } + runOptions.workingDir.set(process.workingDir) + run.workingDir(process.workingDir) + run.args(process.args) + run.jvmArgs(process.jvmArgs) + run.systemProperties(process.systemProperties) + run.environment(process.environment) + Set splitMainOutputs = [ + tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile, + sourceSets.main.output.resourcesDir + ] as Set + run.setClasspath(files(forge10DevelopmentOutput, run.classpath.filter { + !splitMainOutputs.contains(it) + })) + run.exec() + } finally { + runOptions.workingDir.set(originalConfiguredWorkingDir) + run.workingDir(originalWorkingDir) + run.setArgs(originalArgs) + run.setJvmArgs(originalJvmArgs) + run.setSystemProperties(originalSystemProperties) + run.setEnvironment(originalEnvironment) + run.setClasspath(originalClasspath) + run.minHeapSize = originalMinHeap + run.maxHeapSize = originalMaxHeap + if (originalMainClass == null) { + run.mainClass.unset() + } else { + run.mainClass.set(originalMainClass) + } + } + } } def surfaceIntegrationClasses = file("${buildDir}/surface-integration-fixture/classes") task compileSurfaceIntegrationTestMod(type: JavaCompile, dependsOn: classes) { source fileTree('src/biomeIntegrationTest/java') classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) - destinationDir = surfaceIntegrationClasses - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + destinationDirectory = surfaceIntegrationClasses options.encoding = 'UTF-8' } task surfaceIntegrationTestModJar(type: Jar, dependsOn: compileSurfaceIntegrationTestMod) { - archiveName = 'surfaceprobe.jar' - destinationDir = file("${buildDir}/surface-integration-fixture") + archiveFileName = 'surfaceprobe.jar' + destinationDirectory = file("${buildDir}/surface-integration-fixture") from surfaceIntegrationClasses from 'src/biomeIntegrationTest/resources' } @@ -91,7 +345,7 @@ task prepareSurfaceIntegrationTest(dependsOn: surfaceIntegrationTestModJar) { delete surfaceIntegrationRunDirectory surfaceIntegrationRunDirectory.mkdirs() copy { - from surfaceIntegrationTestModJar.archivePath + from surfaceIntegrationTestModJar.archiveFile into new File(surfaceIntegrationRunDirectory, 'mods') } new File(surfaceIntegrationRunDirectory, 'server.properties').setText('''\ @@ -111,16 +365,9 @@ max-tick-time=-1 def createSurfaceProcess = { String phase, Object dependency -> task("surfaceIntegration${phase}Process", type: JavaExec, dependsOn: dependency) { group = 'verification' - dependsOn { runServer.taskDependencies.getDependencies(runServer) } - main = runServer.main - args runServer.args workingDir surfaceIntegrationRunDirectory systemProperty 'surfaceprobe.integrationPhase', phase.toLowerCase() - doFirst { - // ForgeGradle 2 materializes GradleStartServer during makeStart, so - // copy the development classpath only after that dependency runs. - classpath = runServer.classpath - } + configureFromForgeRun(delegate, 'runServer') } } @@ -163,15 +410,13 @@ def migrationIntegrationClasses = file("${buildDir}/migration-integration-fixtur task compileMigrationIntegrationTestMod(type: JavaCompile, dependsOn: classes) { source fileTree('src/migrationIntegrationTest/java') classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) - destinationDir = migrationIntegrationClasses - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + destinationDirectory = migrationIntegrationClasses options.encoding = 'UTF-8' } task migrationIntegrationTestModJar(type: Jar, dependsOn: compileMigrationIntegrationTestMod) { - archiveName = 'migrationprobe.jar' - destinationDir = file("${buildDir}/migration-integration-fixture") + archiveFileName = 'migrationprobe.jar' + destinationDirectory = file("${buildDir}/migration-integration-fixture") from migrationIntegrationClasses from 'src/migrationIntegrationTest/resources' } @@ -180,7 +425,7 @@ if (project.hasProperty('migrationRunDir')) { def migrationRunDirectory = file(project.property('migrationRunDir')) task prepareMigrationIntegrationRun(dependsOn: migrationIntegrationTestModJar) { doLast { - copy { from migrationIntegrationTestModJar.archivePath; into new File(migrationRunDirectory, 'mods') } + copy { from migrationIntegrationTestModJar.archiveFile; into new File(migrationRunDirectory, 'mods') } new File(migrationRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') File serverProperties = new File(migrationRunDirectory, 'server.properties') Properties values = new Properties() @@ -192,13 +437,10 @@ if (project.hasProperty('migrationRunDir')) { } task migrationIntegrationProcess(type: JavaExec, dependsOn: prepareMigrationIntegrationRun) { group = 'verification' - dependsOn { runServer.taskDependencies.getDependencies(runServer) } - main = runServer.main - args runServer.args workingDir migrationRunDirectory systemProperty 'orespawn.migrationFamily', project.findProperty('migrationFamily') ?: 'unspecified' systemProperty 'orespawn.migrationPhase', project.findProperty('migrationPhase') ?: 'fresh' - doFirst { classpath = runServer.classpath } + configureFromForgeRun(delegate, 'runServer') doLast { String phase = project.findProperty('migrationPhase') ?: 'fresh' File marker = new File(migrationRunDirectory, 'world/orespawn4-migration-probe.properties') @@ -217,18 +459,20 @@ if (project.hasProperty('migrationRunDir')) { } def legacyMineralogyMigrationRunDirectory = file("${buildDir}/legacy-mineralogy-migration-run") -def legacyMineralogyMigrationArchive = file("${rootDir}/../migration-fixtures/sources/worlds/sylvester-era-trio-source-v2.zip") +def legacyMineralogyMigrationArchive = new File(legacyFixtureWorlds, + 'sylvester-era-trio-source-v2.zip') -task prepareLegacyMineralogyMigrationRun(dependsOn: migrationIntegrationTestModJar) { +task prepareLegacyMineralogyMigrationRun(dependsOn: [migrationIntegrationTestModJar, + verifyLegacyFixtures]) { doLast { delete legacyMineralogyMigrationRunDirectory copy { from zipTree(legacyMineralogyMigrationArchive); into legacyMineralogyMigrationRunDirectory } File mods = new File(legacyMineralogyMigrationRunDirectory, 'mods') mods.mkdirs() copy { - from file("${rootDir}/../migration-fixtures/sources/artifacts/BaseMetals_1.10.2-2.4.0.11.jar") - from file("${rootDir}/../migration-fixtures/sources/artifacts/Mineralogy-1.10.2-3.3.8.26.jar") - from migrationIntegrationTestModJar.archivePath + from new File(legacyFixtureArtifacts, 'BaseMetals_1.10.2-2.4.0.11.jar') + from new File(legacyFixtureArtifacts, 'Mineralogy-1.10.2-3.3.8.26.jar') + from migrationIntegrationTestModJar.archiveFile into mods } new File(legacyMineralogyMigrationRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') @@ -237,13 +481,10 @@ task prepareLegacyMineralogyMigrationRun(dependsOn: migrationIntegrationTestModJ def configureLegacyMineralogyMigrationProcess = { JavaExec process, String phase -> process.group = 'verification' - process.dependsOn { runServer.taskDependencies.getDependencies(runServer) } - process.main = runServer.main - process.args runServer.args process.workingDir legacyMineralogyMigrationRunDirectory process.systemProperty 'orespawn.migrationFamily', 'legacy-mineralogy-cyano' process.systemProperty 'orespawn.migrationPhase', phase - process.doFirst { process.classpath = runServer.classpath } + configureFromForgeRun(process, 'runServer') process.doLast { File marker = new File(legacyMineralogyMigrationRunDirectory, 'world/orespawn4-migration-probe.properties') @@ -278,40 +519,35 @@ task legacyMineralogyMigrationTest(dependsOn: legacyMineralogyMigrationReload) { check.dependsOn legacyMineralogyMigrationTest -def legacyFixtureArtifacts = file("${rootDir}/../migration-fixtures/sources/artifacts") def os1AbiFixtureClasses = file("${buildDir}/legacy-abi/os1/classes") def os3AbiFixtureClasses = file("${buildDir}/legacy-abi/os3/classes") -task compileOs1AbiFixture(type: JavaCompile) { +task compileOs1AbiFixture(type: JavaCompile, dependsOn: verifyLegacyFixtures) { source fileTree('src/os1AbiFixture/java') classpath = files(sourceSets.main.compileClasspath, new File(legacyFixtureArtifacts, 'OreSpawn_1.10.2-1.1.0.jar')) - destinationDir = os1AbiFixtureClasses - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + destinationDirectory = os1AbiFixtureClasses options.encoding = 'UTF-8' } task os1AbiFixtureJar(type: Jar, dependsOn: compileOs1AbiFixture) { - archiveName = 'os1abiprobe.jar' - destinationDir = file("${buildDir}/legacy-abi/os1") + archiveFileName = 'os1abiprobe.jar' + destinationDirectory = file("${buildDir}/legacy-abi/os1") from os1AbiFixtureClasses from 'src/os1AbiFixture/resources' } -task compileOs3AbiFixture(type: JavaCompile) { +task compileOs3AbiFixture(type: JavaCompile, dependsOn: verifyLegacyFixtures) { source fileTree('src/os3AbiFixture/java') classpath = files(sourceSets.main.compileClasspath, new File(legacyFixtureArtifacts, 'OreSpawn-1.10.2-3.2.2.104.jar')) - destinationDir = os3AbiFixtureClasses - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + destinationDirectory = os3AbiFixtureClasses options.encoding = 'UTF-8' } task os3AbiFixtureJar(type: Jar, dependsOn: compileOs3AbiFixture) { - archiveName = 'os3abiprobe.jar' - destinationDir = file("${buildDir}/legacy-abi/os3") + archiveFileName = 'os3abiprobe.jar' + destinationDirectory = file("${buildDir}/legacy-abi/os3") from os3AbiFixtureClasses from 'src/os3AbiFixture/resources' } @@ -321,7 +557,7 @@ def configureLegacyAbiProcess = { String generation, Task fixtureJar, String mar Task prepare = tasks.create("prepare${generation.capitalize()}AbiRun", Copy) { dependsOn fixtureJar into new File(runDirectory, 'mods') - from fixtureJar.archivePath + from fixtureJar.archiveFile doFirst { project.delete(runDirectory) } doLast { new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') @@ -333,11 +569,8 @@ def configureLegacyAbiProcess = { String generation, Task fixtureJar, String mar tasks.create("${generation}AbiIntegrationProcess", JavaExec) { group = 'verification' dependsOn prepare - dependsOn { runServer.taskDependencies.getDependencies(runServer) } - main = runServer.main - args runServer.args workingDir runDirectory - doFirst { classpath = runServer.classpath } + configureFromForgeRun(delegate, 'runServer') doLast { File marker = new File(runDirectory, "world/${markerName}") if (!marker.isFile() || !marker.text.contains('registered=true')) { @@ -359,30 +592,41 @@ task legacyAbiIntegrationTest { dependsOn os1AbiIntegrationProcess, os3AbiIntegrationProcess } +check.dependsOn legacyAbiIntegrationTest + def clientIntegrationClasses = file("${buildDir}/client-integration-fixture/classes") task compileClientIntegrationTestMod(type: JavaCompile, dependsOn: classes) { source fileTree('src/clientIntegrationTest/java') classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) - destinationDir = clientIntegrationClasses - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + destinationDirectory = clientIntegrationClasses options.encoding = 'UTF-8' } task clientIntegrationTestModJar(type: Jar, dependsOn: compileClientIntegrationTestMod) { - archiveName = 'clientprobe.jar' - destinationDir = file("${buildDir}/client-integration-fixture") + archiveFileName = 'clientprobe.jar' + destinationDirectory = file("${buildDir}/client-integration-fixture") from clientIntegrationClasses from 'src/clientIntegrationTest/resources' } +def packagedClientProbeJar = renamer.classes(tasks.named('clientIntegrationTestModJar', Jar)) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('client-integration-fixture/clientprobe-reobf.jar') +} + +tasks.register('preparePackagedClientProbe') { + group = 'verification' + description = 'Builds the reobfuscated client probe used only with a disposable packaged Forge runtime.' + dependsOn packagedClientProbeJar +} + def clientIntegrationRunDirectory = file("${buildDir}/client-integration-run") task prepareClientIntegrationTest(dependsOn: clientIntegrationTestModJar) { doLast { delete clientIntegrationRunDirectory clientIntegrationRunDirectory.mkdirs() copy { - from clientIntegrationTestModJar.archivePath + from clientIntegrationTestModJar.archiveFile into new File(clientIntegrationRunDirectory, 'mods') } new File(clientIntegrationRunDirectory, 'options.txt').setText( @@ -393,12 +637,9 @@ task prepareClientIntegrationTest(dependsOn: clientIntegrationTestModJar) { task clientIntegrationProcess(type: JavaExec, dependsOn: prepareClientIntegrationTest) { group = 'verification' - dependsOn { runClient.taskDependencies.getDependencies(runClient) } - main = runClient.main - args runClient.args workingDir clientIntegrationRunDirectory systemProperty 'clientprobe.enabled', 'true' - doFirst { classpath = runClient.classpath } + configureFromForgeRun(delegate, 'runClient') doLast { File marker = new File(clientIntegrationRunDirectory, 'client-smoke-pass.properties') if (!marker.isFile()) { @@ -407,6 +648,7 @@ task clientIntegrationProcess(type: JavaExec, dependsOn: prepareClientIntegratio Properties result = new Properties(); marker.withInputStream { result.load(it) } if (result.getProperty('reload_rendered') != 'true' || result.getProperty('world_settings_opened') != 'true' + || result.getProperty('long_editor_roundtrip') != 'true' || Integer.parseInt(result.getProperty('editor_routes', '0')) < 5) { throw new GradleException("Client integration result is incomplete: ${marker}") } @@ -415,6 +657,23 @@ task clientIntegrationProcess(type: JavaExec, dependsOn: prepareClientIntegratio || latest.text.contains('Reported exception thrown!'))) { throw new GradleException("Client integration logged a runtime failure: ${latest}") } + List allowedDevelopmentErrors = [ + 'The binary patch set is missing', + 'FML appears to be missing any signature data', + 'Unable to read a class file correctly', + 'problem reading the entry META-INF/versions/11/' + ] + List unaccountedErrors = latest.readLines('UTF-8').findAll { line -> + (line.contains('/ERROR]') || line.contains('/FATAL]')) + && !allowedDevelopmentErrors.any { line.contains(it) } + } + if (!unaccountedErrors.isEmpty()) { + throw new GradleException("Client integration logged unaccounted errors: ${unaccountedErrors}") + } + File crashDirectory = new File(clientIntegrationRunDirectory, 'crash-reports') + if (crashDirectory.isDirectory() && crashDirectory.listFiles().length > 0) { + throw new GradleException("Client integration produced crash reports: ${crashDirectory}") + } } } @@ -457,7 +716,7 @@ task syncForge12EclipseIntegrationLaunches { } } -tasks.matching { it.name == 'eclipse' }.all { +tasks.named('genEclipseRuns') { finalizedBy syncForge12EclipseIntegrationLaunches } @@ -475,11 +734,8 @@ if (project.hasProperty('benchmarkRunDir')) { 'max-tick-time=-1\n', 'UTF-8') } } - task benchmarkIntegrationProcess(type: JavaExec, dependsOn: prepareWorldgenBenchmark) { +task benchmarkIntegrationProcess(type: JavaExec, dependsOn: prepareWorldgenBenchmark) { group = 'verification' - dependsOn { runServer.taskDependencies.getDependencies(runServer) } - main = runServer.main - args runServer.args workingDir benchmarkRunDirectory systemProperty 'orespawn.worldgenBenchmarkMode', project.findProperty('benchmarkMode') ?: 'sky' systemProperty 'orespawn.worldgenBenchmarkRadius', project.findProperty('benchmarkRadius') ?: '4' @@ -495,7 +751,7 @@ if (project.hasProperty('benchmarkRunDir')) { if (project.hasProperty('benchmarkBlockAudit')) { systemProperty 'orespawn.worldgenBenchmarkBlockAudit', project.property('benchmarkBlockAudit') } - doFirst { classpath = runServer.classpath } + configureFromForgeRun(delegate, 'runServer') doLast { File latest = new File(benchmarkRunDirectory, 'logs/fml-server-latest.log') String mode = project.findProperty('benchmarkMode') ?: 'sky' @@ -510,90 +766,605 @@ if (project.hasProperty('benchmarkRunDir')) { } } -task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource +tasks.named('processResources', ProcessResources) { + filteringCharset = 'UTF-8' + inputs.property('version', project.version) + inputs.property('mcversion', project.minecraft_version) + + filesMatching('mcmod.info') { + expand version: project.version, mcversion: project.minecraft_version + } + from('docs/AGENTS.md') { + into '' + rename { 'AGENTS.md' } + } + from('docs') { + into 'META-INF/orespawn/docs' + } + filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) +} + +tasks.named('jar', Jar) { + archiveClassifier = 'deobf' + destinationDirectory = layout.buildDirectory.dir('libs-dev') + manifest { + attributes([ + 'Specification-Title' : 'OreSpawn', + 'Specification-Vendor' : 'SkyBlade1978', + 'Specification-Version' : '1', + 'Implementation-Title' : base.archivesName.get(), + 'Implementation-Version' : project.version, + 'Implementation-Vendor' : 'SkyBlade1978', + 'OreSpawn-API-Version' : '1', + 'FMLAT' : 'accesstransformer.cfg', + 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}", + 'Built-On-Java' : '8', + 'Built-On' : "${project.minecraft_version}-${project.forge_version}" + ]) + } +} + +def releaseJar = renamer.classes(tasks.named('jar', Jar)) { + map.from minecraft.dependency.toSrgFile + archiveClassifier = null + accessTransformers = true + output = layout.buildDirectory.file("libs/OreSpawn-${project.version}.jar") } -task apiJar(type: Jar, dependsOn: classes) { - classifier = 'api' +def apiJar = tasks.register('apiJar', Jar) { + dependsOn tasks.named('classes') + archiveClassifier = 'api' + destinationDirectory = layout.buildDirectory.dir('libs-dev') from sourceSets.main.output include 'zone/moddev/mc/orespawn/api/**' include 'com/mcmoddev/orespawn/api/**' include 'com/mojang/serialization/**' include 'cyano/orespawn/**' + manifest { + attributes([ + 'Implementation-Title' : 'OreSpawn-api', + 'Implementation-Version': project.version, + 'OreSpawn-API-Version' : '1' + ]) + } } -task deobfJar(type: Jar, dependsOn: classes) { - classifier = 'deobf' - from sourceSets.main.output -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir +tasks.register('deobfJar') { + group = 'build' + description = 'Builds the local deobfuscated development jar under build/libs-dev.' + dependsOn tasks.named('jar') } -artifacts { - archives apiJar - archives deobfJar - archives sourcesJar - archives javadocJar +tasks.named('sourcesJar', Jar) { + filteringCharset = 'UTF-8' + includeEmptyDirs = false + archiveClassifier = 'sources' + filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) + manifest { + attributes([ + 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}:sources", + 'Implementation-Title' : 'OreSpawn-sources', + 'Implementation-Version': project.version + ]) + } } -jar { +tasks.named('javadocJar', Jar) { + filteringCharset = 'UTF-8' + archiveClassifier = 'javadoc' + filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) manifest { attributes([ - 'Specification-Title' : 'OreSpawn', - 'Specification-Vendor' : 'SkyBlade1978', - 'Specification-Version' : '1', - 'Implementation-Title' : project.name, - 'Implementation-Version' : version, - 'Implementation-Vendor' : 'SkyBlade1978', - 'Implementation-Timestamp' : new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), - 'OreSpawn-API-Version' : '1', - 'FMLAT' : 'accesstransformer.cfg' + 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}:javadoc", + 'Implementation-Title' : 'OreSpawn-javadoc', + 'Implementation-Version': project.version ]) } - finalizedBy 'reobfJar' } -processResources { - inputs.property 'version', project.version - inputs.property 'mcversion', project.minecraft.version - - from(sourceSets.main.resources.srcDirs) { - include 'mcmod.info' - expand version: project.version, mcversion: project.minecraft.version +['apiElements', 'runtimeElements'].each { configurationName -> + configurations.named(configurationName) { + artifacts.clear() } - from(sourceSets.main.resources.srcDirs) { - exclude 'mcmod.info' + artifacts { + add(configurationName, releaseJar) } - from('docs/AGENTS.md') { - into '' - rename { 'AGENTS.md' } +} + +tasks.named('assemble') { + dependsOn releaseJar + dependsOn apiJar + dependsOn tasks.named('sourcesJar') + dependsOn tasks.named('javadocJar') +} + +def expectedReleaseFiles = providers.provider { + def prefix = "${base.archivesName.get()}-${project.version}" + [ + "${prefix}.jar", + "${prefix}-sources.jar", + "${prefix}-javadoc.jar" + ] +} +def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') + +tasks.register('verifyReleaseConfiguration') { + group = 'verification' + description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' + + doLast { + if (project.mod_version != '4.0.8.110021') { + throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") + } + if (project.minecraft_version != '1.10.2' + || project.forge_version != '12.18.3.2511' + || project.mapping_channel != 'stable' + || project.mapping_version != '29-1.10.2') { + throw new GradleException('Unexpected Minecraft, Forge, or mappings target') + } + if (project.loader_name != 'forge' || project.loader_code != '1' + || project.java_version != '8' || project.gradle_java_version != '17') { + throw new GradleException('Unexpected dispatcher target metadata') + } + if (project.group.toString() != 'zone.moddev.mc' + || base.archivesName.get() != 'OreSpawn' + || project.curseforge_project_id != '245586') { + throw new GradleException('Unexpected Maven or CurseForge publication identity') + } + + [ + 'src/main/java/zone/moddev/mc/orespawn/OreSpawn.java', + 'src/main/java/cyano/orespawn/OreSpawn.java', + 'src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java', + 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', + 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', + 'README.md', 'CHANGELOG.txt' + ].each { path -> + if (!file(path).getText('UTF-8').contains('4.0.8.110021')) { + throw new GradleException("Authoritative release location does not contain 4.0.8.110021: ${path}") + } + } + if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { + throw new GradleException('Consumer compatibility floor must remain [4.0.6,5.0.0)') + } + if (!file('src/main/java/zone/moddev/mc/orespawn/api/OreSpawnApi.java') + .getText('UTF-8').contains('API_VERSION = 1')) { + throw new GradleException('OreSpawn API major must remain 1') + } + if (!file('src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java') + .getText('UTF-8').contains('SCHEMA_VERSION = 6') + || !file('src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java') + .getText('UTF-8').contains('SCHEMA_VERSION = 5')) { + throw new GradleException('Global/world schema versions must remain 6/5') + } + def providerSchema = new JsonSlurper().parse(file('docs/schemas/orespawn-provider.schema.json')) + if (!(providerSchema.properties.schema_version.enum as List).contains(4)) { + throw new GradleException('Provider schema must continue to support schema version 4') + } } - from('docs') { - into 'META-INF/orespawn/docs' +} + +tasks.register('verifyReleaseArtifacts') { + group = 'verification' + description = 'Audits the exact three distributable jars and their release-critical contents.' + dependsOn tasks.named('verifyReleaseConfiguration') + dependsOn tasks.named('assemble') + + doLast { + File libs = layout.buildDirectory.dir('libs').get().asFile + List jars = (libs.listFiles() ?: [] as File[]) + .findAll { it.name.endsWith('.jar') } + .sort { it.name } + List actual = jars.collect { it.name } + List expected = expectedReleaseFiles.get().sort() + if (actual != expected) { + throw new GradleException("Expected exactly ${expected}, found ${actual}") + } + + jars.each { candidate -> + if (candidate.length() == 0L) { + throw new GradleException("Empty release artifact: ${candidate.name}") + } + ZipFile candidateZip = new ZipFile(candidate) + try { + candidateZip.entries().findAll { entry -> + !entry.isDirectory() + && (archiveTextSuffixes.any { entry.name.endsWith(it) } + || entry.name.endsWith('/element-list') + || entry.name.endsWith('/package-list')) + }.each { entry -> + boolean containsCarriageReturn = candidateZip.getInputStream(entry).withCloseable { input -> + input.bytes.any { value -> value == 13 } + } + if (containsCarriageReturn) { + throw new GradleException( + "${candidate.name}!/${entry.name} does not use canonical LF line endings") + } + } + ['src/test/', 'src/biomeIntegrationTest/', 'src/migrationIntegrationTest/', + 'src/clientIntegrationTest/', 'src/os1AbiFixture/', 'src/os3AbiFixture/', + 'agent-notes/', 'surfaceprobe', 'migrationprobe', 'clientprobe', + 'org/junit/', 'org/mockito/', 'net/bytebuddy/'].each { forbidden -> + if (candidateZip.entries().any { it.name.contains(forbidden) }) { + throw new GradleException( + "${candidate.name} contains forbidden entry matching ${forbidden}") + } + } + } finally { + candidateZip.close() + } + } + + File mainJar = new File(libs, expectedReleaseFiles.get()[0]) + ZipFile zip = new ZipFile(mainJar) + try { + List names = zip.entries().collect { it.name } + [ + 'mcmod.info', + 'META-INF/accesstransformer.cfg', + 'zone/moddev/mc/orespawn/api/OreSpawnApi.class', + 'META-INF/orespawn/docs/VERSIONS.md', + 'META-INF/orespawn/docs/schemas/orespawn-provider.schema.json', + 'META-INF/orespawn/docs/schemas/orespawn-global.schema.json', + 'META-INF/orespawn/docs/schemas/orespawn-world.schema.json', + 'AGENTS.md' + ].each { required -> + if (!names.contains(required)) { + throw new GradleException("Release jar is missing ${required}") + } + } + + String metadata = zip.getInputStream(zip.getEntry('mcmod.info')) + .getText(StandardCharsets.UTF_8.name()) + def parsed = new JsonSlurper().parseText(metadata) + def mod = parsed instanceof List ? parsed.first() : parsed + if (mod.modid != 'orespawn' || mod.version != project.mod_version + || mod.mcversion != project.minecraft_version) { + throw new GradleException('Packaged mcmod.info version or target is incorrect') + } + + String packagedAccessTransformer = zip.getInputStream( + zip.getEntry('META-INF/accesstransformer.cfg')) + .getText(StandardCharsets.UTF_8.name()) + List packagedAccessTransformerRules = packagedAccessTransformer.readLines() + .collect { it.replaceFirst(/\s*#.*/, '').trim() } + .findAll { !it.isEmpty() } + List expectedRuntimeAccessTransformerRules = [ + 'public-f net.minecraft.world.WorldProvider field_76578_c', + 'public-f net.minecraft.world.gen.ChunkProviderOverworld field_186001_t' + ] + if (packagedAccessTransformerRules != expectedRuntimeAccessTransformerRules) { + throw new GradleException('Packaged access transformer was not remapped to the runtime SRG rules') + } + + def manifestEntry = zip.getEntry('META-INF/MANIFEST.MF') + def manifest = manifestEntry == null ? null + : new Manifest(zip.getInputStream(manifestEntry)).mainAttributes + if (manifest == null + || manifest.getValue('Implementation-Version') != project.mod_version + || manifest.getValue('OreSpawn-API-Version') != '1' + || manifest.getValue('FMLAT') != 'accesstransformer.cfg' + || manifest.getValue('Implementation-Timestamp') != null + || manifest.getValue('Timestamp') != null) { + throw new GradleException('Release manifest identity/API/FMLAT is incorrect or volatile') + } + + zip.entries().findAll { it.name.endsWith('.class') }.each { entry -> + byte[] header = new byte[8] + zip.getInputStream(entry).withCloseable { input -> + if (input.read(header) != header.length) { + throw new GradleException("Cannot inspect bytecode header for ${entry.name}") + } + } + int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) + if (major != 52) { + throw new GradleException("${entry.name} uses Java class major ${major}, expected 52") + } + } + } finally { + zip.close() + } + + File sources = new File(libs, expectedReleaseFiles.get()[1]) + new ZipFile(sources).withCloseable { sourceZip -> + if (sourceZip.getEntry('zone/moddev/mc/orespawn/OreSpawn.java') == null) { + throw new GradleException('Sources jar is missing OreSpawn.java') + } + } + File javadocs = new File(libs, expectedReleaseFiles.get()[2]) + new ZipFile(javadocs).withCloseable { javadocZip -> + if (javadocZip.getEntry('index.html') == null) { + throw new GradleException('Javadoc jar is missing index.html') + } + } } } -javadoc { - options.encoding = 'UTF-8' - options.addStringOption('Xdoclint:none', '-quiet') +tasks.register('writeReleaseChecksums') { + group = 'verification' + description = 'Writes SHA-256 checksums for the audited release jars.' + dependsOn tasks.named('verifyReleaseArtifacts') + def outputFile = layout.buildDirectory.file('release/SHA256SUMS') + inputs.files(providers.provider { + expectedReleaseFiles.get().collect { name -> + layout.buildDirectory.file("libs/${name}").get().asFile + } + }) + outputs.file(outputFile) + + doLast { + File output = outputFile.get().asFile + output.parentFile.mkdirs() + File libs = layout.buildDirectory.dir('libs').get().asFile + String contents = expectedReleaseFiles.get().sort().collect { name -> + MessageDigest digest = MessageDigest.getInstance('SHA-256') + new File(libs, name).withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + "${digest.digest().encodeHex().toString().toUpperCase()} ${name}" + }.join('\n') + '\n' + output.setText(contents, 'UTF-8') + } +} + +tasks.register('verifyPreparedReleaseArtifacts') { + group = 'verification' + description = 'Audits a previously built immutable release bundle before remote Maven publication.' + + doLast { + if (!preparedReleaseDir.isPresent()) { + throw new GradleException('preparedReleaseDir is required for prepared artifact publication') + } + File prepared = file(preparedReleaseDir.get()) + if (!prepared.isDirectory()) { + throw new GradleException("Prepared release directory does not exist: ${prepared}") + } + List expected = expectedReleaseFiles.get().sort() + List jars = (prepared.listFiles() ?: [] as File[]) + .findAll { it.name.endsWith('.jar') } + .sort { it.name } + if (jars.collect { it.name } != expected || jars.any { it.length() == 0L }) { + throw new GradleException("Prepared release must contain exactly the non-empty jars ${expected}") + } + File checksums = new File(prepared, 'SHA256SUMS') + File changelog = new File(prepared, 'CHANGELOG.txt') + if (!checksums.isFile() || !changelog.isFile()) { + throw new GradleException('Prepared release is missing SHA256SUMS or CHANGELOG.txt') + } + List actualChecksums = jars.collect { candidate -> + MessageDigest digest = MessageDigest.getInstance('SHA-256') + candidate.withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + "${digest.digest().encodeHex().toString().toUpperCase()} ${candidate.name}" + }.sort() + List declaredChecksums = checksums.readLines('UTF-8') + .findAll { !it.trim().isEmpty() } + .sort() + if (actualChecksums != declaredChecksums) { + throw new GradleException('Prepared release checksums do not match the immutable jars') + } + } } +def mavenUploadUrl = providers.environmentVariable('MAVEN_UPLOAD_URL') + .orElse('https://invalid.invalid/missing-maven-upload-url') +def mavenUploadUsername = providers.environmentVariable('MAVEN_UPLOAD_USERNAME') +def mavenUploadPassword = providers.environmentVariable('MAVEN_UPLOAD_PASSWORD') + publishing { publications { mavenJava(MavenPublication) { - artifact jar - artifact apiJar - artifact deobfJar - artifact sourcesJar - artifact javadocJar + groupId = project.group.toString() + artifactId = base.archivesName.get() + version = project.version.toString() + if (preparedReleaseDir.isPresent()) { + File prepared = file(preparedReleaseDir.get()) + artifact(new File(prepared, expectedReleaseFiles.get()[0])) + artifact(new File(prepared, expectedReleaseFiles.get()[1])) { + classifier = 'sources' + } + artifact(new File(prepared, expectedReleaseFiles.get()[2])) { + classifier = 'javadoc' + } + } else { + artifact(releaseJar) + artifact(tasks.named('sourcesJar')) + artifact(tasks.named('javadocJar')) + } + pom { + name = 'MMD OreSpawn' + description = project.mod_description + url = 'https://github.com/MinecraftModDevelopmentMods/OreSpawn' + licenses { + license { + name = 'GNU Lesser General Public License, Version 2.1' + url = 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt' + } + } + scm { + connection = 'scm:git:https://github.com/MinecraftModDevelopmentMods/OreSpawn.git' + developerConnection = 'scm:git:ssh://git@github.com/MinecraftModDevelopmentMods/OreSpawn.git' + url = 'https://github.com/MinecraftModDevelopmentMods/OreSpawn' + } + } } } repositories { - maven { url "file:///${project.projectDir}/mcmodsrepo" } + maven { + name = 'release' + url = uri(mavenUploadUrl.get()) + credentials { + username = mavenUploadUsername.orNull ?: '' + password = mavenUploadPassword.orNull ?: '' + } + } + } +} + +tasks.register('validateMavenReleaseCredentials') { + group = 'publishing' + description = 'Prevents Maven publication from targeting a local or incomplete repository.' + doLast { + if (!providers.environmentVariable('MAVEN_UPLOAD_URL').isPresent() + || !mavenUploadUsername.isPresent() + || !mavenUploadPassword.isPresent()) { + throw new GradleException( + 'MAVEN_UPLOAD_URL, MAVEN_UPLOAD_USERNAME, and MAVEN_UPLOAD_PASSWORD are required') + } + String target = providers.environmentVariable('MAVEN_UPLOAD_URL').get() + if (target.startsWith('file:')) { + throw new GradleException('Maven release publication must use a remote repository') + } + } +} + +tasks.withType(PublishToMavenRepository).configureEach { + dependsOn tasks.named('validateMavenReleaseCredentials') + if (preparedReleaseDir.isPresent()) { + dependsOn tasks.named('verifyPreparedReleaseArtifacts') + } else { + dependsOn tasks.named('verifyReleaseArtifacts') + } +} + +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +// ForgeGradle 2 generated these launch names with the removed GradleStart +// bootstrap. They can survive an in-place Buildship migration and remain the +// last-run Eclipse configuration even after ForgeGradle 7 generates runClient +// and runServer. Remove only the exact obsolete launch types; leave any +// independently maintained launch with the same file name untouched. +def obsoleteForgeGradle2EclipseLaunches = [ + 'OreSpawn_Client.launch': 'GradleStart', + 'OreSpawn_Server.launch': 'GradleStartServer' +] + +eclipse { + classpath { + downloadSources = true + downloadJavadoc = true + } + synchronizationTasks 'isolateEclipseProductionRuns' +} + +tasks.register('configureEclipseBuildship') { + group = 'ide' + description = 'Creates the Buildship project preferences used by ForgeGradle 7 imports.' + doLast { + File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs') + Properties preferences = new Properties() + Map requiredPreferences = [ + 'eclipse.preferences.version' : '1', + 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', + 'connection.gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, + 'connection.project.dir' : '', + 'gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, + 'override.workspace.settings' : 'true' + ] + requiredPreferences.each { key, value -> preferences.setProperty(key, value) } + preferencesFile.parentFile.mkdirs() + preferencesFile.withOutputStream { + preferences.store(it, + 'Generated by configureEclipseBuildship; keep Eclipse and command-line caches aligned.') + } + } +} + +tasks.register('isolateEclipseProductionRuns') { + group = 'ide' + description = 'Marks ordinary ForgeGradle 7 Eclipse launches as production-only.' + dependsOn tasks.named('genEclipseRuns') + dependsOn tasks.named('configureEclipseBuildship') + + doLast { + obsoleteForgeGradle2EclipseLaunches.each { String name, String mainClass -> + File launch = file(name) + if (launch.isFile()) { + String contents = launch.getText('UTF-8') + String obsoleteMainType = + "org.eclipse.jdt.launching.MAIN_TYPE\" value=\"${mainClass}\"" + if (contents.contains(obsoleteMainType) && !launch.delete()) { + throw new GradleException("Could not remove obsolete ForgeGradle 2 launch ${name}") + } + } + } + + ['runClient.launch', 'runServer.launch'].each { name -> + File launch = file(name) + if (!launch.isFile()) { + throw new GradleException("ForgeGradle did not generate ${name}") + } + String contents = launch.getText('UTF-8') + if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) { + String marker = '' + String attribute = + ' ' + contents = contents.replace(marker, attribute + '\n' + marker) + } + launch.setText(contents.replace('\r\n', '\n'), 'UTF-8') + } + } +} + +tasks.register('verifyEclipseProductionClasspath') { + group = 'verification' + description = 'Verifies that ordinary generated Eclipse launches exclude tests, fixtures, and probe mods.' + dependsOn tasks.named('isolateEclipseProductionRuns') + dependsOn tasks.named('verifyLegacyOracleIsolation') + + doLast { + File buildshipPreferences = file('.settings/org.eclipse.buildship.core.prefs') + if (!buildshipPreferences.isFile()) { + throw new GradleException('Missing Eclipse Buildship project preferences') + } + List launchFiles = fileTree(project.projectDir) { + include 'runClient.launch' + include 'runServer.launch' + include '.eclipse/runClient.launch' + include '.eclipse/runServer.launch' + }.files as List + if (launchFiles.size() < 2) { + throw new GradleException('ForgeGradle did not generate ordinary client/server Eclipse launches') + } + obsoleteForgeGradle2EclipseLaunches.each { String name, String mainClass -> + File launch = file(name) + if (launch.isFile() && launch.getText('UTF-8').contains( + "org.eclipse.jdt.launching.MAIN_TYPE\" value=\"${mainClass}\"")) { + throw new GradleException( + "Obsolete ForgeGradle 2 ${mainClass} launch remains at ${name}") + } + } + List forbidden = [ + 'src/test', 'bin/test', 'build/classes/java/test', + 'biomeIntegrationTest', 'migrationIntegrationTest', 'clientIntegrationTest', + 'os1AbiFixture', 'os3AbiFixture', 'surfaceprobe', 'migrationprobe', 'clientprobe', + 'junit-', 'opentest4j-', 'junit-platform-', 'Mineralogy-1.10.2-3.3.8.26.jar' + ] + launchFiles.each { launch -> + String contents = launch.getText('UTF-8').replace('\\', '/') + List present = forbidden.findAll { contents.contains(it) } + if (!present.isEmpty()) { + throw new GradleException("${launch.name} exposes test code/dependencies: ${present}") + } + if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE') + || !contents.contains('value="true"')) { + throw new GradleException("${launch.name} does not exclude test code") + } + if (!contents.contains('PROJECT_ATTR" value="OreSpawn"')) { + throw new GradleException("${launch.name} targets the wrong Eclipse project") + } + } } } diff --git a/ci-fixtures/README.md b/ci-fixtures/README.md new file mode 100644 index 00000000..346f5e34 --- /dev/null +++ b/ci-fixtures/README.md @@ -0,0 +1,15 @@ +# Sealed CI fixtures + +These files are immutable OreSpawn compatibility-test inputs. They make the +OS1/OS3 ABI and Sylvester migration gates reproducible in a clean GitHub Actions +checkout without adding historical mods to the production, ordinary Gradle, or +Eclipse runtime classpaths. + +- `artifacts/OreSpawn_1.10.2-1.1.0.jar` is the published OS1 ABI oracle. +- `artifacts/OreSpawn-1.10.2-3.2.2.104.jar` is the published OS3 ABI oracle. +- The Base Metals and Mineralogy jars plus the sealed world archive form the + existing Sylvester upgrade corpus. + +`verifyLegacyFixtures` checks every SHA-256 before a fixture is compiled against +or copied into an isolated test run. Release-archive audits reject these files +and their probe classes from all public and development OreSpawn jars. diff --git a/ci-fixtures/SHA256SUMS b/ci-fixtures/SHA256SUMS new file mode 100644 index 00000000..e25b0379 --- /dev/null +++ b/ci-fixtures/SHA256SUMS @@ -0,0 +1,5 @@ +F919CEF794C73F02758BBD967346DBA0B53F6383153C7E29BD79F211CDA8C319 artifacts/BaseMetals_1.10.2-2.4.0.11.jar +88A6237C9A0E2C8891718B68C373E741C78B8494F5E68D8093CA9339F3BC4D87 artifacts/Mineralogy-1.10.2-3.3.8.26.jar +D964148C20175E16CCEFAB02AE43A181A50DC08A1D636A77E59CFE8C8EA8D41A artifacts/OreSpawn-1.10.2-3.2.2.104.jar +91345E4B825AA06E05F470066F467AABA0C7A2804A8DBCA4D3CD1905ED53607C artifacts/OreSpawn_1.10.2-1.1.0.jar +11FC2FA44C43520AA9B21C29B43203EBFE0A84773D2BDCBB4F3C48C490559B07 worlds/sylvester-era-trio-source-v2.zip diff --git a/ci-fixtures/artifacts/BaseMetals_1.10.2-2.4.0.11.jar b/ci-fixtures/artifacts/BaseMetals_1.10.2-2.4.0.11.jar new file mode 100644 index 00000000..12287973 Binary files /dev/null and b/ci-fixtures/artifacts/BaseMetals_1.10.2-2.4.0.11.jar differ diff --git a/ci-fixtures/artifacts/Mineralogy-1.10.2-3.3.8.26.jar b/ci-fixtures/artifacts/Mineralogy-1.10.2-3.3.8.26.jar new file mode 100644 index 00000000..13398b48 Binary files /dev/null and b/ci-fixtures/artifacts/Mineralogy-1.10.2-3.3.8.26.jar differ diff --git a/ci-fixtures/artifacts/OreSpawn-1.10.2-3.2.2.104.jar b/ci-fixtures/artifacts/OreSpawn-1.10.2-3.2.2.104.jar new file mode 100644 index 00000000..ed2f2423 Binary files /dev/null and b/ci-fixtures/artifacts/OreSpawn-1.10.2-3.2.2.104.jar differ diff --git a/ci-fixtures/artifacts/OreSpawn_1.10.2-1.1.0.jar b/ci-fixtures/artifacts/OreSpawn_1.10.2-1.1.0.jar new file mode 100644 index 00000000..d5fa3940 Binary files /dev/null and b/ci-fixtures/artifacts/OreSpawn_1.10.2-1.1.0.jar differ diff --git a/ci-fixtures/worlds/sylvester-era-trio-source-v2.zip b/ci-fixtures/worlds/sylvester-era-trio-source-v2.zip new file mode 100644 index 00000000..5535b780 Binary files /dev/null and b/ci-fixtures/worlds/sylvester-era-trio-source-v2.zip differ diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index ecee8a69..7010eebf 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -49,9 +49,9 @@ version. Examples: -| Minecraft | Loader | Target | Full OreSpawn 4.0.6 version | +| Minecraft | Loader | Target | Example full version | | --- | --- | ---: | --- | -| 1.10.2 | Forge | `110021` | `4.0.6.110021` | +| 1.10.2 | Forge | `110021` | `4.0.8.110021` | | 1.12.2 | Forge | `112021` | `4.0.6.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | diff --git a/gradle.properties b/gradle.properties index 10881f9a..42b4031c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,18 +2,31 @@ # This is required to provide enough memory for the Minecraft decompilation process. org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false +org.gradle.configuration-cache=false +org.gradle.caching=true +org.gradle.parallel=false +net.minecraftforge.gradle.merge-source-sets=false minecraft_version=1.10.2 minecraft_version_range=[1.10.2] forge_version=12.18.3.2511 forge_version_range=[12.18.3.2511,) loader_version_range=[12,) -mapping_version=stable_29 +mapping_channel=stable +mapping_version=29-1.10.2 + +# Release metadata consumed by the generic dispatcher. +loader_name=forge +loader_code=1 +java_version=8 +java_toolchain_version=8.0.502+7 +gradle_java_version=17 +curseforge_project_id=245586 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.6.110021 -mod_group_id=zone.moddev.mc.orespawn +mod_version=4.0.8.110021 +mod_group=zone.moddev.mc mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7a3265ee..0d4a9516 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 949819d2..2c68b418 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e7080f2e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +plugins { + id('org.gradle.toolchains.foojay-resolver-convention') version '1.0.0' +} + +rootProject.name = 'OreSpawn' diff --git a/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java b/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java index 10764b2e..1a9003a6 100644 --- a/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java +++ b/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java @@ -8,6 +8,10 @@ import java.util.Properties; import java.util.Set; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiCreateWorld; @@ -24,10 +28,11 @@ import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; /** Build-only client probe. It is compiled and packaged outside every release artifact. */ @Mod(modid = ClientProbeTestMod.MODID, name = "OreSpawn Client Probe", version = "1", - acceptedMinecraftVersions = "[1.10.2]") + acceptedMinecraftVersions = "[1.10.2]", dependencies = "required-after:orespawn") public final class ClientProbeTestMod { static final String MODID = "clientprobe"; private static final String WORLD_DIRECTORY = "client-smoke-world"; @@ -40,6 +45,7 @@ public final class ClientProbeTestMod { private int reloadWorldFrames; private int editorFrames; private boolean worldSettingsOpened; + private boolean longEditorRoundTrip; @Mod.EventHandler public void initialize(FMLInitializationEvent event) { @@ -94,6 +100,7 @@ public void onClientTick(TickEvent.ClientTickEvent event) { if (minecraft.currentScreen instanceof OreSpawnWorldSettingsScreen && editorFrames >= 2) { worldSettingsOpened = true; validateCaptions((OreSpawnWorldSettingsScreen) minecraft.currentScreen); + validateLongEditorRoundTrip(minecraft, minecraft.currentScreen); nextState(3); } break; @@ -192,6 +199,119 @@ private static void validateCaptions(OreSpawnScreen screen) { } } + private void validateLongEditorRoundTrip(Minecraft minecraft, GuiScreen parent) { + JsonObject root = WorldGeologyProfile.recommended(true).rootCopy(); + JsonObject ores = new JsonObject(); + JsonObject ore = new JsonObject(); + ore.addProperty("enabled", true); + ore.addProperty("block", "minecraft:diamond_ore"); + JsonObject oreDimensions = new JsonObject(); + JsonObject oreRule = new JsonObject(); + oreRule.addProperty("enabled", true); + oreRule.addProperty("min_y", 0); + oreRule.addProperty("max_y", 64); + oreRule.addProperty("frequency", 1.0D); + oreRule.addProperty("quantity", 8); + oreRule.addProperty("discard_chance_on_air_exposure", 0.0D); + oreRule.addProperty("pattern", "vein"); + oreRule.addProperty("height_distribution", "uniform"); + oreRule.addProperty("spread", 8); + oreRule.addProperty("vertical_spread", 4); + oreRule.addProperty("node_size", 4); + oreRule.add("host_families", new JsonArray()); + oreRule.add("host_blocks", values( + "example:ore_host_block_identifier_longer_than_thirty_two_characters")); + oreRule.add("host_tags", values( + "forge:ore_host_tag_identifier_longer_than_thirty_two_characters", + "forge:second_ore_host_tag_in_the_same_comma_separated_list")); + oreDimensions.add("minecraft:overworld", oreRule); + ore.add("dimensions", oreDimensions); + ores.add("example:long_editor_ore", ore); + root.add("ores", ores); + + JsonObject deposits = new JsonObject(); + JsonObject deposit = new JsonObject(); + deposit.addProperty("enabled", true); + deposit.addProperty("block", "minecraft:water"); + JsonObject fluidDimensions = new JsonObject(); + JsonObject fluidRule = new JsonObject(); + fluidRule.addProperty("enabled", true); + fluidRule.addProperty("min_y", 0); + fluidRule.addProperty("max_y", 48); + fluidRule.addProperty("frequency", 0.08D); + fluidRule.addProperty("min_radius", 5); + fluidRule.addProperty("max_radius", 12); + fluidRule.addProperty("min_vertical_radius", 2); + fluidRule.addProperty("max_vertical_radius", 5); + fluidRule.addProperty("max_lobes", 4); + fluidRule.addProperty("min_solid_cover", 2); + fluidRule.addProperty("min_solid_shell", 1); + fluidRule.add("host_families", new JsonArray()); + fluidRule.add("host_blocks", values( + "example:fluid_host_block_identifier_longer_than_thirty_two_characters")); + fluidRule.add("host_tags", values( + "forge:fluid_host_tag_identifier_longer_than_thirty_two_characters", + "forge:second_fluid_host_tag_in_the_same_comma_separated_list")); + fluidRule.add("biome_ids", values( + "example:included_biome_identifier_longer_than_thirty_two_characters")); + fluidRule.add("excluded_biome_ids", values( + "example:excluded_biome_identifier_longer_than_thirty_two_characters")); + fluidRule.add("biome_dictionary", values( + "INCLUDED_DICTIONARY_VALUE_LONGER_THAN_THIRTY_TWO_CHARACTERS", + "SECOND_INCLUDED_DICTIONARY_VALUE_IN_THE_COMMA_LIST")); + fluidRule.add("excluded_biome_dictionary", values( + "EXCLUDED_DICTIONARY_VALUE_LONGER_THAN_THIRTY_TWO_CHARACTERS")); + fluidRule.add("geomes", new JsonObject()); + fluidDimensions.add("minecraft:overworld", fluidRule); + deposit.add("dimensions", fluidDimensions); + deposits.add("example:long_editor_deposit", deposit); + root.add("fluid_deposits", deposits); + // Keep the synthetic profile in the editor's canonical shape so this + // assertion is about preservation of the eight long text fields rather + // than the session adding an unrelated optional empty section. + root.add("geomes", new JsonObject()); + + GeologyEditorSession session = new GeologyEditorSession( + WorldGeologyProfile.recommended(true).withRoot(root)); + String before = session.root().toString(); + + OreDimensionScreen oreScreen = new OreDimensionScreen(parent, session, + "example:long_editor_ore", "minecraft:overworld"); + ((GuiScreen) oreScreen).setWorldAndResolution(minecraft, 640, 480); + pressDone(oreScreen); + + FluidDepositDimensionScreen fluidScreen = new FluidDepositDimensionScreen(parent, session, + "example:long_editor_deposit", "minecraft:overworld"); + ((GuiScreen) fluidScreen).setWorldAndResolution(minecraft, 640, 480); + pressDone(fluidScreen); + + String after = session.root().toString(); + if (!before.equals(after)) { + throw new IllegalStateException("Opening and saving long editor values changed profile JSON\nBefore: " + + before + "\nAfter: " + after); + } + longEditorRoundTrip = true; + } + + private static JsonArray values(String... entries) { + JsonArray result = new JsonArray(); + for (String entry : entries) result.add(new JsonPrimitive(entry)); + return result; + } + + private static void pressDone(OreSpawnScreen screen) { + for (GuiButton widget : screen.buttons) { + if (!(widget instanceof Button)) continue; + String caption = TextFormatting.getTextWithoutFormattingCodes(((Button) widget).getMessage()); + if ("done".equalsIgnoreCase(caption)) { + ((Button) widget).press(); + return; + } + } + throw new IllegalStateException("Editor did not expose its Done action: " + + screen.getClass().getSimpleName()); + } + private static void stopIntegratedServer(Minecraft minecraft) { // Match GuiIngameMenu's target-native disconnect path. loadWorld(null) // coordinates the integrated-server save/stop; installing the replacement @@ -205,6 +325,7 @@ private static void stopIntegratedServer(Minecraft minecraft) { private void writeMarker() throws IOException { Properties values = new Properties(); values.setProperty("world_settings_opened", Boolean.toString(worldSettingsOpened)); + values.setProperty("long_editor_roundtrip", Boolean.toString(longEditorRoundTrip)); values.setProperty("editor_routes", Integer.toString(editorRoutes.size())); values.setProperty("editor_classes", editorRoutes.toString()); values.setProperty("first_world_rendered", Boolean.toString(firstWorldFrames >= 8)); diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index 5687f6f4..0828af5a 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -985,7 +985,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.6.110021 Upgrade Report"); + lines.add("OreSpawn 4.0.8.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/cyano/orespawn/OreSpawn.java b/src/main/java/cyano/orespawn/OreSpawn.java index a01e8b89..5a943010 100644 --- a/src/main/java/cyano/orespawn/OreSpawn.java +++ b/src/main/java/cyano/orespawn/OreSpawn.java @@ -19,7 +19,7 @@ public class OreSpawn { public static OreSpawn INSTANCE; public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.0.6.110021"; + public static final String VERSION = "4.0.8.110021"; public static final List oreSpawnConfigFiles = new ArrayList<>(); public static final List additionalStoneBlocks = new ArrayList<>(); public static boolean disableVanillaOreGen; diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index d6d86457..e56e3e1e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.0.6.110021"; + public static final String VERSION = "4.0.8.110021"; private static final Logger LOGGER = LogManager.getLogger(); diff --git a/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java index fcec4837..365b471f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java @@ -156,7 +156,7 @@ private TextFieldWidget placementField(int index, String key, String value) { int fieldWidth = Math.min(72, Math.max(58, columnWidth / 3)); TextFieldWidget box = new TextFieldWidget(font, groupX + columnWidth - fieldWidth, 90 + (row * 24), fieldWidth, 20, new TextComponentString(key)); - box.setValue(value); box.setMaxLength(32); + box.setMaxLength(32); box.setValue(value); placementWidgets.add(OreSpawnScreenLayout.explain(this, addButton(box), placementHelp(key))); return box; @@ -165,7 +165,7 @@ private TextFieldWidget placementField(int index, String key, String value) { private TextFieldWidget hostField(int index, String key, String value) { int x = index == 0 ? left : left + columnWidth + 5; TextFieldWidget box = new TextFieldWidget(font, x, 106, columnWidth, 20, new TextComponentString(key)); - box.setValue(value); box.setMaxLength(1024); + box.setMaxLength(1024); box.setValue(value); hostWidgets.add(OreSpawnScreenLayout.explain(this, addButton(box), "tooltip.orespawn." + key)); return box; @@ -175,7 +175,7 @@ private TextFieldWidget biomeField(int index, String key, String value) { int x = (index & 1) == 0 ? left : left + columnWidth + 5; int y = 106 + ((index / 2) * 44); TextFieldWidget box = new TextFieldWidget(font, x, y, columnWidth, 20, new TextComponentString(key)); - box.setValue(value); box.setMaxLength(1024); + box.setMaxLength(1024); box.setValue(value); biomeWidgets.add(OreSpawnScreenLayout.explain(this, addButton(box), "tooltip.orespawn.fluid." + key)); return box; diff --git a/src/main/java/zone/moddev/mc/orespawn/client/OreDimensionScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/OreDimensionScreen.java index 36032030..a5eb3ea5 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/OreDimensionScreen.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreDimensionScreen.java @@ -235,8 +235,8 @@ protected void init() { private TextFieldWidget addPlacementField(int x, int y, String key, String value) { TextFieldWidget box = new TextFieldWidget(font, x, y, columnWidth, 20, new TextComponentString(key)); - box.setValue(value); box.setMaxLength(32); + box.setValue(value); OreSpawnScreenLayout.explain(this, box, placementHelp(key)); placementWidgets.add(addButton(box)); return box; @@ -248,8 +248,8 @@ private int compactPlacementFieldY(int row) { private TextFieldWidget addHostField(int x, int y, String key, String value) { TextFieldWidget box = new TextFieldWidget(font, x, y, contentWidth, 20, new TextComponentString(key)); - box.setValue(value); box.setMaxLength(1024); + box.setValue(value); OreSpawnScreenLayout.explain(this, box, "tooltip.orespawn." + key); hostWidgets.add(addButton(box)); return box; @@ -257,8 +257,8 @@ private TextFieldWidget addHostField(int x, int y, String key, String value) { private TextFieldWidget addPatternField(int x, int y, String key, String value) { TextFieldWidget box = new TextFieldWidget(font, x, y, columnWidth, 20, new TextComponentString(key)); - box.setValue(value); box.setMaxLength(32); + box.setValue(value); OreSpawnScreenLayout.explain(this, box, "tooltip.orespawn.ore." + key); patternWidgets.add(addButton(box)); return box; diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index 4e7acc66..ff6cc696 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -125,7 +125,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.6.110021 Upgrade Report"); + lines.add("OreSpawn 4.0.8.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + mineralogyVersion diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index 45082f20..4a8add81 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.0.6.110021 Upgrade Report" + newline + String text = "OreSpawn 4.0.8.110021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 58440151..f6e960f5 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -1,2 +1,2 @@ -public-f net.minecraft.world.WorldProvider field_76578_c # biomeProvider -public-f net.minecraft.world.gen.ChunkProviderOverworld field_186001_t # oceanBlock +public-f net.minecraft.world.WorldProvider biomeProvider +public-f net.minecraft.world.gen.ChunkProviderOverworld oceanBlock diff --git a/src/test/java/zone/moddev/mc/orespawn/client/ClientButtonTextTest.java b/src/test/java/zone/moddev/mc/orespawn/client/ClientButtonTextTest.java index e9e67c3e..dcaad524 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/ClientButtonTextTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/ClientButtonTextTest.java @@ -29,6 +29,8 @@ class ClientButtonTextTest { "src", "main", "resources", "assets", "orespawn", "lang", "en_US.lang"); private static final Pattern LITERAL_TRANSLATION = Pattern.compile( "new\\s+TextComponentTranslation\\(\\s*\\\"([^\\\"]+)\\\"\\s*[,)]"); + private static final Pattern VALUE_BEFORE_MAXIMUM = Pattern.compile( + "\\.setValue\\([^;\\r\\n]*\\);\\s*\\w+\\.setMaxLength\\(\\d+\\)"); private static final Set MINECRAFT_1_14_KEYS = new HashSet<>(Arrays.asList( "gui.cancel", "gui.done", "options.off", "options.on")); @@ -64,4 +66,33 @@ void everyLiteralClientTranslationKeyExistsOnTheTarget() throws Exception { assertTrue(missing.isEmpty(), "Client labels must exist in OreSpawn or Minecraft 1.10: " + missing); } + + @Test + void textFieldsApplyTheirMaximumBeforeLoadingExistingValues() throws Exception { + List unsafeInitializers = new ArrayList<>(); + try (Stream files = Files.list(CLIENT_SOURCE)) { + for (Path source : (Iterable) files + .filter(path -> path.getFileName().toString().endsWith(".java"))::iterator) { + String text = new String(Files.readAllBytes(source), StandardCharsets.UTF_8); + if (VALUE_BEFORE_MAXIMUM.matcher(text).find()) { + unsafeInitializers.add(source.getFileName().toString()); + } + } + } + + assertTrue(unsafeInitializers.isEmpty(), + "GuiTextField truncates an existing value to its default 32 characters before " + + "a later maximum is applied: " + unsafeInitializers); + } + + @Test + void targetTextFieldRetainsLongExistingValuesWhenConfiguredFirst() { + String value = "minecraft:netherrack,minecraft:end_stone,minecraft:stone"; + TextFieldWidget field = new TextFieldWidget(null, 0, 0, 200, 20, "host_blocks"); + + field.setMaxLength(1024); + field.setValue(value); + + assertEquals(value, field.getValue()); + } }