diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ebd6108 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf +*.png binary +*.jar binary +*.zip binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..09ea0df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,252 @@ +name: Base Minerals 1.12 CI + +on: + push: + branches: + - master-1.12 + - 'feature/**' + pull_request: + branches: + - master-1.12 + +permissions: + contents: read + +concurrency: + group: baseminerals-1.12-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cold-forge-bootstrap: + name: Cold Forge bootstrap + runs-on: ubuntu-latest + timeout-minutes: 75 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Stage checksum-verified legacy dependencies + id: dependencies + shell: bash + run: | + set -euo pipefail + mirror="$RUNNER_TEMP/legacy-dependency-mirror" + bash gradle/stage-legacy-dependencies.sh "$mirror" + echo "repository=$mirror" >> "$GITHUB_OUTPUT" + + - name: Bootstrap Forge from an empty cache + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/baseminerals-cold-gradle + run: | + set -euo pipefail + test ! -e .gradle + test ! -e "$GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + chmod +x ./gradlew + gradle_args=( + classes verifyReleaseDependencies + -PlegacyDependencyVerificationRepository="${{ steps.dependencies.outputs.repository }}" + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Verify same-cache bootstrap offline + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/baseminerals-cold-gradle + run: | + set -euo pipefail + gradle_args=( + classes verifyReleaseDependencies + -PlegacyDependencyVerificationRepository="${{ steps.dependencies.outputs.repository }}" + --rerun-tasks --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + build: + name: Build, test, and audit + runs-on: ubuntu-latest + timeout-minutes: 90 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation and runtime toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - name: Stage checksum-verified dependencies + id: dependencies + shell: bash + run: | + set -euo pipefail + mirror="$RUNNER_TEMP/legacy-dependency-mirror" + bash gradle/stage-legacy-dependencies.sh "$mirror" + echo "repository=$mirror" >> "$GITHUB_OUTPUT" + + - name: Prepare clean packaged Forge runtime + id: forge + shell: bash + run: | + set -euo pipefail + runtime="$RUNNER_TEMP/forge-1.12.2-2588-runtime" + bash gradle/prepare-packaged-forge-runtime.sh "$runtime" "$JAVA_HOME_8_X64/bin/java" + echo "runtime=$runtime" >> "$GITHUB_OUTPUT" + + - name: Install world-audit tools + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.12' + + - name: Install pinned schema and workflow validators + run: python -m pip install -r tools/validation/requirements.txt + + - name: Build, test, audit, qualify runtimes, and generate Eclipse launches + shell: bash + run: | + set -euo pipefail + chmod +x ./gradlew + gradle_args=( + clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts + writeReleaseChecksums prepareEclipse verifyProjectResources packagedForgeRuntimeTest packagedSourcePolicyTest + -PpythonExecutable=python3 + -PpackagedForgeRuntimeOutput="$RUNNER_TEMP/baseminerals-packaged-checks" + -PpackagedSourcePolicyOutput="$RUNNER_TEMP/baseminerals-policy-checks" + -PreleaseBuild=true + -PlegacyDependencyVerificationRepository="${{ steps.dependencies.outputs.repository }}" + -PpackagedForgeRuntimeRoot="${{ steps.forge.outputs.runtime }}" + --no-daemon --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + cp build/release/SHA256SUMS "$RUNNER_TEMP/first-SHA256SUMS" + + - name: Verify the prepared release bundle + shell: bash + run: | + set -euo pipefail + bundle="$RUNNER_TEMP/release-input" + mkdir -p "$bundle" + cp build/libs/BaseMinerals-2.5.1.112021{,-sources,-javadoc}.jar "$bundle/" + cp build/release/SHA256SUMS CHANGELOG.txt "$bundle/" + ./gradlew verifyPreparedReleaseArtifacts verifyMavenCoordinates \ + -PpreparedReleaseDir="$bundle" \ + -PlegacyDependencyVerificationRepository="${{ steps.dependencies.outputs.repository }}" \ + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" \ + -Dorg.gradle.java.installations.auto-detect=false \ + -Dorg.gradle.java.installations.auto-download=false \ + --offline --no-daemon --stacktrace --max-workers=2 + + - name: Verify reproducible release archives + shell: bash + run: | + set -euo pipefail + gradle_args=( + clean build javadoc verifyReleaseArtifacts writeReleaseChecksums + -PreleaseBuild=true + -PlegacyDependencyVerificationRepository="${{ steps.dependencies.outputs.repository }}" + --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + cmp "$RUNNER_TEMP/first-SHA256SUMS" build/release/SHA256SUMS + + - name: Upload audited release candidate + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: BaseMinerals-1.12.2-${{ github.sha }} + if-no-files-found: error + retention-days: 30 + path: | + build/libs/BaseMinerals-2.5.1.112021.jar + build/libs/BaseMinerals-2.5.1.112021-sources.jar + build/libs/BaseMinerals-2.5.1.112021-javadoc.jar + build/release/SHA256SUMS + CHANGELOG.txt + + - name: Upload bounded diagnostics on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: BaseMinerals-1.12.2-diagnostics-${{ github.sha }} + if-no-files-found: ignore + retention-days: 14 + path: | + build/test-results/** + build/reports/** + ${{ runner.temp }}/baseminerals-packaged-checks/**/*-console.log + ${{ runner.temp }}/baseminerals-packaged-checks/**/logs/*.log + ${{ runner.temp }}/baseminerals-packaged-checks/**/crash-reports/*.txt + ${{ runner.temp }}/baseminerals-packaged-checks/**/*-minerals.json + ${{ runner.temp }}/baseminerals-packaged-checks/**/*-review.json + ${{ runner.temp }}/baseminerals-packaged-checks/**/*-launch.json + ${{ runner.temp }}/baseminerals-policy-checks/**/*-console.log + ${{ runner.temp }}/baseminerals-policy-checks/**/*-minerals.json + ${{ runner.temp }}/baseminerals-policy-checks/**/crash-reports/*.txt + build/problems/** + + - name: Upload generation qualification evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: BaseMinerals-1.12.2-worldgen-${{ github.sha }} + if-no-files-found: ignore + retention-days: 30 + path: | + ${{ runner.temp }}/baseminerals-packaged-checks/results.json + ${{ runner.temp }}/baseminerals-packaged-checks/timings.json + ${{ runner.temp }}/baseminerals-packaged-checks/source-fixtures.json + ${{ runner.temp }}/baseminerals-packaged-checks/**/input-jars.json + ${{ runner.temp }}/baseminerals-packaged-checks/**/*-minerals.json + ${{ runner.temp }}/baseminerals-packaged-checks/**/*-console.log + ${{ runner.temp }}/baseminerals-policy-checks/results.json + ${{ runner.temp }}/baseminerals-policy-checks/**/*-minerals.json + ${{ runner.temp }}/baseminerals-policy-checks/**/*-console.log diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..3541471 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,80 @@ +name: CodeQL + +on: + push: + branches: + - master-1.12 + - 'feature/**' + pull_request: + branches: + - master-1.12 + schedule: + - cron: '47 7 * * 4' + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze Java + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: 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: Stage checksum-verified dependencies + id: dependencies + shell: bash + run: | + set -euo pipefail + mirror="$RUNNER_TEMP/legacy-dependency-mirror" + bash gradle/stage-legacy-dependencies.sh "$mirror" + echo "repository=$mirror" >> "$GITHUB_OUTPUT" + + - name: Compile production code + shell: bash + run: | + set -euo pipefail + chmod +x ./gradlew + gradle_args=( + clean classes + -PlegacyDependencyVerificationRepository="${{ steps.dependencies.outputs.repository }}" + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Analyze + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 66922bd..d4d8215 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -6,11 +6,11 @@ on: workflow_dispatch: inputs: release_version: - description: Base Minerals version ending in .110021 or .111021 + description: Base Minerals version ending in .110021, .111021 or .112021 required: true type: string release_sha: - description: Full merged commit SHA on the matching 1.10.2 or 1.11.2 branch + description: Full merged commit SHA on the matching Minecraft branch required: true type: string ci_run_id: @@ -54,12 +54,12 @@ jobs: set -euo pipefail repo=MinecraftModDevelopmentMods/BaseMinerals [[ "$GITHUB_REPOSITORY" == "$repo" && "$GITHUB_REF" == refs/heads/master-1.12 ]] - [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.(110021|111021)$ ]] + [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.(110021|111021|112021)$ ]] case "${RELEASE_VERSION##*.}" in - 110021) minecraft_version=1.10.2 ;; - 111021) minecraft_version=1.11.2 ;; + 110021) minecraft_version=1.10.2; branch=master-1.10.2 ;; + 111021) minecraft_version=1.11.2; branch=master-1.11.2 ;; + 112021) minecraft_version=1.12.2; branch=master-1.12 ;; esac - branch="master-$minecraft_version" [[ "$RELEASE_SHA" =~ ^[a-f0-9]{40}$ && "$PLAYER_SHA256" =~ ^[a-fA-F0-9]{64}$ && "$CI_RUN_ID" =~ ^[0-9]+$ ]] branch_sha="$(gh api "repos/$repo/branches/$branch" --jq .commit.sha)" @@ -81,7 +81,18 @@ jobs: grep -Fx 'curseforge_project_id=270972' <<< "$properties" gh api "repos/$repo/contents/CHANGELOG.txt?ref=$RELEASE_SHA" --jq .content | base64 --decode > "$RUNNER_TEMP/merged-changelog.txt" - echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" + { + echo "minecraft_version=$minecraft_version" + echo 'curseforge_dependencies<> "$GITHUB_OUTPUT" - name: Download the audited CI bundle uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -131,9 +142,7 @@ jobs: with: curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} curseforge-id: 270972 - curseforge-dependencies: | - 240967(required) - 245586(required) + curseforge-dependencies: ${{ steps.target.outputs.curseforge_dependencies }} name: Base Minerals ${{ inputs.release_version }} for Minecraft ${{ steps.target.outputs.minecraft_version }} (Forge) version: ${{ inputs.release_version }} version-type: release diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml new file mode 100644 index 0000000..2208bab --- /dev/null +++ b/.github/workflows/release-on-tag.yml @@ -0,0 +1,81 @@ +name: Start Base Minerals release from tag + +on: + push: + tags: + - '*.*.*.*' + +permissions: + actions: read + contents: read + +concurrency: + group: baseminerals-release-starter-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + validate-release-tag: + name: Validate tag for manual release confirmation + if: github.repository == 'MinecraftModDevelopmentMods/BaseMinerals' + 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 + env: + GH_TOKEN: ${{ github.token }} + shell: bash + 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" + mc_patch="${mc_patch:-0}" + if [[ -n "${extra:-}" || ! "$mc_major" =~ ^[0-9]+$ || ! "$mc_minor" =~ ^[0-9]+$ \ + || ! "$mc_patch" =~ ^[0-9]+$ || "$loader_name:$loader_code" != 'forge:1' ]]; then + echo "Invalid target metadata $minecraft_version/$loader_name/$loader_code" >&2 + exit 1 + fi + 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 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 + + - name: Record the required manual publication step + shell: bash + run: | + { + echo "## Release candidate validated" + echo + echo "Tag \`$GITHUB_REF_NAME\` matches the selected target and has a successful Build, test, and audit check." + echo + echo "**Nothing has been published.**" + echo + echo "Follow docs/RELEASING.md to verify the release inputs before publication." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml new file mode 100644 index 0000000..cc6223d --- /dev/null +++ b/.github/workflows/validate-gradle-build.yml @@ -0,0 +1,23 @@ +name: Validate Gradle Wrapper + +on: + push: + branches: + - master-1.12 + - 'feature/**' + pull_request: + branches: + - master-1.12 + +permissions: + contents: read + +jobs: + validation: + name: Validation + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate wrapper integrity + uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 diff --git a/.gitignore b/.gitignore index 05a6827..3a14b22 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,19 @@ build # other eclipse run -classes /README.txt /forge-*-changelog.txt -secret.json \ No newline at end of file +secret.json +# Local context and generated verification +/AGENTS.md +/agent-notes/ +/.codex/ +/.verification*/ +/release-candidates/ +__pycache__/ +*.pyc +.DS_Store +Thumbs.db +desktop.ini diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 342b030..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -dist: trusty -language: java -sudo: false -cache: - directories: - - $HOME/.gradle -notifications: - email: false -jdk: - - oraclejdk8 -before_install: - - chmod a+x gradlew -install: ./gradlew setupCIWorkspace -S -script: ./gradlew clean build -S diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4c8c253..bf75a3f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ -Addendum: update to using the "MaterialConfigOptions" API +2.5.1.112021 for 1.12.2 -*Metals family wide bug-fixes and updates. No longer requires BaseMetals - only MMDLib and MMD OreSpawn. +This brings the 1.10 and 1.11 updates across to 1.12.2. Use Forge 14.23.5.2859 with MMDLib 1.0.0 RC3 and OreSpawn 4.1.6.112021. Base Metals is optional. I checked that its hammer works on the ores with Base Metals 2.6.2.112021 installed. + +The seven ores use the same depths and deposit shapes as the 1.10 and 1.11 updates, with KiriCattus's overlays on vanilla stone. Your saved ore settings and existing chunks carry forward. If you're moving an OreSpawn 1 world, import its settings on 1.10.2 or 1.11.2 first and bring the saved profile with you. + +You can change material and fluid settings from Forge Mods or OreSpawn Mods. Restart Minecraft to apply them. Disabled materials switch off their recipes, and lithium and silicon still work with Tinkers' Construct. diff --git a/README.md b/README.md index b308ae0..a4647e7 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,24 @@ -[![](https://img.shields.io/badge/Discord-MMD-green.svg?style=flat&logo=Discord)](https://discord.mcmoddev.com) -[![](http://cf.way2muchnoise.eu/full_base-minerals_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/base-minerals) -[![](http://cf.way2muchnoise.eu/versions/Minecraft_base-minerals_all.svg)](https://www.curseforge.com/minecraft/mc-mods/base-minerals) -[![Build Status](https://travis-ci.org/jriwanek/BaseMinerals.svg?branch=master)](https://travis-ci.org/jriwanek/BaseMinerals) - -### Base Minerals -Addon to Base Metals supporting Minerals - -Content -------- -Bars - -Doors - -Ores - -Plates - -Trapdoors - -Armor - -Tools: Crackhammers, Axes, Hoes, Pickaxes, Shovels, Fishing Rods - -Ores, Blocks, Ingots & Nuggets - -Powders & Blends (Ingot and Nugget size) - -Rods - -Gears - -Weapons: Swords, Bows, Arrows, Crossbows & Bolts - -Materials ---------- -[In Progress] +# Base Minerals + +Base Minerals adds lithium, niter, phosphorus, potash, salt, saltpeter, silicon +and sulfur. It provides powders, storage blocks and, for some materials, +ingots, blends and molten forms. Silicon is made from sand and coal or charcoal +dust. The seven ores use KiriCattus's overlays on Minecraft's stone texture. + +This branch targets Minecraft 1.12.2 and Forge 14.23.5.2859 or later. Install +MMDLib 1.0.0 RC3 and OreSpawn 4.1.6.112021 on the client and server. Base Metals +and Mineralogy are optional. Tinkers' Construct adds melting and casting for +lithium and silicon. + +The material and fluid settings in `config/BaseMinerals.cfg` require a full +restart. Open the same screen from Forge Mods or OreSpawn's Mods directory. +Configure ore placement and shared outputs in OreSpawn. Vanilla stone and its +variants are valid hosts. + +Registry names and the 1.12 config keys carry forward. OreSpawn preserves saved +profiles, imported rules and already generated chunks. If you are moving an +OreSpawn 1 setup from 1.10.2 or 1.11.2, import it there first and carry the +saved world profile forward. + +See [ore generation](docs/ORE_GENERATION.md), [building](docs/BUILDING.md) and +[verification](tools/validation/legacy112/README.md). diff --git a/build.gradle b/build.gradle index d5e93c4..c71ec6e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,480 +1,181 @@ -buildscript { - repositories { - jcenter() - maven { - name = 'forge' - url = 'http://files.minecraftforge.net/maven' - } - maven { - name = 'gradle' - url 'https://plugins.gradle.org/m2/' - } - maven { - name = 'sonatype' - url = 'https://oss.sonatype.org/content/groups/public' - } - } - dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:${project.fg_version}" - classpath 'org.junit.platform:junit-platform-gradle-plugin:1.2.+' -// classpath 'com.jaredsburrows:gradle-checker-framework-plugin:0.2.1' - } -} +import org.apache.tools.ant.filters.FixCrLfFilter plugins { -//id 'com.github.spotbugs' version '1.6.2' - id 'com.matthewprenger.cursegradle' version '1.4.0' - id 'org.sonarqube' version '3.0' -} - -apply plugin: 'net.minecraftforge.gradle.forge' -apply plugin: 'com.matthewprenger.cursegradle' -apply plugin: 'maven-publish' -apply plugin: 'org.sonarqube' -apply plugin: 'org.junit.platform.gradle.plugin' -apply plugin: 'jacoco' -//apply plugin: 'com.github.spotbugs' -//apply plugin: 'com.jaredsburrows.checkerframework' - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - testCompile('org.junit.jupiter:junit-jupiter-api:5.2.+') - testCompile('org.junit.platform:junit-platform-launcher:1.2.+') - testRuntime('org.junit.jupiter:junit-jupiter-engine:5.2.+') - testCompile('org.mockito:mockito-core:2.+') -} - -project.afterEvaluate { - def junitPlatformTestTask = project.tasks.getByName('junitPlatformTest') - - // configure jacoco to analyze the junitPlatformTest task - jacoco { - // this tool version is compatible with - toolVersion = '0.8.1' - applyTo junitPlatformTestTask - } - - // create junit platform jacoco task - project.task(type: JacocoReport, "junitPlatformJacocoReport", - { - sourceDirectories = files("./src/main") - classDirectories = files("$buildDir/classes/main") - executionData junitPlatformTestTask - }) -} - -junitPlatform { - // platformVersion '1.2.0' - // logManager 'org.apache.logging.log4j.jul.LogManager' - reportsDir file("${project.buildDir}/test-results/junit-platform") - // enableStandardTestTask true - // selectors (optional) - // filters (optional) -} - -/* -spotbugs { - toolVersion = '3.1.5' -} -*/ - -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - options.compilerArgs = [ - '-Xlint:deprecation', // deprecation, rawtypes, cast, unchecked, all -// '-Xdiags:verbose', -// '-Werror' - ] -} - -javadoc { - failOnError = false -} - -test { -// useJUnitPlatform() -/* - useJUnitPlatform { - includeTags 'fast', 'smoke & feature-a' - // excludeTags 'slow', 'ci' - includeEngines 'junit-jupiter' - // excludeEngines 'junit-vintage' - } -*/ -} - -def mod_file = getModFile() -def short_version = getVersion('VERSION', mod_file) -version = mc_version + '-' + short_version -def display_version = short_version - -if (System.getenv().BUILD_NUMBER) { - version += '.' + System.getenv().BUILD_NUMBER - display_version += '.' + System.getenv().BUILD_NUMBER -} - -sourceCompatibility = targetCompatibility = '1.8' - -eclipse.project { - buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' - natures 'org.eclipse.buildship.core.gradleprojectnature' -} - -class Secrets { - def data = null - - def getProperty(String key) { - return data ? data[key] : '' - } -} - -import groovy.json.JsonSlurper - -def secretFile -if (System.getenv().SECRET_FILE) { - secretFile = file System.getenv().SECRET_FILE -} else { - secretFile = file 'secret.json' -} - -project.ext.secret = new Secrets() -if (secretFile.exists()) { - secretFile.withReader { - project.ext.secret.data = new JsonSlurper().parse it - } + id 'java' + id 'eclipse' + id 'idea' + id 'maven-publish' + id 'net.minecraftforge.renamer' version '1.1.5' + id 'net.minecraftforge.gradle' version '7.0.34' +} + +group = project.mod_group +version = project.mod_version +base.archivesName = 'BaseMinerals' + +if (!(project.version ==~ /\d+\.\d+\.\d+\.112021/)) { + throw new GradleException('Use Major.Minor.Patch.112021 for Minecraft 1.12.2 on Forge.') +} + +// Gradle and Mavenizer use modern JDKs; the mod and game still use Java 8. +java { + toolchain.languageVersion = JavaLanguageVersion.of(8) + withSourcesJar() + withJavadocJar() +} +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + sourceCompatibility = targetCompatibility = '1.8' + options.compilerArgs.addAll(['-Xmaxerrs', '1000']) +} +tasks.named('compileTestJava', JavaCompile) { options.compilerArgs.add('-proc:none') } +tasks.withType(Test).configureEach { useJUnitPlatform() } +tasks.withType(Javadoc).configureEach { + failOnError = false + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') + options.addStringOption('docencoding', 'UTF-8') + options.addStringOption('charset', 'UTF-8') + options.addBooleanOption('notimestamp', true) +} +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false + reproducibleFileOrder = true + includeEmptyDirs = false + exclude '**/desktop.ini', '**/Thumbs.db', '**/.DS_Store' +} +// Mapped dependency bytes no longer match their original signatures. Keep the +// verified downloads intact and strip signatures only from renamed copies. +tasks.withType(net.minecraftforge.renamer.gradle.RenameJar).configureEach { + additionalArgs.addAll('--strip-sigs', 'ALL', '--disable-abstract-param') +} + +def textPatterns = ['cfg', 'css', 'html', 'info', 'java', 'js', 'json', 'lang', 'mcmeta', 'md', 'properties', 'txt', 'xml'] + .collect { "**/*.${it}".toString() } +def normalizeText = { details -> + details.filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('lf'), + eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) +} + +def developmentOutput = layout.buildDirectory.dir('forge12-development/main') +def prepareDevelopmentMod = tasks.register('prepareDevelopmentMod', Sync) { + group = 'ide' + description = 'Merges production classes and processed resources for Forge 1.12 launches.' + dependsOn tasks.named('classes') + from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory } + from sourceSets.main.output.resourcesDir + into developmentOutput } minecraft { - version = project.forge_mc_version + '-' + project.forge_version - runDir = 'run' - mappings = project.mcp_mappings - - if (project.hasProperty('signSHA1')) { - replace '@FINGERPRINT@', project.findProperty('signSHA1') - } else { - replace '@FINGERPRINT@', '' - } - - if (project.hasProperty('coreplugin')) { - clientJvmArgs += '-Dfml.coreMods.load=' + project.coreplugin - serverJvmArgs += '-Dfml.coreMods.load=' + project.coreplugin - } - - if (project.hasProperty('mc_username')) { - clientRunArgs += ['--username', project.mc_username] - if (project.hasProperty('mc_password')) { - clientRunArgs += ['--password', project.mc_password] + mappings channel: project.mappings_channel, version: project.mappings_version + if (gradle.startParameter.offline) mavenizerArguments.add('--offline') + runs { + configureEach { + workingDir.convention layout.projectDirectory.dir( + providers.gradleProperty('baseMineralsRunDirectory').getOrElse('run')) + environment 'MC_VERSION', project.minecraft_version + environment 'MOD_CLASSES', developmentOutput.get().asFile.absolutePath + systemProperty 'fml.coreMods.load', project.coreplugin + systemProperty 'log4j.skipJansi', 'true' + systemProperty 'forge.logging.console.level', 'info' + systemProperty 'log4j.configurationFile', file('gradle/ide/log4j2.xml').toURI().toString() + if (providers.gradleProperty('baseMineralsLaunchProbePhase').isPresent()) { + systemProperty 'launch.phase', providers.gradleProperty('baseMineralsLaunchProbePhase').get() + } + } + register('client') + register('server') { args 'nogui' } } - } - if (project.hasProperty('mc_uuid')) { - clientRunArgs += ['--uuid', project.mc_uuid] - } - - // disable server gui - serverRunArgs += 'nogui' - - // skip the screen to confirm that you want to load a world with missing registry entries - serverJvmArgs += '-Dfml.doNotBackup=true' - clientJvmArgs += '-Dfml.doNotBackup=true' - - // skip having to confirm on server - serverJvmArgs += '-Dfml.queryResult=confirm' - - //skip jansi warnings in the log - serverJvmArgs += '-Dlog4j.skipJansi=true' - clientJvmArgs += '-Dlog4j.skipJansi=true' - - if (project.hasProperty('client_args')) { - clientJvmArgs += project.client_args - } - if (project.hasProperty('server_args')) { - serverJvmArgs += project.server_args - } -} - -repositories { - maven { // Mantle, TCon, JEI - name 'DVS1 Maven FS' - url 'http://dvs1.progwml6.com/files/maven/' - } - maven { // CCL, CCC, NEI - name 'Chickenbones Repo' - url 'http://chickenbones.net/maven/' - } - maven { // The One Probe, EnderIO, EnderCore - name 'tterrag' - url 'http://maven.tterrag.com/' - } - maven { // MCMultipart - name 'amadornes' - url 'http://maven.amadornes.com/' - } - maven { // CraftTweaker, ZenScript - name 'blamejared' - url 'http://maven.blamejared.com/' - } - maven { // IC2 - name 'industrialcraft' - url 'http://maven.ic2.player.to/' - } - maven { // HWYLA - name 'tehnut' - url 'http://tehnut.info/maven/' - } - maven { // CoFH - name 'Covers Maven' - url 'http://maven.covers1624.net/' - } - maven { - name = 'CurseForge' - url = 'https://minecraft.curseforge.com/api/maven/' - } - maven { - name 'opencomputers' - url 'http://maven.cil.li/' - } - maven { // Mekanism, TAIGA - name 'jitpack' - url 'https://jitpack.io/' - } - maven { // Buildcraft - name 'buildcraft' - url 'https://mod-buildcraft.com/maven/' - } - maven { // Storage Drawers - name 'thiakil' - url 'http://maven.thiakil.com/' - } - maven { // Applied Energistics - name 'forge' - url 'https://files.minecraftforge.net/maven/' - } - - maven { - name 'MMD' - url 'https://maven.mcmoddev.com/' - } -} - -dependencies { - deobfProvided "com.mcmoddev:AdditionalLootTables:${mc_version}-${alt_version}" - runtime "com.mcmoddev:AdditionalLootTables:${mc_version}-${alt_version}" - deobfProvided "com.mcmoddev:OreSpawn:${mc_version}-${orespawn_version}" - runtime "com.mcmoddev:OreSpawn:${mc_version}-${orespawn_version}" - // deobfProvided "com.mcmoddev:BaseMetals:${mc_version}-${bme_version}" - // runtime "com.mcmoddev:BaseMetals:${mc_version}-${bme_version}" - deobfProvided "com.mcmoddev:MMDLib:${mc_version}-${mmdlib_version}" } -processResources { - inputs.property 'version', project.version - inputs.property 'mcversion', project.minecraft.version - - from (sourceSets.main.resources.srcDirs) { - include 'mcmod.info' - expand 'version': short_version, 'mcversion': project.minecraft.version - } - - from (sourceSets.main.resources.srcDirs) { - exclude 'mcmod.info' - } - - rename '(.+_at.cfg)', 'META-INF/$1' -} - -jar { - manifest { - if (project.hasProperty('coreplugin')) { - attributes 'FMLCorePluginContainsFMLMod': 'true' - attributes 'FMLCorePlugin': project.coreplugin +tasks.matching { it.name in ['runClient', 'runServer'] }.configureEach { + dependsOn prepareDevelopmentMod + dependsOn 'prepareJava8Launcher' + doFirst { + Set splitOutputs = sourceSets.main.output.files.collect { it.canonicalFile } as Set + classpath = files(developmentOutput, tasks.named('prepareJava8Launcher'), classpath.filter { + !splitOutputs.contains(it.canonicalFile) && it.name != 'slime-launcher-0.2.2.jar' + }) + environment 'MC_VERSION', project.minecraft_version + environment 'MOD_CLASSES', developmentOutput.get().asFile.absolutePath } -// attributes 'FMLAT' : '' - } } -task apiJar(type: Jar) { - classifier = 'apiClasses' - from apiClasses -} +def legacyMirror = file(providers.gradleProperty('legacyDependencyVerificationRepository') + .orElse(providers.gradleProperty('legacyDependencyRepository')) + .getOrElse('.gradle/legacy-dependencies')) +def legacyRepository = { String remote -> legacyMirror.isDirectory() ? legacyMirror.toURI() : uri(remote) } -task deobfJar(type: Jar) { - classifier = 'deobf' - from sourceSets.main.output -} - -task signJar(type: SignJar, dependsOn: reobfJar) { - // Skips if the keyStore property is missing. - onlyIf { - project.hasProperty('keyStore') - } - - // findProperty allows us to reference the property without it existing. - // Using project.propName would cause the script to fail validation if - // the property did not exist. - keyStore = project.findProperty('keyStore') - alias = project.findProperty('keyStoreAlias') - storePass = project.findProperty('keyStorePass') - keyPass = project.findProperty('keyStoreKeyPass') - inputFile = jar.archivePath - outputFile = jar.archivePath -} - -build.dependsOn signJar - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.getDestinationDir() -} - -artifacts { - if (project.hasProperty('doAPIJar')) { - archives apiJar - } - if (project.hasProperty('doDeobfJar')) { - archives deobfJar - } - if (project.hasProperty('doSourceJar')) { - archives sourceJar - } - if (project.hasProperty('doJavadocJar')) { - archives javadocJar - } -} - -publishing { - publications { - mavenJava(MavenPublication) { - groupId project.group - artifactId project.archivesBaseName - version project.version - from components.java - if (project.hasProperty('doSourceJar')) { - artifact sourceJar { - classifier 'sources' - } - } - if (project.hasProperty('doAPIJar')) { - artifact apiJar { - classifier 'api' - } - } - if (project.hasProperty('doDeobfJar')) { - artifact deobfJar { - classifier 'deobf' - } - } - if (project.hasProperty('doJavadocJar')) { - artifact javadocJar { - classifier 'javadoc' - } - } - } - } - repositories { +repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven maven { - credentials { - username secret.username - password secret.password - } - url secret.url + name = 'VerifiedLegacyMirror' + url = legacyMirror.toURI() + metadataSources { artifact() } } - } -} - -curseforge { - if (project.hasProperty('cf_project_id') && project.hasProperty('cf_release_type') && project.hasProperty('cf_changelog_filename') && - ((project.cf_release_type == 'alpha') || (project.cf_release_type == 'beta') || (project.cf_release_type == 'release'))) { - apiKey = secret.curseforgeAPIKey - project { - id = project.cf_project_id - changelog = file(project.cf_changelog_filename) - releaseType = project.cf_release_type - addGameVersion(project.mc_version) - mainArtifact(jar) { - displayName = "$project.description $display_version" - relations { - requiredLibrary 'mmd-orespawn' - requiredLibrary 'mmdlib' - optionalLibrary 'additional-loot-tables' - } - } - if (project.hasProperty('doAPIJar')) { - addArtifact(apiJar) { - displayName = "$project.description $display_version API" - } - } - if (project.hasProperty('doSourceJar')) { - addArtifact(sourceJar) { - displayName = "$project.description $display_version Sources" - } - } - if (project.hasProperty('doDeobfJar')) { - addArtifact(deobfJar) { - displayName = "$project.description $display_version Development" - } - } - if (project.hasProperty('doJavadocJar')) { - addArtifact(javadocJar) { - displayName = "$project.description $display_version Javadoc" - } - } + exclusiveContent { + forRepository { maven { url = legacyRepository('https://www.cursemaven.com'); metadataSources { artifact() } } } + filter { includeGroup 'curse.maven' } } - } -} - -sonarqube { - properties { - property 'sonar.host.url', secret.sonarHost - property 'sonar.organization', secret.sonarOrganization - property 'sonar.login', secret.sonarToken - // property 'sonar.projectName', project.archivesBaseName - // property 'sonar.projectKey', "$project.group:$project.archivesBaseName" - // property 'sonar.branch.name', 'master-1.12' - // property 'sonar.branch.target', '' - property 'sonar.junit.reportPaths', "${project.buildDir}/test-results/junit-platform/*.xml" - property 'sonar.jacoco.reportPaths', "${project.buildDir}/jacoco/junitPlatformTest.exec" - } -} - -String getModFile() { - String path = '' - FileTree tree = fileTree(dir: 'src/main/java') - tree.include '**/*.java' - tree.visit { element -> - if (element.file.isFile()) { - element.file.eachLine { String s -> - s = s.trim() - if (s.startsWith('@Mod(')) { - path = "src/main/java/$element.relativePath" - } - } + exclusiveContent { + forRepository { maven { url = legacyRepository('https://chickenbones.net/maven/'); metadataSources { artifact() } } } + filter { includeGroup 'codechicken' } } - } - return path -} - -String getVersion(String type, String mod_file) { - String major = '0' - String revision = '0' - String patch = '0' - File file = file(mod_file) - def prefix = ~/^(?:public|protected|private) static final String $type = ".*$/ - file.eachLine { String s -> - s = s.trim() - if (s ==~ prefix || prefix.matcher(s).matches()) { - String[] bits = s.split("=") - String interesting = bits[1].trim() - interesting = interesting.substring(1, interesting.length() - 2) - String[] pts = interesting.trim().split("\\.") - - major = pts[0] ?: '0' - revision = pts[1] ?: '0' - patch = pts[2] ?: '0' + exclusiveContent { + forRepository { maven { url = legacyRepository('https://dvs1.progwml6.com/files/maven/'); metadataSources { artifact() } } } + filter { includeGroup 'slimeknights'; includeGroup 'slimeknights.mantle'; includeGroup 'mezz.jei' } + } + exclusiveContent { + forRepository { maven { url = legacyRepository('https://maven.ic2.player.to/'); metadataSources { artifact() } } } + filter { includeGroup 'net.industrial-craft' } + } + exclusiveContent { + forRepository { maven { url = legacyRepository('https://maven.covers1624.net/'); metadataSources { artifact() } } } + filter { includeGroup 'cofh' } + } + exclusiveContent { + forRepository { maven { url = legacyRepository('https://maven.moddev.zone/releases'); metadataSources { artifact() } } } + filter { includeModule 'zone.moddev.mc.basemetals', 'BaseMetals'; includeModule 'zone.moddev.mc.orespawn', 'OreSpawn' } + } + mavenCentral() +} + +apply from: 'gradle/dependencies.gradle' +renamer.mappings.from minecraft.dependency.toSrgFile + +tasks.named('processResources', ProcessResources) { + filteringCharset = 'UTF-8' + exclude '**/desktop.ini', '**/Thumbs.db', '**/.DS_Store' + inputs.property 'version', project.version + inputs.property 'mcversion', project.minecraft_version + filesMatching('mcmod.info') { expand version: project.version, mcversion: project.minecraft_version } + filesMatching('assets/baseminerals/lang/*.lang') { details -> details.name = details.name.toLowerCase(Locale.ROOT) } + rename '(.+_at.cfg)', 'META-INF/$1' + filesMatching(textPatterns, normalizeText) +} + +tasks.named('jar', Jar) { + archiveClassifier = 'deobf' + destinationDirectory = layout.buildDirectory.dir('libs-dev') + manifest.attributes( + 'Implementation-Title': 'BaseMinerals', 'Implementation-Version': project.version, + 'Maven-Artifact': "${project.group}:BaseMinerals:${project.version}", + 'Built-On-Java': '8', 'Built-On': "${project.minecraft_version}-${project.forge_version}", + 'FMLCorePluginContainsFMLMod': 'true', 'FMLCorePlugin': project.coreplugin) +} +['sourcesJar', 'javadocJar'].each { name -> + tasks.named(name, Jar) { + filteringCharset = 'UTF-8' + filesMatching(textPatterns, normalizeText) } - } - return "$major.$revision.$patch" } + +ext.baseMineralsBuild = [developmentOutput: developmentOutput].asImmutable() +apply from: 'gradle/ore-models.gradle' +apply from: 'gradle/release/artifacts.gradle' +apply from: 'gradle/release/publishing.gradle' +apply from: 'gradle/verification/runtime.gradle' +apply from: 'gradle/ide/eclipse.gradle' diff --git a/docs/BUILDING.md b/docs/BUILDING.md new file mode 100644 index 0000000..79ca685 --- /dev/null +++ b/docs/BUILDING.md @@ -0,0 +1,83 @@ +# Building Base Minerals 1.12.2 + +Use Gradle 9.6.1, ForgeGradle 7.0.34, Renamer 1.1.5 and Foojay 1.0.0. Gradle runs +on Java 17 and Mavenizer uses Java 25. Compilation, bytecode and Minecraft use Java 8. +Forge is 14.23.5.2859 with stable-39 mappings. Release pins are Temurin 8u502-b07, +17.0.1+12 and 25.0.3+9-LTS. Install them or pass their homes with +`org.gradle.java.installations.paths`. Do not launch Gradle on Java 8. +On the command line use `-Dorg.gradle.java.installations.paths=` and +`-Dorg.gradle.java.installations.auto-detect=false` to keep offline runs on the +same physical JDK installations. Gradle 9 deprecates the older `-P` form. + +The staging script downloads and verifies MMDLib, OreSpawn and the optional +compatibility jars using `gradle/dependencies.json`. The exact MMDLib pin is the +published 1.0.0 RC3 jar. Its embedded Forge metadata still reports `1.0.0-rc2`, +which is why that version appears in the runtime dependency range. +The mirror also supplies the checksum-pinned LegacyDev 0.2.3.1 used to bootstrap +Forge 1.12.2. + +Select Java 17 with `JAVA_HOME` and `PATH`, then stage the dependencies and build. + +```powershell +$env:GRADLE_USER_HOME='' +$mirror = '' +python gradle/stage-legacy-dependencies.py $mirror +python -m pip install -r tools/validation/requirements.txt +.\gradlew.bat clean check build javadoc verifyReleaseArtifacts verifyProjectResources prepareEclipse "-PlegacyDependencyVerificationRepository=$mirror" +``` + +Use `-PpythonExecutable=` if Python is not named `python3`. +`gradle/dependencies.json` pins production downloads and SHA-256. Staging creates +a verified Maven mirror. `-PlegacyDependencyVerificationRepository=` selects +its location. The build remaps development copies and strips invalid signatures +from those copies. No floating mod versions, JCenter or +`mavenLocal()` are used. A normal build never publishes. + +`build/libs/BaseMinerals-2.5.1.112021.jar` is the installed jar. Sources/Javadocs +are separate. Test probes are isolated from all release archives. + +## Eclipse + +Run `prepareEclipse` and import the inner Git/Gradle project using Buildship. Use +Java 17 for Gradle, a Java 8 project JRE and one physical cache for Eclipse and the +terminal. The standard client and server profiles load Base Minerals, MMDLib and OreSpawn. +Base Metals and Tinkers are optional compatibility dependencies. Regenerate +profiles when dependency/toolchain locations change. FG7 uses `genEclipseRuns` and +`prepareEclipse`. + +Use `runClientWithBaseMetals` in Eclipse to test Base Metals tools against the +Base Minerals ores. It loads the pinned Base Metals jar and keeps its saves and +configuration under `run/with-base-metals`. + +Gradle launches merge production classes and processed resources into one mod +directory. Eclipse uses `bin/main`, with resources from `build/resources/main` so +metadata is not duplicated. `verifyEclipseProductionClasspath` checks isolation. +ForgeGradle's `source-sets-not-merged-for-eclipse` diagnostic refers to its own +automatic merge. This project merges production output explicitly and audits +the generated profiles, Buildship model and Java 8 Eclipse compiler. + +## Ore models + +`generateOreModels` writes the ore block models to `build/generated/ore-models`. +The normal build includes them in the game and sources jars. Blockstates and +inventory models use the existing model names. + +The seven ores use KiriCattus's transparent PNGs in +`src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/`. The build +layers each PNG over Minecraft stone for the block and inventory models. +An ore with only a full texture in `textures/blocks/` uses that texture on a +single cube. To switch it to an overlay, put a PNG with the same name in +`ore_overlays/` and rebuild. + +## Verification + +`test` checks native declarations, configuration latching and namespace compatibility. +`verifyProjectResources` validates resources and the pinned engine schema. Packaged server checks require a clean Forge template: + +```text +gradlew packagedForgeRuntimeTest -PpackagedForgeRuntimeRoot= -PpackagedForgeRuntimeOutput= +gradlew packagedSourcePolicyTest -PpackagedForgeRuntimeRoot= -PpackagedSourcePolicyOutput= +``` + +The output directory must not already exist. See the validation README for exact +client commands, migration scenarios and limitations. diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md new file mode 100644 index 0000000..c3ba4a8 --- /dev/null +++ b/docs/KNOWN_ISSUES.md @@ -0,0 +1,19 @@ +# Known issues for Minecraft 1.12.2 + +## Moving an OreSpawn 1 configuration + +The 1.12.2 importer reads OreSpawn 2 and 3 configuration files. For an OreSpawn +1 configuration, use OreSpawn 4 on 1.10.2 or 1.11.2 to import it before +moving the world. Carry the resulting `serverconfig/orespawn-worldgen.json` +with the save. + +[OreSpawn 4.1.6.112021](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9527784) +keeps the saved imported lithium rule. It backs up affected profiles before +removing an untouched duplicate. If the added rule or its source policy was +edited, OreSpawn preserves the edits and flags the rule for review. + +## MMDLib version in Forge + +The [published MMDLib 1.0.0 RC3 jar](https://www.curseforge.com/minecraft/mc-mods/mmdlib/files/9414723) +reports `1.0.0-rc2` in the Forge Mods list. Base Minerals builds and tests against +the exact RC3 download and accepts the version reported by its metadata. diff --git a/docs/ORE_GENERATION.md b/docs/ORE_GENERATION.md new file mode 100644 index 0000000..e72e8d1 --- /dev/null +++ b/docs/ORE_GENERATION.md @@ -0,0 +1,64 @@ +# Ore generation + +Base Minerals supplies one native schema-5 declaration through OreSpawn's public +API. OreSpawn owns baking and execution. This mod adds no fallback generator, +rocks, terrain, biomes, strata or fluid deposits, and needs no Mineralogy. + +## Fresh-world defaults + +Ranges are inclusive. Frequency is expected attempts per chunk. Quantity is a +requested block budget, so veins can yield less when hosts are unavailable. + +| Mineral | Y | Attempts/chunk | Quantity | Shape | +| --- | --- | ---: | --- | --- | +| Lithium | 8..47 | 4 | 12..18 | Vein | +| Niter | 16..63 | 5 | 8..16 | Clusters | +| Salt | 24..79 | 2.5 | 16..32 | Flat clusters | +| Sulfur | 4..31 | 3 | 12..18 | Clusters | +| Phosphorus | 16..63 | 3 | 12..18 | Clusters | +| Potash | 8..47 | 2.5 | 12..24 | Flat clusters | +| Saltpeter | 24..79 | 3.75 | 8..16 | Clusters | + +Lithium and phosphorus use a triangular height curve. Sulfur favors the bottom +of its range, and the others use uniform heights. Each mineral retains its previous +expected requested budget while changing depth and shape. These are gameplay +approximations rather than geological simulations. + +Lithium uses vein length 8 and node size 3. Cluster horizontal spread, vertical +spread and node size are respectively niter `(4,2,3)`, salt `(8,0,8)`, sulfur +`(4,2,4)`, phosphorus `(6,1,6)`, potash `(6,0,6)` and saltpeter `(4,1,3)`. +The balance draws on [lithium pegmatites](https://www.usgs.gov/data/lithium-deposits-united-states), +[evaporite potash](https://www.usgs.gov/publications/potash-a-global-overview-evaporate-related-potash-resources-including-spatial) +and [sedimentary phosphate](https://pubs.usgs.gov/bul/b1693/html/bull74kl.htm). + +Vanilla stone, including metadata variants, is eligible. Additional hosts are +intrusive igneous rocks for lithium, sedimentary rocks for nitrates, salt, potash +and phosphorus, and sedimentary or volcanic rocks for sulfur. There are no hard biome +restrictions. Rules cover ordinary dimensions except vanilla Nether/End. +Silicon and charcoal do not generate. + +## Saved settings and shared ores + +Rule IDs remain `baseminerals:legacy/_ore`. Canonical materials use +`orespawn:`, with the `orespawn:standard` placement channel. Niter and +saltpeter remain separate. Sulfur/Sulphur aliases refer to the same material. + +OreSpawn's reviewed source policies can share one placement budget across +equivalent outputs. Sulfur priority is Mineralogy, Base Minerals, then Base +Electric. Lithium priority is Base Minerals then Base Electric. Only actual +loaded/managed sources qualify for automatic consolidation. An external generator +does not become controllable merely through an Ore Dictionary alias. +An existing profile choice remains authoritative even after sources become +manageable. Review the resulting source groups in OreSpawn before changing them. + +Native global overrides use `config/baseminerals-orespawn.json`. Existing world +profiles and native overrides take precedence over legacy imports. Saved OS1 +rules carried forward from 1.10.2 or 1.11.2 and imported OS3 rules keep their +custom settings and disabled flags. The explicit Overworld rule overrides the ordinary-dimension fallback, +so each chunk receives a single placement budget. Existing +ore entries are kept as whole entries, including older selector-only rules. Missing entries follow OreSpawn's normal provider merge behavior. +Retrogen stays disabled. Keep legacy files until +the importer report and resulting settings have been reviewed. +OreSpawn 4.1.6 preserves the imported lithium rule when only the saved world +is moved. It backs up profiles before removing an untouched duplicate and +flags edited duplicates for review. See [known issues](KNOWN_ISSUES.md). diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..52dc9db --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,22 @@ +# Release preparation + +Normal builds never publish. Main, sources, Javadocs and SHA-256 form one audited +candidate. Gradle enforces Java 8 bytecode, deterministic archives, processed +metadata, dependency hashes and exclusion of tests, local context and shared +library copies. Prepared bundles contain exactly those three jars, `SHA256SUMS` +and `CHANGELOG.txt`. `verifyPreparedReleaseArtifacts` audits the prepared files. + +CI has a cold bootstrap/offline rebuild, build/tests, resource/schema checks, +packaged server qualification, Eclipse generation and clean reproducibility check. +CodeQL/wrapper checks are separate. Actions use full commit hashes. + +The shared dispatcher on MMD's default branch, `master-1.12`, picks the branch +and CurseForge dependencies from the version. For 1.12.2, supply the +`Major.Minor.Patch.112021` version, full merged `master-1.12` SHA, successful +upstream push CI run ID and SHA-256 of the tested player jar. Run verification +with publication confirmation off before uploading. + +CurseForge project **270972** should list MMDLib **261744** and MMD OreSpawn +**245586** as required, with Base Metals **240967** optional. The workflow needs +access to `CURSEFORGE_TOKEN`. Check the public file and its download checksum +after upload. If an upload times out, check its status before trying again. diff --git a/gradle.properties b/gradle.properties index b9a4533..1426979 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,56 +1,34 @@ -org.gradle.daemon = false +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 -group = com.mcmoddev -archivesBaseName = BaseMinerals -name = BaseMinerals -description = Base Minerals +minecraft_version=1.12.2 +forge_version=14.23.5.2859 +mappings_channel=stable +mappings_version=39-1.12 -mc_version = 1.12 +mod_id=baseminerals +mod_group=zone.moddev.mc.baseminerals +mod_version=2.5.1.112021 +mod_artifact=BaseMinerals +loader_name=forge +loader_code=1 +java_version=8 +java_toolchain_version=8.0.502+7 +java_setup_version=8.0.502+7 +gradle_java_version=17 +gradle_java_setup_version=17.0.1+12 +mavenizer_java_version=25.0.3+9 +curseforge_project_id=270972 +cf_requirements=mmd-orespawn,mmdlib +orespawn_curse_project_id=245586 +coreplugin=zone.moddev.mc.baseminerals.compat.BaseMineralsLoadingPlugin +orespawn_version=4.1.6.112021 +orespawn_version_range=[4.1.6.112021,5.0.0) -fg_version = 2.3-SNAPSHOT -forge_mc_version = 1.12.2 -forge_version = 14.23.4.2705 -mcp_mappings = stable_39 - -cf_project_id = 270972 -cf_release_type = release -cf_changelog_filename = CHANGELOG.txt - -// coreplugin = - -doSourceJar = true -// doAPIJar -doDeobfJar = true -doJavadocJar = true - -ccl_version=3.1.3.+ -ccc_version=2.4.1.+ -nei_version=2.4.0.+ -jei_version=4.7.11.+ -mantle_version=1.3.2.+ -tconstruct_version=2.8.1.+ -mcmp_version=2.3.3 -mcmp_experimental_version=2.0.1_31 -top_version=1.4.18-10 -alt_version=2.0.+ -orespawn_version=3.2.+ -tesla_version=1.0.63 -bge_version=2.5.0-rc+ -bmi_version=2.5.0-rc+ -bme_version=2.5.0-rc+ -fme_version=2.5.0-rc+ -mme_version=2.5.0-rc+ -cofhcore_version=4.3.6.+ -mekanism_version=9efd716ea0 -ic2_version=2.8.73-ex112 -minetweaker_version=3.+ -mtlib_version=3.+ -ct_version=4.1.9.+ -ct_api_version=4.1.9.+ -hwyla_version=1.8.23-B38_1.12 -veinminer_version=1.12-0.38.2.647+b31535a -endercore_version=+ -enderio_version=+ -thermal_foundation_version=+ -thermal_expansion_version=+ -mmdlib_version=1.0.0-rc+ \ No newline at end of file +mmdlib_version=1.0.0-rc2 +mmdlib_version_range=[1.0.0-rc2,2.0.0) +forge_installer_sha256=DBBA1CD1BF34A4B82148BAC50A6150F63FFCB2D5DC7D88A0508C27FDCFF15B76 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle new file mode 100644 index 0000000..1c79908 --- /dev/null +++ b/gradle/dependencies.gradle @@ -0,0 +1,67 @@ +import groovy.json.JsonSlurper +import java.security.MessageDigest + +def specs = new JsonSlurper().parse(file('gradle/dependencies.json')) +def mapped = specs.collectEntries { [(it.id): renamer.dependency(it.coordinate)] } + +configurations { + legacyModArtifacts { canBeConsumed = false; transitive = false } + java8LauncherArtifact { canBeConsumed = false; transitive = false } + eclipseBaseMetalsRuntime { canBeConsumed = false; transitive = false } +} +dependencies { + implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") + java8LauncherArtifact 'net.minecraftforge:slime-launcher:0.2.2' + eclipseBaseMetalsRuntime mapped['basemetals'] + compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209' + specs.each { spec -> + if (spec.runtime) implementation mapped[spec.id] + else compileOnly mapped[spec.id] + legacyModArtifacts spec.coordinate + } + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +// Forge 1.12's mod scanner cannot read Java 11 multi-release entries. They are +// unused on Java 8, so omit them from the development launcher copy. +tasks.register('prepareJava8Launcher', Jar) { + dependsOn configurations.java8LauncherArtifact + archiveFileName = 'slime-launcher-0.2.2-java8.jar' + destinationDirectory = layout.buildDirectory.dir('development-dependencies') + from { zipTree(configurations.java8LauncherArtifact.singleFile) } + exclude 'META-INF/versions/**', 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA' +} + +// Verify the original files, not renamed copies whose bytes depend on mappings. +tasks.register('verifyReleaseDependencies') { + group = 'verification' + description = 'Checks every pinned integration API against its SHA-256.' + inputs.file 'gradle/dependencies.json' + doLast { + def artifacts = configurations.legacyModArtifacts.resolvedConfiguration.resolvedArtifacts + specs.each { spec -> + def parts = spec.coordinate.split(':') + def artifact = artifacts.find { it.moduleVersion.id.group == parts[0] + && it.name == parts[1] && it.moduleVersion.id.version == parts[2] } + if (artifact == null) throw new GradleException("Missing ${spec.coordinate}") + String actual = MessageDigest.getInstance('SHA-256').digest(artifact.file.bytes).encodeHex().toString() + if (!actual.equalsIgnoreCase(spec.sha256)) { + throw new GradleException("Checksum mismatch for ${spec.coordinate}: ${actual}") + } + if (spec.id == 'orespawn') { + new java.util.jar.JarFile(artifact.file).withCloseable { jar -> + def metadata = new JsonSlurper().parse(jar.getInputStream(jar.getJarEntry('mcmod.info'))) + if (!metadata.any { it.modid == 'orespawn' && it.version == project.orespawn_version } + || jar.getJarEntry('zone/moddev/mc/orespawn/api/OreSpawnApi.class') == null + || jar.manifest.mainAttributes.getValue('OreSpawn-API-Version') != '1') { + throw new GradleException('Wrong OreSpawn identity or public API.') + } + } + } + } + } +} +tasks.named('compileJava') { dependsOn tasks.named('verifyReleaseDependencies') } +tasks.named('check') { dependsOn tasks.named('verifyReleaseDependencies') } diff --git a/gradle/dependencies.json b/gradle/dependencies.json new file mode 100644 index 0000000..2e8033b --- /dev/null +++ b/gradle/dependencies.json @@ -0,0 +1,44 @@ +[ + { + "id": "mmdlib", + "coordinate": "curse.maven:mmdlib-261744:9414723", + "sha256": "B35CD5A4454B03D43744EE263BD92A4B718660557C360F961B65A6303A80F88C", + "repository": "https://www.cursemaven.com", + "runtime": true + }, + { + "id": "orespawn", + "coordinate": "curse.maven:mmd-orespawn-245586:9527784", + "sha256": "DA6B243317E3365371C397CC9A895D108312F3437FB1797FBB5E8241525EA75F", + "repository": "https://www.cursemaven.com", + "runtime": true + }, + { + "id": "basemetals", + "coordinate": "curse.maven:base-metals-240967:9532211", + "sha256": "0CFD379FA059974E8DBA2AC743FF3E1E8E8183E48DA87BD63377E2EF0FB30312", + "repository": "https://www.cursemaven.com", + "runtime": false + }, + { + "id": "alt", + "coordinate": "curse.maven:additional-loot-tables-243157:2691613", + "repository": "https://www.cursemaven.com", + "sha256": "D5661DBF99859E8C9CF29704AB0FD4A132D062031A4023A0FF5E6F9D06ACE3E2", + "runtime": false + }, + { + "id": "mantle", + "coordinate": "curse.maven:tinkers-mantle-74924:2550550", + "repository": "https://www.cursemaven.com", + "sha256": "7758817445D55B65581A34A906EE01155C4598FDDF06E7D72944C840A1105184", + "runtime": false + }, + { + "id": "tconstruct", + "coordinate": "curse.maven:tinkers-construct-74072:2571785", + "repository": "https://www.cursemaven.com", + "sha256": "B1E21352C7BCEC95E33769308D9E70CC83E8F3A97C27FB94F4F0F55CE3CF96F2", + "runtime": false + } +] diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle new file mode 100644 index 0000000..5ddc1e7 --- /dev/null +++ b/gradle/ide/eclipse.gradle @@ -0,0 +1,224 @@ +def escapeXml = { String text -> + text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') +} +def archiveEntry = { File archive -> + String memento = '' + ' ' +} +def baseMetalsClientName = 'runClientWithBaseMetals.launch' +def baseMetalsRunDirectory = file('run/with-base-metals') +def baseMetalsArchive = { configurations.eclipseBaseMetalsRuntime.singleFile.canonicalFile } + +eclipse { + project { + buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' + natures 'org.eclipse.buildship.core.gradleprojectnature' + } + classpath { + downloadSources = true + downloadJavadoc = true + file.beforeMerged { classpath -> + Set managed = ['src/main/java', 'src/main/resources', 'src/test/java', + 'build/resources/main', 'tools/validation/legacy112', + 'tools/validation/legacy112/migration-probe', + 'tools/validation/legacy112/launch-probe', + 'tools/validation/legacy112/orespawn-probe'] as Set + classpath.entries.removeAll { it.kind == 'src' && managed.contains(it.path) } + } + file.whenMerged { classpath -> + // Eclipse must copy the expanded metadata, not the source placeholders. + classpath.entries.findAll { it.kind == 'src' && it.path == 'src/main/resources' }.each { + it.path = 'build/resources/main' + } + classpath.entries.findAll { it.kind == 'src' && !it.path.startsWith('src/main/') }.each { + if (it.path != 'build/resources/main') it.entryAttributes['test'] = 'true' + } + } + file.withXml { provider -> + Node root = provider.asNode() + root.classpathentry.findAll { it.@kind == 'lib' }.each { root.remove(it) } + if (!root.classpathentry.any { it.@path == 'org.eclipse.buildship.core.gradleclasspathcontainer' }) { + root.appendNode('classpathentry', [kind: 'con', path: 'org.eclipse.buildship.core.gradleclasspathcontainer']) + } + } + } + synchronizationTasks 'genEclipseRuns' +} +idea.module { downloadSources = true; downloadJavadoc = true } + +def configureBuildship = tasks.register('configureEclipseBuildship') { + group = 'ide' + doLast { + File target = file('.settings/org.eclipse.buildship.core.prefs') + Properties values = new Properties() + if (target.isFile()) target.withInputStream { values.load(it) } + [ + 'eclipse.preferences.version': '1', + 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', + 'connection.gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, + 'gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, + 'connection.java.home': System.getProperty('java.home'), + 'java.home': System.getProperty('java.home'), + 'connection.project.dir': '', 'override.workspace.settings': 'true' + ].each { key, value -> values.setProperty(key, value) } + target.parentFile.mkdirs() + target.withOutputStream { values.store(it, 'Generated by configureEclipseBuildship.') } + } +} + +tasks.register('isolateEclipseProductionRuns') { + group = 'ide' + description = 'Quotes launcher paths and keeps optional API jars and tests out of ordinary launches.' + dependsOn configureBuildship, tasks.named('processResources'), tasks.named('prepareJava8Launcher') + doLast { + // Copy, rather than Sync: Eclipse owns and compiles the classes in bin/main. + copy { from layout.buildDirectory.dir('resources/main'); into file('bin/main') } + file('run').mkdirs() + List runtime = configurations.runtimeClasspath.files.collect { it.canonicalFile } + ['runClient.launch', 'runServer.launch'].each { name -> + File launch = file(name) + if (!launch.isFile()) throw new GradleException("Missing generated launch: ${name}") + String text = launch.getText('UTF-8').replace('\r\n', '\n') + text = text.replace(escapeXml(configurations.java8LauncherArtifact.singleFile.canonicalPath), + escapeXml(tasks.named('prepareJava8Launcher', Jar).get().archiveFile.get().asFile.canonicalPath)) + def arguments = text =~ /()/ + if (arguments.count != 1) throw new GradleException("Malformed launcher arguments: ${name}") + String args = arguments[0][2] + ['cache', 'metadata', 'to-srg', 'to-obf'].each { flag -> + int start = args.indexOf("--${flag} ") + if (start < 0) throw new GradleException("Missing --${flag} in ${name}") + start += flag.length() + 3 + int end = args.indexOf(' --', start) + if (end < 0) throw new GradleException("Unterminated --${flag} in ${name}") + String value = args.substring(start, end) + if (!value.startsWith('"')) args = args.substring(0, start) + '"' + value + '"' + args.substring(end) + } + text = text.replace(arguments[0][0], arguments[0][1] + args + arguments[0][3]) + text = text.replaceAll(//, + '') + String modClasses = '' + if (text.contains('key="MOD_CLASSES"')) { + text = text.replaceAll(//, + java.util.regex.Matcher.quoteReplacement(modClasses)) + } else { + text = text.replace('', ' ' + modClasses + '\n ') + } + List lines = text.split('\n', -1) as List + def entries = (0..', + ' \n') + } + launch.setText(text, 'UTF-8') + if (name == 'runClient.launch') { + baseMetalsRunDirectory.mkdirs() + String classpathTag = '' + String companion = text.replace(classpathTag, classpathTag + '\n' + archiveEntry(baseMetalsArchive())) + String workingDirectory = '/, + java.util.regex.Matcher.quoteReplacement(workingDirectory + escapeXml(baseMetalsRunDirectory.absolutePath) + '"/>')) + file(baseMetalsClientName).setText(companion, 'UTF-8') + } + } + } +} +tasks.named('genEclipseRuns') { finalizedBy tasks.named('isolateEclipseProductionRuns') } +tasks.named('eclipse') { dependsOn tasks.named('genEclipseRuns') } + +def verifyJavaSourceLayout = tasks.register('verifyJavaSourceLayout') { + group = 'verification' + description = 'Checks that Java package names match their source folders, as Eclipse requires.' + inputs.files(provider { sourceSets.collect { it.java } }) + doLast { + List mismatches = [] + sourceSets.each { sourceSet -> + sourceSet.java.visit { entry -> + if (entry.directory || !entry.name.endsWith('.java')) return + + String path = entry.relativePath.pathString + int separator = path.lastIndexOf('/') + String expected = separator < 0 ? '' : path.substring(0, separator).replace('/', '.') + def declaration = entry.file.getText('UTF-8') =~ /(?m)^\s*package\s+([\w.]+)\s*;/ + String declared = declaration.find() ? declaration.group(1) : '' + + if (declared != expected) { + mismatches.add("${sourceSet.name}: ${path} declares '${declared}', expected '${expected}'") + } + } + } + if (!mismatches.isEmpty()) { + throw new GradleException('Java packages do not match their source folders:\n' + mismatches.join('\n')) + } + } +} + +tasks.named('check') { dependsOn verifyJavaSourceLayout } + +tasks.register('verifyEclipseProductionClasspath') { + group = 'verification' + dependsOn tasks.named('genEclipseRuns'), verifyJavaSourceLayout + mustRunAfter tasks.named('eclipse') + doLast { + List runtime = configurations.runtimeClasspath.files.collect { it.canonicalFile } + Set optional = configurations.compileClasspath.files.collect { it.canonicalFile } as Set + optional.removeAll(runtime) + ['runClient.launch': runtime, 'runServer.launch': runtime, + (baseMetalsClientName): runtime + baseMetalsArchive()].each { name, expected -> + String text = file(name).getText('UTF-8').replace('\\', '/') + if (!text.contains('MAIN_TYPE" value="net.minecraftforge.launcher.Main"') + || !text.contains('StandardVMType/JavaSE-8') + || !text.contains('MC_VERSION" value="1.12.2"') + || !text.contains('ATTR_EXCLUDE_TEST_CODE') + || text.contains('gradleclasspathcontainer') || text.contains('bin/test') + || text.contains('slime-launcher-0.2.2.jar') + || text.contains('junit-') || text.contains('legacy112-probe')) { + throw new GradleException("Unsafe production launch: ${name}") + } + if (expected.any { !text.contains(it.path.replace('\\', '/')) } + || (optional - expected).any { text.contains(it.path.replace('\\', '/')) }) { + throw new GradleException("Runtime/compile-only separation failed in ${name}") + } + if (name == baseMetalsClientName && !text.contains('WORKING_DIRECTORY" value="' + + escapeXml(baseMetalsRunDirectory.absolutePath).replace('\\', '/') + '"')) { + throw new GradleException("The Base Metals launch needs a separate game directory.") + } + ['cache', 'metadata', 'to-srg', 'to-obf'].each { flag -> + if (!text.contains("--${flag} "")) throw new GradleException("Unquoted --${flag} in ${name}") + } + } + String info = file('bin/main/mcmod.info').getText('UTF-8') + if (info.contains('${version}') || !info.contains(project.version.toString())) { + throw new GradleException('Eclipse is using unprocessed resources.') + } + def classpath = new groovy.xml.XmlSlurper().parse(file('.classpath')) + List sourcePaths = classpath.classpathentry.findAll { it.@kind == 'src' }.collect { it.@path.text() } + if (sourcePaths.size() != sourcePaths.unique(false).size() + || sourcePaths.contains('tools/validation/legacy112')) { + throw new GradleException('Eclipse retains stale or duplicate source-folder entries.') + } + classpath.classpathentry.findAll { it.@kind == 'src' && !it.@path.text().startsWith('src/main/') + && it.@path.text() != 'build/resources/main' }.each { + if (!it.attributes.attribute.any { it.@name == 'test' && it.@value == 'true' }) { + throw new GradleException("Non-production Eclipse source is not marked as test code: ${it.@path}") + } + } + } +} +tasks.register('prepareEclipse') { + group = 'ide' + description = 'Generates Eclipse metadata and verifies Java 8 production-only launches.' + dependsOn tasks.named('eclipse'), tasks.named('verifyEclipseProductionClasspath'), 'writeEclipseCompileClasspath' +} + +tasks.register('writeEclipseCompileClasspath') { + group = 'verification' + description = 'Writes the resolved editor classpath for headless Eclipse compiler checks.' + doLast { + File target = layout.buildDirectory.file('ide/compile-classpath.txt').get().asFile + target.parentFile.mkdirs() + target.setText(configurations.compileClasspath.asPath, 'UTF-8') + } +} diff --git a/gradle/ide/log4j2.xml b/gradle/ide/log4j2.xml new file mode 100644 index 0000000..a770e98 --- /dev/null +++ b/gradle/ide/log4j2.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/ore-models.gradle b/gradle/ore-models.gradle new file mode 100644 index 0000000..5f32362 --- /dev/null +++ b/gradle/ore-models.gradle @@ -0,0 +1,77 @@ +import groovy.json.JsonOutput + +// Forge 1.12 predates Minecraft's data generator. Generate models during the build. +def assets = layout.projectDirectory.dir('src/main/resources/assets/baseminerals') +def textures = assets.dir('textures/blocks') +def solidTextures = fileTree(textures) { include '*_ore.png' } +def overlayTextures = fileTree(textures.dir('ore_overlays')) { include '*_ore.png' } +def oreStates = fileTree(assets.dir('blockstates')) { + include '*_ore.json' + // Historical placeholders with no registered ore blocks. + exclude 'charcoal_ore.json', 'silicon_ore.json' +} +def generated = layout.buildDirectory.dir('generated/ore-models') + +def generateOreModels = tasks.register('generateOreModels') { + group = 'build' + description = 'Generates ore models from the available overlay or solid textures.' + inputs.files(oreStates, solidTextures, overlayTextures).withPathSensitivity(PathSensitivity.RELATIVE) + outputs.dir(generated) + + doLast { + def names = oreStates.files.collect { it.name - '.json' }.sort() + def overlays = overlayTextures.files.collect { it.name - '.png' }.toSet() + def solid = solidTextures.files.collect { it.name - '.png' }.toSet() + def extra = overlays - names + def missing = names.findAll { !overlays.contains(it) && !solid.contains(it) } + if (extra || missing) { + throw new GradleException("Invalid ore textures. Missing textures: ${missing}. Unmatched overlays: ${extra}.") + } + + def directory = generated.get().dir('assets/baseminerals/models/block').asFile + directory.mkdirs() + def writeModel = { String name, Map model -> + new File(directory, "${name}.json").setText(JsonOutput.prettyPrint(JsonOutput.toJson(model)) + '\n', 'UTF-8') + } + def cube = { Number from, Number to, String texture -> + def faces = ['down', 'up', 'north', 'south', 'west', 'east'].collectEntries { face -> + [(face): [texture: texture, cullface: face, uv: [0, 0, 16, 16]]] + } + [from: [from, from, from], to: [to, to, to], faces: faces] + } + writeModel('ore_overlay', [ + parent: 'minecraft:block/block', + elements: [cube(0, 16, '#base'), cube(-0.05, 16.05, '#overlay')] + ]) + names.each { name -> + if (overlays.contains(name)) { + writeModel(name, [ + parent: 'baseminerals:block/ore_overlay', + textures: [particle: 'minecraft:blocks/stone', base: 'minecraft:blocks/stone', + overlay: "baseminerals:blocks/ore_overlays/${name}"] + ]) + } else { + writeModel(name, [ + parent: 'block/cube_all', + textures: [all: "baseminerals:blocks/${name}"] + ]) + } + } + directory.listFiles().findAll { + it.isFile() && it.name.endsWith('_ore.json') && !names.contains(it.name - '.json') + }.each { + if (!it.delete()) throw new GradleException("Could not remove stale generated ore model ${it.name}") + } + } +} + +tasks.named('processResources', ProcessResources) { + dependsOn generateOreModels + from generated + duplicatesStrategy = DuplicatesStrategy.FAIL +} + +tasks.named('sourcesJar', Jar) { + dependsOn generateOreModels + from generated +} diff --git a/gradle/prepare-packaged-forge-runtime.sh b/gradle/prepare-packaged-forge-runtime.sh new file mode 100644 index 0000000..016bea1 --- /dev/null +++ b/gradle/prepare-packaged-forge-runtime.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +runtime="$1" +java8="$2" +version="1.12.2-14.23.5.2859" +installer="${runtime}/forge-${version}-installer.jar" +expected="$(sed -n 's/^forge_installer_sha256=//p' gradle.properties)" + +if [[ -e "$runtime" ]] && [[ -n "$(find "$runtime" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then + echo "Packaged Forge destination must be empty: $runtime" >&2 + exit 1 +fi +mkdir -p "$runtime" + +curl --fail --location --silent --show-error \ + "https://maven.minecraftforge.net/net/minecraftforge/forge/${version}/forge-${version}-installer.jar" \ + --output "$installer" +actual="$(sha256sum "$installer" | awk '{print toupper($1)}')" +if [[ "$actual" != "$expected" ]]; then + echo "Forge installer SHA-256 is $actual; expected $expected" >&2 + exit 1 +fi + +"$java8" -jar "$installer" --installServer "$runtime" +installed="$runtime/forge-${version}.jar" +universal="$runtime/forge-${version}-universal.jar" +test -f "$installed" +cp "$installed" "$universal" +test -f "$runtime/forge-${version}-universal.jar" +test -f "$runtime/minecraft_server.1.12.2.jar" +test -d "$runtime/libraries" diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle new file mode 100644 index 0000000..75a7cf9 --- /dev/null +++ b/gradle/release/artifacts.gradle @@ -0,0 +1,186 @@ +import groovy.json.JsonSlurper +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile + +def releaseJar = renamer.classes(tasks.named('jar', Jar)) { + map.from minecraft.dependency.toSrgFile + archiveClassifier = null + output = layout.buildDirectory.file("libs/BaseMinerals-${project.version}.jar") +} +['apiElements', 'runtimeElements'].each { name -> + configurations.named(name) { artifacts.clear() } + artifacts.add(name, releaseJar) +} +tasks.named('assemble') { dependsOn releaseJar, tasks.named('sourcesJar'), tasks.named('javadocJar') } + +tasks.register('verifyJava8Toolchain') { + group = 'verification' + description = 'Checks Java 8 compatibility; exact compiler versions are required only for release builds.' + doLast { + def compiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(8) }.get().metadata + if (compiler.languageVersion.asInt() != 8) throw new GradleException('Minecraft 1.12 requires Java 8.') + if (providers.gradleProperty('releaseBuild').getOrElse('false').toBoolean() + && (compiler.javaRuntimeVersion != '1.8.0_502-b07' + || System.getProperty('java.runtime.version') != '17.0.1+12')) { + throw new GradleException('Release builds require Temurin 8u502-b07 and Gradle on 17.0.1+12.') + } + if (providers.gradleProperty('releaseBuild').getOrElse('false').toBoolean()) { + def mavenizer = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }.get().metadata + if (mavenizer.javaRuntimeVersion != '25.0.3+9-LTS') { + throw new GradleException('Release setup requires Mavenizer on Temurin 25.0.3+9.') + } + } + } +} + +tasks.register('verifyReleaseConfiguration') { + group = 'verification' + dependsOn tasks.named('verifyJava8Toolchain') + doLast { + if (project.group != 'zone.moddev.mc.baseminerals' || project.version != '2.5.1.112021' + || project.minecraft_version != '1.12.2' || project.forge_version != '14.23.5.2859' + || project.mappings_channel != 'stable' || project.mappings_version != '39-1.12') { + throw new GradleException('Base Minerals target, mappings or Maven identity changed.') + } + String source = file('src/main/java/zone/moddev/mc/baseminerals/BaseMinerals.java').getText('UTF-8') + if (!source.contains('VERSION = "' + project.version + '"') || !source.contains('MODID = "baseminerals"')) { + throw new GradleException('The mod identity differs from the build metadata.') + } + if (project.orespawn_version != '4.1.6.112021' + || project.orespawn_version_range != '[4.1.6.112021,5.0.0)' + || !source.contains('required-after:orespawn@' + project.orespawn_version_range) + || !source.contains('required-after:forge@[14.23.5.2859,)')) { + throw new GradleException('The OreSpawn or Forge runtime requirement changed.') + } + if (project.mmdlib_version != '1.0.0-rc2' + || project.mmdlib_version_range != '[1.0.0-rc2,2.0.0)' + || !source.contains('required-after:mmdlib@' + project.mmdlib_version_range)) { + throw new GradleException('The published MMDLib runtime requirement changed.') + } + } +} + +def expectedFiles = ['', '-sources', '-javadoc'].collect { "BaseMinerals-${project.version}${it}.jar".toString() }.asImmutable() +def preparedDirectory = providers.gradleProperty('preparedReleaseDir') +def auditJars = { File directory -> + ['','-sources','-javadoc'].each { suffix -> + File jar = new File(directory, "BaseMinerals-${project.version}${suffix}.jar") + if (!jar.isFile() || jar.length() == 0) throw new GradleException("Missing ${jar.name}") + new ZipFile(jar).withCloseable { zip -> + zip.entries().each { entry -> + String path = entry.name.toLowerCase(Locale.ROOT) + if (path.startsWith('review/') || path.contains('probe') || path.contains('fixture') + || path.contains('agent-notes') || path.contains('agents.md') || path.contains('.codex') + || path.contains('.agents') || path.contains('claude') || path.contains('gemini') + || path.tokenize('/').any { it in ['desktop.ini', 'thumbs.db', '.ds_store'] } + || path.startsWith('tools/') || path.startsWith('reference/') + || path.startsWith('com/mcmoddev/lib/') || path.startsWith('zone/moddev/mc/mmdlib/') || path.startsWith('zone/moddev/mc/basemetals/') + || path.startsWith('com/mcmoddev/orespawn/') + || path == 'assets/baseminerals/orespawn/baseminerals.json' + || path.startsWith('org/junit/') || path.startsWith('net/minecraft/')) { + throw new GradleException("Non-production entry in ${jar.name}: ${entry.name}") + } + def date = entry.timeLocal + if (!entry.directory && !(date.toString() in ['1980-01-01T00:00', '1980-02-01T00:00'])) { + throw new GradleException("Volatile archive timestamp: ${entry.name}") + } + if (!entry.directory && path ==~ /.*\.(java|html|css|js|json|lang|mcmeta|md|txt|xml|cfg|info|properties)/) { + def decoder = java.nio.charset.StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(java.nio.charset.CodingErrorAction.REPORT) + .onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPORT) + String content = decoder.decode(java.nio.ByteBuffer.wrap(zip.getInputStream(entry).bytes)).toString() + if (content.contains('\r')) throw new GradleException("Non-LF text in ${entry.name}") + } + if (!entry.directory && path ==~ /.*\.(class|java|html|json|info|xml|properties|txt|md|lang|cfg)/) { + String content = new String(zip.getInputStream(entry).bytes, + java.nio.charset.StandardCharsets.ISO_8859_1) + if (content.contains(project.projectDir.absolutePath) + || content.contains('/home/runner/work/')) { + throw new GradleException("Local build path in ${entry.name}") + } + } + if (entry.name.endsWith('.class')) { + byte[] data = zip.getInputStream(entry).bytes + int major = ((data[6] & 255) << 8) | (data[7] & 255) + if (major != 52) throw new GradleException("Not Java 8 bytecode: ${entry.name}") + String constants = new String(data, java.nio.charset.StandardCharsets.ISO_8859_1) + if (constants.contains(project.projectDir.absolutePath)) { + throw new GradleException("Local path in ${entry.name}") + } + } + } + if (suffix == '') { + def manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))).mainAttributes + if (manifest.getValue('Maven-Artifact') != "${project.group}:BaseMinerals:${project.version}" + || manifest.getValue('FMLCorePlugin') != project.coreplugin + || manifest.keySet().any { it.toString().toLowerCase().contains('timestamp') }) { + throw new GradleException('Incorrect release manifest.') + } + def info = new JsonSlurper().parse(zip.getInputStream(zip.getEntry('mcmod.info')))[0] + if (info.modid != 'baseminerals' || info.version != project.version || info.mcversion != '1.12.2') { + throw new GradleException('Incorrect processed mcmod.info.') + } + if (zip.getEntry('zone/moddev/mc/baseminerals/BaseMinerals.class') == null + || zip.getEntry('META-INF/baseminerals-legacy-api.list') == null) { + throw new GradleException('Missing mod entry point or legacy API map.') + } + } + } + } +} + +tasks.register('verifyReleaseArtifacts') { + group = 'verification' + description = 'Audits the shipped jars for Java 8 bytecode, metadata and test isolation.' + dependsOn tasks.named('assemble'), tasks.named('verifyReleaseConfiguration'), tasks.named('verifyReleaseDependencies') + doLast { auditJars(layout.buildDirectory.dir('libs').get().asFile) } +} + +tasks.register('writeReleaseChecksums') { + group = 'verification' + dependsOn tasks.named('verifyReleaseArtifacts') + doLast { + File directory = layout.buildDirectory.dir('libs').get().asFile + List lines = ['','-sources','-javadoc'].collect { suffix -> + File jar = new File(directory, "BaseMinerals-${project.version}${suffix}.jar") + MessageDigest.getInstance('SHA-256').digest(jar.bytes).encodeHex().toString() + ' ' + jar.name + } + File sums = layout.buildDirectory.file('release/SHA256SUMS').get().asFile + sums.parentFile.mkdirs() + sums.setText(lines.join('\n') + '\n', 'UTF-8') + } +} +tasks.named('check') { dependsOn tasks.named('verifyReleaseConfiguration') } + +tasks.register('verifyPreparedReleaseArtifacts') { + group = 'verification' + description = 'Verifies a previously built bundle without rebuilding its jars.' + doLast { + if (!preparedDirectory.isPresent()) throw new GradleException('Set preparedReleaseDir to the immutable release bundle.') + File directory = file(preparedDirectory.get()) + Set expected = (expectedFiles + ['SHA256SUMS', 'CHANGELOG.txt']) as Set + if (!directory.isDirectory() || directory.listFiles().collect { it.name }.toSet() != expected) { + throw new GradleException('The prepared release must contain exactly three jars, SHA256SUMS and CHANGELOG.txt.') + } + List lines = new File(directory, 'SHA256SUMS').readLines('UTF-8') + if (lines.size() != 3) throw new GradleException('The prepared checksum list must contain exactly three files.') + Set checked = [] as Set + lines.each { line -> + def match = line =~ /^([a-fA-F0-9]{64}) (BaseMinerals-[^\/\\]+\.jar)$/ + if (!match.matches() || !expectedFiles.contains(match[0][2]) || !checked.add(match[0][2])) { + throw new GradleException('Invalid or duplicate prepared checksum entry.') + } + File jar = new File(directory, match[0][2]) + String actual = MessageDigest.getInstance('SHA-256').digest(jar.bytes).encodeHex().toString() + if (!actual.equalsIgnoreCase(match[0][1])) throw new GradleException("Prepared checksum mismatch: ${jar.name}") + } + if (!new File(directory, 'CHANGELOG.txt').getText('UTF-8').startsWith(project.version.toString() + ' for 1.12.2')) { + throw new GradleException('The prepared changelog belongs to a different release.') + } + auditJars(directory) + } +} + +ext.baseMineralsRelease = [jar: releaseJar, expectedFiles: expectedFiles, + preparedDirectory: preparedDirectory].asImmutable() diff --git a/gradle/release/publishing.gradle b/gradle/release/publishing.gradle new file mode 100644 index 0000000..1e8d2cb --- /dev/null +++ b/gradle/release/publishing.gradle @@ -0,0 +1,96 @@ +import groovy.xml.XmlSlurper + +def releaseJar = project.ext.baseMineralsRelease.jar +def expectedReleaseFiles = project.ext.baseMineralsRelease.expectedFiles +def preparedReleaseDir = project.ext.baseMineralsRelease.preparedDirectory + +// A normal build does not publish anything; publishing needs credentials. +def mavenUploadUrl = providers.environmentVariable('MAVEN_UPLOAD_URL') + .map { it.trim() }.filter { !it.isEmpty() } + .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) { + 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[0])) + artifact(new File(prepared, expectedReleaseFiles[1])) { classifier = 'sources' } + artifact(new File(prepared, expectedReleaseFiles[2])) { classifier = 'javadoc' } + } else { + artifact(releaseJar) + artifact(tasks.named('sourcesJar')) + artifact(tasks.named('javadocJar')) + } + pom { + name = 'Base Minerals' + description = 'Minerals, ores and materials for Base Metals on Minecraft 1.12.2.' + url = 'https://github.com/MinecraftModDevelopmentMods/BaseMinerals' + 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/BaseMinerals.git' + developerConnection = 'scm:git:ssh://git@github.com/MinecraftModDevelopmentMods/BaseMinerals.git' + url = 'https://github.com/MinecraftModDevelopmentMods/BaseMinerals' + } + } + } + } + repositories { + maven { + name = 'release' + url = uri(mavenUploadUrl.get()) + credentials { + username = mavenUploadUsername.orNull ?: '' + password = mavenUploadPassword.orNull ?: '' + } + } + } +} + +tasks.register('validateMavenReleaseCredentials') { + group = 'publishing' + doLast { + if (!providers.environmentVariable('MAVEN_UPLOAD_URL').getOrElse('').trim() + || !mavenUploadUsername.getOrElse('').trim() || !mavenUploadPassword.getOrElse('').trim()) { + throw new GradleException('MAVEN_UPLOAD_URL, MAVEN_UPLOAD_USERNAME, and MAVEN_UPLOAD_PASSWORD are required') + } + URI target = uri(providers.environmentVariable('MAVEN_UPLOAD_URL').get()) + if (target.scheme != 'https' || !target.host || target.host == 'invalid.invalid' + || !mavenUploadUsername.get().trim() || !mavenUploadPassword.get().trim()) { + throw new GradleException('Maven release publication requires an HTTPS repository and non-empty credentials') + } + } +} + +tasks.withType(PublishToMavenRepository).configureEach { + dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn 'verifyMavenCoordinates' + if (preparedReleaseDir.isPresent()) { + dependsOn tasks.named('verifyPreparedReleaseArtifacts') + } else { + dependsOn tasks.named('verifyReleaseArtifacts') + } +} + +tasks.register('verifyMavenCoordinates') { + group = 'verification' + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + doLast { + def pom = new XmlSlurper(false, false).parse(layout.buildDirectory.file('publications/mavenJava/pom-default.xml').get().asFile) + def actual = [pom.groupId.text(), pom.artifactId.text(), pom.version.text()] + def expected = ['zone.moddev.mc.baseminerals', 'BaseMinerals', project.version.toString()] + if (actual != expected) throw new GradleException("Wrong Maven coordinate: ${actual.join(':')}") + } +} + +tasks.named('check') { dependsOn tasks.named('verifyMavenCoordinates') } diff --git a/gradle/stage-legacy-dependencies.py b/gradle/stage-legacy-dependencies.py new file mode 100644 index 0000000..e7eb67d --- /dev/null +++ b/gradle/stage-legacy-dependencies.py @@ -0,0 +1,100 @@ +"""Build a checksum-verified Maven mirror from downloads or an existing Gradle cache.""" + +import argparse +import hashlib +import json +import time +import urllib.error +import urllib.request +from pathlib import Path + + +LEGACYDEV_VERSION = "0.2.3.1" +LEGACYDEV_SHA256 = "5A906209C2DC429DCCD9AA4B208100D4E2E07A6B9FA1D8A48D436236F4F02642" +FORGE_RELEASES = ( + "https://maven.minecraftforge.net/releases", + "https://maven.minecraftforge.net", + "https://maven.creeperhost.net", +) + + +def download(relative, repositories): + for repository in repositories: + url = repository.rstrip("/") + "/" + relative.as_posix() + request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + for attempt in range(3): + try: + with urllib.request.urlopen(request, timeout=30) as response: + return response.read() + except (OSError, urllib.error.URLError) as error: + if attempt == 2: + last_error = error + break + print(f"Retrying {url}", flush=True) + time.sleep(2) + raise last_error + + +def stage_legacydev(destination, cache): + module = Path("net/minecraftforge/legacydev") + relative = module / LEGACYDEV_VERSION / f"legacydev-{LEGACYDEV_VERSION}-fatjar.jar" + target = destination / relative + cached = cache / "caches/minecraftforge/forgegradle/mavenizer/caches/maven/forge" / relative if cache else None + if target.is_file(): + data = target.read_bytes() + elif cached and cached.is_file(): + data = cached.read_bytes() + else: + data = download(relative, FORGE_RELEASES) + if hashlib.sha256(data).hexdigest().upper() != LEGACYDEV_SHA256: + raise ValueError(f"Checksum mismatch for LegacyDev {LEGACYDEV_VERSION}") + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + target.with_suffix(".jar.sha1").write_text(hashlib.sha1(data).hexdigest(), encoding="ascii") + # Mavenizer follows the legacy userdev version range. Offer only the tested build. + (destination / module / "maven-metadata.xml").write_text(f""" + net.minecraftforge + legacydev + + {LEGACYDEV_VERSION} + {LEGACYDEV_VERSION} + {LEGACYDEV_VERSION} + + +""", encoding="utf-8") + print("legacydev", LEGACYDEV_SHA256, flush=True) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("destination", type=Path) + parser.add_argument("--cache", type=Path, help="Existing Gradle user home; matching files are verified before reuse") + args = parser.parse_args() + specs = json.loads(Path(__file__).with_name("dependencies.json").read_text(encoding="utf-8")) + destination = args.destination.resolve() + stage_legacydev(destination, args.cache) + + for spec in specs: + group, artifact, version, *classifier = spec["coordinate"].split(":") + filename = f"{artifact}-{version}" + ("-" + classifier[0] if classifier else "") + ".jar" + relative = Path(group.replace(".", "/")) / artifact / version / filename + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + + if not target.exists(): + cached = list((args.cache / "caches/modules-2/files-2.1" / group / artifact / version).glob("*/" + filename)) if args.cache else [] + if cached: + data = cached[0].read_bytes() + else: + data = download(relative, (spec["repository"],)) + if hashlib.sha256(data).hexdigest().upper() != spec["sha256"]: + raise ValueError(f"Checksum mismatch: {spec['coordinate']}") + target.write_bytes(data) + + if hashlib.sha256(target.read_bytes()).hexdigest().upper() != spec["sha256"]: + raise ValueError(f"Checksum mismatch in mirror: {target}") + print(spec["id"], spec["sha256"], flush=True) + + +if __name__ == "__main__": + main() diff --git a/gradle/stage-legacy-dependencies.sh b/gradle/stage-legacy-dependencies.sh new file mode 100644 index 0000000..530f893 --- /dev/null +++ b/gradle/stage-legacy-dependencies.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +"${PYTHON:-python3}" "$(dirname "$0")/stage-legacy-dependencies.py" "$1" diff --git a/gradle/verification/runtime.gradle b/gradle/verification/runtime.gradle new file mode 100644 index 0000000..cc55fad --- /dev/null +++ b/gradle/verification/runtime.gradle @@ -0,0 +1,88 @@ +// Isolated probes are never included in ordinary launches or release jars. +sourceSets { + policyFixture { + java.srcDirs = ['tools/validation/legacy112/policy-fixture'] + compileClasspath = sourceSets.main.compileClasspath + sourceSets.main.output + } + runtimeProbe { + java.srcDirs = ['tools/validation/legacy112/runtime-probe'] + compileClasspath = sourceSets.main.compileClasspath + sourceSets.main.output + } +} +def legacyConsumer = tasks.register('generateLegacyConsumer', JavaExec) { + dependsOn tasks.named('runtimeProbeClasses') + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) } + classpath = sourceSets.runtimeProbe.output + sourceSets.main.compileClasspath + mainClass = 'review.minerals.LegacyFixtureGenerator' + args layout.buildDirectory.dir('test-fixtures/legacy-consumer').get().asFile.absolutePath + outputs.dir layout.buildDirectory.dir('test-fixtures/legacy-consumer') +} +def probe = tasks.register('runtimeProbeJar', Jar) { + dependsOn legacyConsumer + group = 'verification' + archiveFileName = 'baseminerals-probe.jar' + destinationDirectory = layout.buildDirectory.dir('test-mods') + from sourceSets.runtimeProbe.output + from layout.buildDirectory.dir('test-fixtures/legacy-consumer') +} +def packagedProbe = renamer.classes(probe) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('test-mods/baseminerals-probe-packaged.jar') +} +tasks.register('packagedRuntimeProbeJar') { dependsOn packagedProbe } + +def policyFixtureJar = tasks.register('policyFixtureJar', Jar) { + archiveFileName = 'synthetic-source-policy-fixture.jar' + destinationDirectory = layout.buildDirectory.dir('test-mods') + from sourceSets.policyFixture.output +} +def packagedPolicyFixture = renamer.classes(policyFixtureJar) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('test-mods/synthetic-source-policy-fixture-packaged.jar') +} +tasks.register('packagedPolicyFixtureJar') { dependsOn packagedPolicyFixture } + +// The schema audit also needs this deterministic JUnit export after a cache hit. +tasks.named('test', Test) { + outputs.file(layout.buildDirectory.file('reports/native-provider.json')) +} + +tasks.register('verifyProjectResources', Exec) { + group = 'verification' + dependsOn tasks.named('test'), tasks.named('processResources') + doFirst { + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python3'), + 'tools/validation/legacy112/audit_resources.py', '--mirror', + file(providers.gradleProperty('legacyDependencyVerificationRepository').getOrElse('.gradle/legacy-dependencies')).absolutePath + } +} +tasks.register('packagedForgeRuntimeTest', Exec) { + group = 'verification' + dependsOn 'verifyReleaseArtifacts', 'packagedRuntimeProbeJar', 'test' + doFirst { + if (!providers.gradleProperty('packagedForgeRuntimeRoot').isPresent()) + throw new GradleException('Set packagedForgeRuntimeRoot to a disposable Forge server template.') + final def java8 = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) }.get() + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python3'), + 'tools/validation/legacy112/run_packaged_suite.py', '--java', java8.executablePath.asFile.absolutePath, + '--template', file(providers.gradleProperty('packagedForgeRuntimeRoot').get()).absolutePath, + '--output', file(providers.gradleProperty('packagedForgeRuntimeOutput').getOrElse('build/packaged-forge-checks')).absolutePath, + '--mirror', file(providers.gradleProperty('legacyDependencyVerificationRepository').getOrElse('.gradle/legacy-dependencies')).absolutePath + } +} + +tasks.register('packagedSourcePolicyTest', Exec) { + group = 'verification' + description = 'Tests managed-source policies with an isolated synthetic sibling, not a public integration claim.' + dependsOn 'verifyReleaseArtifacts', 'packagedRuntimeProbeJar', 'packagedPolicyFixtureJar' + doFirst { + if (!providers.gradleProperty('packagedForgeRuntimeRoot').isPresent()) + throw new GradleException('Set packagedForgeRuntimeRoot to a disposable Forge server template.') + final def java8 = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) }.get() + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python3'), + 'tools/validation/legacy112/run_policy_fixture.py', '--java', java8.executablePath.asFile.absolutePath, + '--template', file(providers.gradleProperty('packagedForgeRuntimeRoot').get()).absolutePath, + '--output', file(providers.gradleProperty('packagedSourcePolicyOutput').getOrElse('build/packaged-source-policies')).absolutePath, + '--mirror', file(providers.gradleProperty('legacyDependencyVerificationRepository').getOrElse('.gradle/legacy-dependencies')).absolutePath + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0d4a951..b1b8ef5 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 a95009c..dbe66e1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip +distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..c0dee63 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,128 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..a51ec4f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,82 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 -:mainEnd -if "%OS%"=="Windows_NT" endlocal +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle b/settings.gradle index 0e64f4a..bd5a40c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ -rootProject.name = archivesBaseName -enableFeaturePreview('STABLE_PUBLISHING') +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'BaseMinerals' diff --git a/src/main/java/com/mcmoddev/baseminerals/proxy/ClientProxy.java b/src/main/java/com/mcmoddev/baseminerals/proxy/ClientProxy.java deleted file mode 100644 index eb8d07a..0000000 --- a/src/main/java/com/mcmoddev/baseminerals/proxy/ClientProxy.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.mcmoddev.baseminerals.proxy; - -import com.mcmoddev.baseminerals.init.Blocks; -import com.mcmoddev.baseminerals.init.Fluids; -import com.mcmoddev.baseminerals.init.Items; -import com.mcmoddev.lib.client.registrations.RegistrationHelper; -import com.mcmoddev.lib.material.MMDMaterial; - -import net.minecraftforge.client.event.ModelRegistryEvent; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.RegistryEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; - -/** - * Base Minerals Client Proxy - * - * @author Jasmine Iwanek - * - */ -public class ClientProxy extends CommonProxy { - - @Override - public void preInit(FMLPreInitializationEvent event) { - super.preInit(event); - - MinecraftForge.EVENT_BUS.register(this); - } - - @SubscribeEvent - public void fluidRendering(RegistryEvent.Register event) { - for (final String name : Fluids.getFluidBlockRegistry().keySet()) { - RegistrationHelper.registerFluidRender(name); - } - } - - @SubscribeEvent - public void modelRegistry(ModelRegistryEvent event) { - for (final String name : Items.getItemRegistry().keySet()) { - RegistrationHelper.registerItemRender(name); - } - - for (final String name : Blocks.getBlockRegistry().keySet()) { - RegistrationHelper.registerBlockRender(name); - } - } -} diff --git a/src/main/java/com/mcmoddev/baseminerals/proxy/CommonProxy.java b/src/main/java/com/mcmoddev/baseminerals/proxy/CommonProxy.java deleted file mode 100644 index 4323191..0000000 --- a/src/main/java/com/mcmoddev/baseminerals/proxy/CommonProxy.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.mcmoddev.baseminerals.proxy; - -import java.util.HashSet; - -import com.mcmoddev.baseminerals.data.MaterialNames; -import com.mcmoddev.baseminerals.init.ItemGroups; -import com.mcmoddev.baseminerals.init.Recipes; -import com.mcmoddev.baseminerals.init.VillagerTrades; -import com.mcmoddev.baseminerals.util.BMiConfig; -import com.mcmoddev.lib.data.SharedStrings; -import com.mcmoddev.lib.oregen.FallbackGenerator; -import com.mcmoddev.lib.util.Config.Options; - -import net.minecraftforge.fml.common.Loader; -import net.minecraftforge.fml.common.MissingModsException; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.registry.GameRegistry; -import net.minecraftforge.fml.common.versioning.ArtifactVersion; -import net.minecraftforge.fml.common.versioning.DefaultArtifactVersion; - -/** - * Base Minerals Common Proxy - * - * @author Jasmine Iwanek - * - */ -public class CommonProxy { - - @SuppressWarnings("deprecation") - public void preInit(FMLPreInitializationEvent event) { - if ((Options.requireMMDOreSpawn()) && (!Loader.isModLoaded(SharedStrings.ORESPAWN_MODID))) { - if (Options.fallbackOrespawn()) { - GameRegistry.registerWorldGenerator(new FallbackGenerator(), 0); - } else { - final HashSet orespawnMod = new HashSet<>(); - orespawnMod.add(new DefaultArtifactVersion(SharedStrings.ORESPAWN_VERSION)); - throw new MissingModsException(orespawnMod, SharedStrings.ORESPAWN_MODID, - SharedStrings.ORESPAWN_MISSING_TEXT); - } - } - - VillagerTrades.init(); - ItemGroups.init(); - } - - - public void init(FMLInitializationEvent event) { - Recipes.init(); - - ItemGroups.setupIcons(MaterialNames.SILICON); - } - - public void postInit(FMLPostInitializationEvent event) { - BMiConfig.postInit(); - } -} diff --git a/src/main/java/com/mcmoddev/baseminerals/util/BMiConfig.java b/src/main/java/com/mcmoddev/baseminerals/util/BMiConfig.java deleted file mode 100644 index d7f6360..0000000 --- a/src/main/java/com/mcmoddev/baseminerals/util/BMiConfig.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.mcmoddev.baseminerals.util; - -import java.io.File; - -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; -import com.mcmoddev.lib.registry.CrusherRecipeRegistry; -import com.mcmoddev.lib.util.Config; -import com.mcmoddev.lib.util.MaterialConfigOptions; - -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.fml.client.event.ConfigChangedEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; - -/** - * @author Jasmine Iwanek - * - */ -public class BMiConfig extends Config { - - private static Configuration configuration; - private static final String CONFIG_FILE = "config/BaseMinerals.cfg"; - - private static final MaterialConfigOptions[] MATERIAL_CONFIG_OPTIONS = new MaterialConfigOptions[]{ - new MaterialConfigOptions(MaterialNames.CHARCOAL, true, true, true, true), - new MaterialConfigOptions(MaterialNames.LITHIUM, false, true, true, true), - new MaterialConfigOptions(MaterialNames.NITER, false, true, false, false), - new MaterialConfigOptions(MaterialNames.PHOSPHORUS, false, true, false, false), - new MaterialConfigOptions(MaterialNames.POTASH, false, true, false, false), - new MaterialConfigOptions(MaterialNames.SALT, false, true, false, false), - new MaterialConfigOptions(MaterialNames.SALTPETER, false, true, false, false), - new MaterialConfigOptions(MaterialNames.SILICON, false, true, false, false), - new MaterialConfigOptions(MaterialNames.SULFUR, false, true, false, false) - }; - - @SubscribeEvent - public void onConfigChange(ConfigChangedEvent.OnConfigChangedEvent e) { - if (e.getModID().equals(BaseMinerals.MODID)) { - init(); - } - } - - public static void init() { - if (configuration == null) { - configuration = new Configuration(new File(CONFIG_FILE)); - MinecraftForge.EVENT_BUS.register(new BMiConfig()); - } - - configMaterialOptions(MATERIAL_CONFIG_OPTIONS, configuration); - - if (configuration.hasChanged()) { - configuration.save(); - } - } - - public static void postInit() { - CrusherRecipeRegistry.getInstance().clearCache(); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/BaseMineralsOreSpawn.java b/src/main/java/com/mcmoddev/orespawn/BaseMineralsOreSpawn.java deleted file mode 100644 index 7b372ec..0000000 --- a/src/main/java/com/mcmoddev/orespawn/BaseMineralsOreSpawn.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.mcmoddev.orespawn; - -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.lib.data.SharedStrings; -import com.mcmoddev.orespawn.api.os3.OS3API; -import com.mcmoddev.orespawn.api.plugin.IOreSpawnPlugin; -import com.mcmoddev.orespawn.api.plugin.OreSpawnPlugin; - -@OreSpawnPlugin(modid = BaseMinerals.MODID, resourcePath = SharedStrings.ORESPAWN_MODID) -public class BaseMineralsOreSpawn implements IOreSpawnPlugin { - - @Override - public void register(OS3API apiInterface) { - // nothing for us to do - all of our ores are in the - // jar and the code handles that - } -} diff --git a/src/main/java/com/mcmoddev/baseminerals/BaseMinerals.java b/src/main/java/zone/moddev/mc/baseminerals/BaseMinerals.java similarity index 72% rename from src/main/java/com/mcmoddev/baseminerals/BaseMinerals.java rename to src/main/java/zone/moddev/mc/baseminerals/BaseMinerals.java index 1e576c1..cc71861 100644 --- a/src/main/java/com/mcmoddev/baseminerals/BaseMinerals.java +++ b/src/main/java/zone/moddev/mc/baseminerals/BaseMinerals.java @@ -1,10 +1,10 @@ -package com.mcmoddev.baseminerals; +package zone.moddev.mc.baseminerals; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.mcmoddev.baseminerals.util.BMiConfig; -import com.mcmoddev.baseminerals.proxy.CommonProxy; +import zone.moddev.mc.baseminerals.util.BMiConfig; +import zone.moddev.mc.baseminerals.proxy.CommonProxy; import com.mcmoddev.lib.data.SharedStrings; import net.minecraftforge.common.MinecraftForge; @@ -28,9 +28,9 @@ modid = BaseMinerals.MODID, name = BaseMinerals.NAME, version = BaseMinerals.VERSION, - dependencies = "required-after:forge@[14.21.1.2387,);required-after:mmdlib;before:appliedenergistics2;before:buildingbricks", - acceptedMinecraftVersions = "[1.12,)", - certificateFingerprint = "@FINGERPRINT@", + dependencies = "required-after:forge@[14.23.5.2859,);required-after:mmdlib@[1.0.0-rc2,2.0.0);required-after:orespawn@[4.1.6.112021,5.0.0);after:basemetals;after:tconstruct;after:enderio;before:appliedenergistics2;before:buildingbricks", + acceptedMinecraftVersions = "[1.12.2]", + guiFactory = "zone.moddev.mc.baseminerals.client.BaseMineralsGuiFactory", updateJSON = BaseMinerals.UPDATEJSON) public class BaseMinerals { @@ -48,11 +48,11 @@ public class BaseMinerals { * increased whenever a change is made that has the potential to break * compatibility with other mods that depend on this one. */ - protected static final String VERSION = "2.5.0-rc2"; + public static final String VERSION = "2.5.1.112021"; - protected static final String UPDATEJSON = SharedStrings.UPDATE_JSON_URL + "BaseMinerals/master/update.json"; + protected static final String UPDATEJSON = SharedStrings.UPDATE_JSON_URL + "BaseMinerals/master-1.12/update.json"; - private static final String PROXY_BASE = SharedStrings.MMD_PROXY_GROUP + MODID + SharedStrings.DOT_PROXY_DOT; + private static final String PROXY_BASE = "zone.moddev.mc.baseminerals.proxy."; @SidedProxy(clientSide = PROXY_BASE + SharedStrings.CLIENTPROXY, serverSide = PROXY_BASE + SharedStrings.SERVERPROXY) @@ -74,8 +74,8 @@ public static void constructing(FMLConstructionEvent event) { public static void preInit(FMLPreInitializationEvent event) { proxy.preInit(event); - MinecraftForge.EVENT_BUS.register(com.mcmoddev.baseminerals.init.Items.class); - MinecraftForge.EVENT_BUS.register(com.mcmoddev.baseminerals.init.Blocks.class); + MinecraftForge.EVENT_BUS.register(zone.moddev.mc.baseminerals.init.Items.class); + MinecraftForge.EVENT_BUS.register(zone.moddev.mc.baseminerals.init.Blocks.class); } @EventHandler diff --git a/src/main/java/zone/moddev/mc/baseminerals/block/OverlayOreBlock.java b/src/main/java/zone/moddev/mc/baseminerals/block/OverlayOreBlock.java new file mode 100644 index 0000000..b77b9a7 --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/block/OverlayOreBlock.java @@ -0,0 +1,20 @@ +package zone.moddev.mc.baseminerals.block; + +import com.mcmoddev.lib.block.BlockMMDOre; +import com.mcmoddev.lib.material.MMDMaterial; +import net.minecraft.util.BlockRenderLayer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +/** Uses the shared ore behaviour with alpha testing for overlay models. */ +public final class OverlayOreBlock extends BlockMMDOre { + public OverlayOreBlock(MMDMaterial material) { + super(material); + } + + @Override + @SideOnly(Side.CLIENT) + public BlockRenderLayer getRenderLayer() { + return BlockRenderLayer.CUTOUT_MIPPED; + } +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/client/BaseMineralsConfigScreen.java b/src/main/java/zone/moddev/mc/baseminerals/client/BaseMineralsConfigScreen.java new file mode 100644 index 0000000..e8dadcd --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/client/BaseMineralsConfigScreen.java @@ -0,0 +1,30 @@ +package zone.moddev.mc.baseminerals.client; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraftforge.common.config.ConfigElement; +import net.minecraftforge.fml.client.config.GuiConfig; +import net.minecraftforge.fml.client.config.IConfigElement; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.util.BMiConfig; + +/** One parent-aware screen for Forge's mod list and OreSpawn's child-mod list. */ +@SideOnly(Side.CLIENT) +public final class BaseMineralsConfigScreen extends GuiConfig { + public BaseMineralsConfigScreen(final GuiScreen parent) { + super(parent, elements(), + BaseMinerals.MODID, false, true, I18n.format("config.baseminerals.title"), + I18n.format("config.baseminerals.description")); + } + private static List elements() { + final List elements = new ArrayList<>( + new ConfigElement(BMiConfig.getConfiguration().getCategory(BMiConfig.MATERIALS)).getChildElements()); + elements.addAll(new ConfigElement(BMiConfig.getConfiguration().getCategory(BMiConfig.FLUIDS)).getChildElements()); + elements.addAll(new ConfigElement(BMiConfig.getConfiguration().getCategory(BMiConfig.VANILLA)).getChildElements()); + return elements; + } +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/client/BaseMineralsGuiFactory.java b/src/main/java/zone/moddev/mc/baseminerals/client/BaseMineralsGuiFactory.java new file mode 100644 index 0000000..2f0045c --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/client/BaseMineralsGuiFactory.java @@ -0,0 +1,18 @@ +package zone.moddev.mc.baseminerals.client; + +import java.util.Collections; +import java.util.Set; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.fml.client.IModGuiFactory; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +/** Forge 1.12's configuration factory; never loaded by a dedicated server. */ +@SideOnly(Side.CLIENT) +public final class BaseMineralsGuiFactory implements IModGuiFactory { + @Override public void initialize(final Minecraft minecraft) { } + @Override public boolean hasConfigGui() { return true; } + @Override public GuiScreen createConfigGui(final GuiScreen parent) { return new BaseMineralsConfigScreen(parent); } + @Override public Set runtimeGuiCategories() { return Collections.emptySet(); } +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/compat/BaseMineralsLoadingPlugin.java b/src/main/java/zone/moddev/mc/baseminerals/compat/BaseMineralsLoadingPlugin.java new file mode 100644 index 0000000..8249f6e --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/compat/BaseMineralsLoadingPlugin.java @@ -0,0 +1,19 @@ +package zone.moddev.mc.baseminerals.compat; + +import java.util.Map; +import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; + +/** Relinks add-on class names and leaves MMDLib's shared API in its own jar. */ +@IFMLLoadingPlugin.Name("BaseMinerals") +@IFMLLoadingPlugin.MCVersion("1.12.2") +@IFMLLoadingPlugin.SortingIndex(1002) +@IFMLLoadingPlugin.TransformerExclusions("zone.moddev.mc.baseminerals.compat.") +public final class BaseMineralsLoadingPlugin implements IFMLLoadingPlugin { + @Override public String[] getASMTransformerClass() { + return new String[] { "zone.moddev.mc.baseminerals.compat.LegacyNamespaceTransformer" }; + } + @Override public String getModContainerClass() { return null; } + @Override public String getSetupClass() { return null; } + @Override public void injectData(final Map data) { } + @Override public String getAccessTransformerClass() { return null; } +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/compat/LegacyNamespaceTransformer.java b/src/main/java/zone/moddev/mc/baseminerals/compat/LegacyNamespaceTransformer.java new file mode 100644 index 0000000..848c833 --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/compat/LegacyNamespaceTransformer.java @@ -0,0 +1,89 @@ +package zone.moddev.mc.baseminerals.compat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import net.minecraft.launchwrapper.IClassTransformer; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.commons.Remapper; +import org.objectweb.asm.commons.RemappingClassAdapter; + +/** Relinks existing MMD add-ons to the same classes and registries as Base Minerals. */ +public final class LegacyNamespaceTransformer implements IClassTransformer { + + private static final String OLD = "com/mcmoddev/baseminerals/"; + private static final String CURRENT = "zone/moddev/mc/baseminerals/"; + private static final Set EXPORTS = loadExports(); + + @Override + public byte[] transform(final String name, final String transformedName, final byte[] bytes) { + if (bytes == null || name == null || name.startsWith("zone.moddev.mc.baseminerals.") + || !(contains(bytes, OLD) || contains(bytes, OLD.replace('/', '.')))) { + return bytes; + } + + final ClassWriter output = new ClassWriter(0); + final Remapper names = new Remapper() { + @Override + public String map(final String internalName) { + return currentName(internalName); + } + + @Override + public Object mapValue(final Object value) { + if (value instanceof String) { + final String text = (String) value; + final String internal = text.replace('.', '/'); + final String mapped = currentName(internal); + if (mapped.equals(internal)) return value; + return text.indexOf('/') < 0 ? mapped.replace('/', '.') : mapped; + } + return super.mapValue(value); + } + }; + + new ClassReader(bytes).accept(new RemappingClassAdapter(output, names), 0); + return output.toByteArray(); + } + + private static String currentName(final String name) { + if (!name.startsWith(OLD)) return name; + + final String suffix = name.substring(OLD.length()); + final int nested = suffix.indexOf('$'); + final String owner = nested < 0 ? suffix : suffix.substring(0, nested); + return EXPORTS.contains(owner) ? CURRENT + suffix : name; + } + + private static Set loadExports() { + final Set result = new HashSet<>(); + final InputStream stream = LegacyNamespaceTransformer.class.getClassLoader() + .getResourceAsStream("META-INF/baseminerals-legacy-api.list"); + if (stream == null) throw new IllegalStateException("Missing Base Minerals API name list"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty() && !line.startsWith("#")) result.add(line); + } + } catch (final IOException failure) { + throw new IllegalStateException("Cannot read Base Minerals API names", failure); + } + return Collections.unmodifiableSet(result); + } + + private static boolean contains(final byte[] bytes, final String text) { + for (int offset = 0; offset <= bytes.length - text.length(); offset++) { + int index = 0; + while (index < text.length() && bytes[offset + index] == text.charAt(index)) index++; + if (index == text.length()) return true; + } + return false; + } +} diff --git a/src/main/java/com/mcmoddev/baseminerals/data/MaterialNames.java b/src/main/java/zone/moddev/mc/baseminerals/data/MaterialNames.java old mode 100755 new mode 100644 similarity index 91% rename from src/main/java/com/mcmoddev/baseminerals/data/MaterialNames.java rename to src/main/java/zone/moddev/mc/baseminerals/data/MaterialNames.java index e931689..4f3d6c4 --- a/src/main/java/com/mcmoddev/baseminerals/data/MaterialNames.java +++ b/src/main/java/zone/moddev/mc/baseminerals/data/MaterialNames.java @@ -1,20 +1,20 @@ -package com.mcmoddev.baseminerals.data; - -import com.mcmoddev.lib.data.SharedStrings; - -public final class MaterialNames { - - public static final String SULFUR = "sulfur"; - public static final String SILICON = "silicon"; - public static final String SALTPETER = "saltpeter"; - public static final String SALT = "salt"; - public static final String POTASH = "potash"; - public static final String NITER = "niter"; - public static final String PHOSPHORUS = "phosphorus"; - public static final String LITHIUM = "lithium"; - public static final String CHARCOAL = "charcoal"; // Vanilla - - private MaterialNames() { - throw new IllegalAccessError(SharedStrings.NOT_INSTANTIABLE); - } -} +package zone.moddev.mc.baseminerals.data; + +import com.mcmoddev.lib.data.SharedStrings; + +public final class MaterialNames { + + public static final String SULFUR = "sulfur"; + public static final String SILICON = "silicon"; + public static final String SALTPETER = "saltpeter"; + public static final String SALT = "salt"; + public static final String POTASH = "potash"; + public static final String NITER = "niter"; + public static final String PHOSPHORUS = "phosphorus"; + public static final String LITHIUM = "lithium"; + public static final String CHARCOAL = "charcoal"; // Vanilla + + private MaterialNames() { + throw new IllegalAccessError(SharedStrings.NOT_INSTANTIABLE); + } +} diff --git a/src/main/java/com/mcmoddev/baseminerals/init/Blocks.java b/src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java similarity index 63% rename from src/main/java/com/mcmoddev/baseminerals/init/Blocks.java rename to src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java index d731071..66546ca 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/Blocks.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java @@ -1,10 +1,12 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; import java.util.Arrays; import java.util.List; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.block.OverlayOreBlock; +import com.mcmoddev.lib.util.Oredicts; import com.mcmoddev.lib.data.Names; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.material.MMDMaterial; @@ -29,7 +31,7 @@ protected Blocks() { * */ public static void init() { - final List materials = Arrays.asList(MaterialNames.CHARCOAL, MaterialNames.LITHIUM, MaterialNames.NITER, + final List materials = Arrays.asList(MaterialNames.LITHIUM, MaterialNames.NITER, MaterialNames.PHOSPHORUS, MaterialNames.POTASH, MaterialNames.SALT, MaterialNames.SALTPETER, MaterialNames.SILICON, MaterialNames.SULFUR); @@ -39,7 +41,12 @@ public static void init() { final MMDMaterial material = Materials.getMaterialByName(materialName); create(Names.BLOCK, material); - create(Names.ORE, material); + if (material.hasOre() && !material.hasBlock(Names.ORE) && isNameEnabled(Names.ORE)) { + final Block ore = addBlock(new OverlayOreBlock(material), Names.ORE, material, + ItemGroups.getTab(BaseMinerals.MODID, SharedStrings.TAB_BLOCKS)); + material.addNewBlock(Names.ORE.toString(), ore); + Oredicts.registerOre("ore" + material.getCapitalizedName(), ore); + } }); } diff --git a/src/main/java/com/mcmoddev/baseminerals/init/Fluids.java b/src/main/java/zone/moddev/mc/baseminerals/init/Fluids.java similarity index 89% rename from src/main/java/com/mcmoddev/baseminerals/init/Fluids.java rename to src/main/java/zone/moddev/mc/baseminerals/init/Fluids.java index cc23b53..97aabc2 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/Fluids.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/Fluids.java @@ -1,9 +1,9 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; import java.util.Arrays; import java.util.List; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.util.Config.Options; diff --git a/src/main/java/com/mcmoddev/baseminerals/init/ItemGroups.java b/src/main/java/zone/moddev/mc/baseminerals/init/ItemGroups.java similarity index 91% rename from src/main/java/com/mcmoddev/baseminerals/init/ItemGroups.java rename to src/main/java/zone/moddev/mc/baseminerals/init/ItemGroups.java index 069e21a..8d41508 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/ItemGroups.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/ItemGroups.java @@ -1,7 +1,7 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; import com.mcmoddev.lib.init.Materials; -import com.mcmoddev.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.BaseMinerals; import com.mcmoddev.lib.data.Names; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.material.MMDMaterial; diff --git a/src/main/java/com/mcmoddev/baseminerals/init/Items.java b/src/main/java/zone/moddev/mc/baseminerals/init/Items.java similarity index 65% rename from src/main/java/com/mcmoddev/baseminerals/init/Items.java rename to src/main/java/zone/moddev/mc/baseminerals/init/Items.java index 9fc10b4..cce6694 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/Items.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/Items.java @@ -1,19 +1,14 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; import java.util.Arrays; import java.util.List; -import javax.annotation.Nonnull; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.data.Names; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.init.Materials; -import com.mcmoddev.lib.item.ItemMMDBlock; -import com.mcmoddev.lib.item.ItemMMDNugget; -import com.mcmoddev.lib.item.ItemMMDPowder; -import com.mcmoddev.lib.item.ItemMMDSmallPowder; import com.mcmoddev.lib.material.MMDMaterial; import com.mcmoddev.lib.util.Oredicts; @@ -39,7 +34,7 @@ protected Items() { * */ public static void init() { - final List materials = Arrays.asList(MaterialNames.CHARCOAL, MaterialNames.LITHIUM, MaterialNames.NITER, + final List materials = Arrays.asList(MaterialNames.LITHIUM, MaterialNames.NITER, MaterialNames.PHOSPHORUS, MaterialNames.POTASH, MaterialNames.SALT, MaterialNames.SALTPETER, MaterialNames.SILICON, MaterialNames.SULFUR); @@ -59,36 +54,13 @@ public static void init() { .forEach(material -> create(name, Materials.getMaterialByName(material))) ); - if (Materials.hasMaterial(MaterialNames.CHARCOAL)) { - setBurnTimes(MaterialNames.CHARCOAL); - } - if (Materials.hasMaterial(MaterialNames.SILICON)) { final MMDMaterial silicon = Materials.getMaterialByName(MaterialNames.SILICON); - create(Names.BLEND, silicon); create(Names.SMALLBLEND, silicon); } } - private static void setBurnTimes(@Nonnull final String materialName) { - final MMDMaterial mat = Materials.getMaterialByName(materialName); - if (mat.isEmpty()) return; - - if (mat.hasItem(Names.NUGGET)) - ((ItemMMDNugget) mat.getItem(Names.NUGGET)).setBurnTime(160); - - if (mat.hasItem(Names.POWDER)) - ((ItemMMDPowder) mat.getItem(Names.POWDER)).setBurnTime(1600); - - if (mat.hasItem(Names.SMALLPOWDER)) - ((ItemMMDSmallPowder) mat.getItem(Names.SMALLPOWDER)).setBurnTime(160); - - // simple hack to fix this shit - I give up on trying for more - if (mat.hasBlock(Names.BLOCK) && mat.getName().equals(MaterialNames.CHARCOAL)) - ((ItemMMDBlock) mat.getItem("ItemBlock_charcoal_block")).setBurnTime(16000); - } - @SubscribeEvent public static void registerItems(RegistryEvent.Register event) { Materials.getMaterialsByMod(BaseMinerals.MODID).stream() diff --git a/src/main/java/zone/moddev/mc/baseminerals/init/MaterialEnabledCondition.java b/src/main/java/zone/moddev/mc/baseminerals/init/MaterialEnabledCondition.java new file mode 100644 index 0000000..4388c30 --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/init/MaterialEnabledCondition.java @@ -0,0 +1,17 @@ +package zone.moddev.mc.baseminerals.init; + +import com.google.gson.JsonObject; +import java.util.function.BooleanSupplier; +import net.minecraft.util.JsonUtils; +import net.minecraftforge.common.crafting.IConditionFactory; +import net.minecraftforge.common.crafting.JsonContext; +import zone.moddev.mc.baseminerals.util.BMiConfig; + +/** Uses the add-on's latched switches without changing another mod's materials. */ +public final class MaterialEnabledCondition implements IConditionFactory { + @Override + public BooleanSupplier parse(JsonContext context, JsonObject condition) { + String material = JsonUtils.getString(condition, "material"); + return () -> BMiConfig.isEnabled(material); + } +} diff --git a/src/main/java/com/mcmoddev/baseminerals/init/Materials.java b/src/main/java/zone/moddev/mc/baseminerals/init/Materials.java similarity index 97% rename from src/main/java/com/mcmoddev/baseminerals/init/Materials.java rename to src/main/java/zone/moddev/mc/baseminerals/init/Materials.java index c1c9c72..38e52f7 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/Materials.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/Materials.java @@ -1,11 +1,11 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; import java.util.Arrays; import java.util.List; import javax.annotation.Nonnull; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.material.MMDMaterialType.MaterialType; import com.mcmoddev.lib.util.Config.Options; diff --git a/src/main/java/com/mcmoddev/baseminerals/init/Recipes.java b/src/main/java/zone/moddev/mc/baseminerals/init/Recipes.java similarity index 68% rename from src/main/java/com/mcmoddev/baseminerals/init/Recipes.java rename to src/main/java/zone/moddev/mc/baseminerals/init/Recipes.java index 23526e6..17b60cd 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/Recipes.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/Recipes.java @@ -1,10 +1,12 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.data.Names; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.material.MMDMaterial; +import net.minecraft.item.ItemStack; +import net.minecraft.item.crafting.FurnaceRecipes; import net.minecraftforge.fml.common.registry.GameRegistry; import net.minecraftforge.oredict.OreDictionary; @@ -33,7 +35,10 @@ private static void initModSpecificRecipes() { if (silicon.hasItem(Names.INGOT)) { OreDictionary.registerOre("itemSilicon", silicon.getItem(Names.INGOT)); if (silicon.hasItem(Names.POWDER)) { - GameRegistry.addSmelting(silicon.getItemStack(Names.POWDER), silicon.getItemStack(Names.INGOT), 0); + ItemStack powder = silicon.getItemStack(Names.POWDER); + if (FurnaceRecipes.instance().getSmeltingResult(powder).isEmpty()) { + GameRegistry.addSmelting(powder, silicon.getItemStack(Names.INGOT), 0); + } } } } diff --git a/src/main/java/com/mcmoddev/baseminerals/init/VillagerTrades.java b/src/main/java/zone/moddev/mc/baseminerals/init/VillagerTrades.java similarity index 88% rename from src/main/java/com/mcmoddev/baseminerals/init/VillagerTrades.java rename to src/main/java/zone/moddev/mc/baseminerals/init/VillagerTrades.java index 0723e2f..c5eb213 100644 --- a/src/main/java/com/mcmoddev/baseminerals/init/VillagerTrades.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/VillagerTrades.java @@ -1,4 +1,4 @@ -package com.mcmoddev.baseminerals.init; +package zone.moddev.mc.baseminerals.init; import com.mcmoddev.lib.data.SharedStrings; diff --git a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiDenseOres.java b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiDenseOres.java similarity index 88% rename from src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiDenseOres.java rename to src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiDenseOres.java index 2ab137c..f368510 100644 --- a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiDenseOres.java +++ b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiDenseOres.java @@ -1,7 +1,7 @@ -package com.mcmoddev.baseminerals.integration.plugins; +package zone.moddev.mc.baseminerals.integration.plugins; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.util.Config.Options; import com.mcmoddev.lib.init.Materials; import com.mcmoddev.lib.integration.IIntegration; diff --git a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiEnderIO.java b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiEnderIO.java similarity index 85% rename from src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiEnderIO.java rename to src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiEnderIO.java index 6602f78..6a60499 100644 --- a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiEnderIO.java +++ b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiEnderIO.java @@ -1,7 +1,7 @@ -package com.mcmoddev.baseminerals.integration.plugins; +package zone.moddev.mc.baseminerals.integration.plugins; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.integration.MMDPlugin; import com.mcmoddev.lib.util.Config.Options; import com.mcmoddev.lib.integration.IIntegration; diff --git a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiMekanism.java b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiMekanism.java similarity index 91% rename from src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiMekanism.java rename to src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiMekanism.java index 988147e..473b299 100644 --- a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiMekanism.java +++ b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiMekanism.java @@ -1,12 +1,12 @@ -package com.mcmoddev.baseminerals.integration.plugins; +package zone.moddev.mc.baseminerals.integration.plugins; import java.util.Arrays; import javax.annotation.Nonnull; import com.mcmoddev.lib.init.Materials; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.integration.MMDPlugin; import com.mcmoddev.lib.material.MMDMaterial; import com.mcmoddev.lib.util.Config.Options; diff --git a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiThaumcraft.java b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiThaumcraft.java similarity index 80% rename from src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiThaumcraft.java rename to src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiThaumcraft.java index e3b0b19..ff2436b 100644 --- a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiThaumcraft.java +++ b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiThaumcraft.java @@ -1,6 +1,6 @@ -package com.mcmoddev.baseminerals.integration.plugins; +package zone.moddev.mc.baseminerals.integration.plugins; -import com.mcmoddev.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.BaseMinerals; import com.mcmoddev.lib.integration.MMDPlugin; import com.mcmoddev.lib.util.Config.Options; import com.mcmoddev.lib.integration.IIntegration; diff --git a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiThermalExpansion.java b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiThermalExpansion.java similarity index 91% rename from src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiThermalExpansion.java rename to src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiThermalExpansion.java index 871c889..56506ec 100644 --- a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiThermalExpansion.java +++ b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiThermalExpansion.java @@ -1,11 +1,11 @@ -package com.mcmoddev.baseminerals.integration.plugins; +package zone.moddev.mc.baseminerals.integration.plugins; import java.util.Arrays; import javax.annotation.Nonnull; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.data.Names; import com.mcmoddev.lib.init.Materials; import com.mcmoddev.lib.integration.IIntegration; diff --git a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiTinkersConstruct.java b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiTinkersConstruct.java similarity index 85% rename from src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiTinkersConstruct.java rename to src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiTinkersConstruct.java index 12005f6..5d520a2 100644 --- a/src/main/java/com/mcmoddev/baseminerals/integration/plugins/BMiTinkersConstruct.java +++ b/src/main/java/zone/moddev/mc/baseminerals/integration/plugins/BMiTinkersConstruct.java @@ -1,10 +1,10 @@ -package com.mcmoddev.baseminerals.integration.plugins; +package zone.moddev.mc.baseminerals.integration.plugins; import java.util.Arrays; -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.data.MaterialNames; -import com.mcmoddev.baseminerals.init.Materials; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.init.Materials; import com.mcmoddev.lib.integration.MMDPlugin; import com.mcmoddev.lib.integration.IIntegration; import com.mcmoddev.lib.material.MMDMaterial; diff --git a/src/main/java/zone/moddev/mc/baseminerals/proxy/ClientProxy.java b/src/main/java/zone/moddev/mc/baseminerals/proxy/ClientProxy.java new file mode 100644 index 0000000..861b164 --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/proxy/ClientProxy.java @@ -0,0 +1,46 @@ +package zone.moddev.mc.baseminerals.proxy; + +import com.mcmoddev.lib.client.registrations.RegistrationHelper; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.event.ModelRegistryEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.client.BaseMineralsConfigScreen; +import zone.moddev.mc.baseminerals.init.Blocks; +import zone.moddev.mc.baseminerals.init.Fluids; +import zone.moddev.mc.baseminerals.init.Items; +import zone.moddev.mc.orespawn.api.client.WorldSettingsExtensionRegistry; + +public class ClientProxy extends CommonProxy { + @Override + public void preInit(FMLPreInitializationEvent event) { + super.preInit(event); + MinecraftForge.EVENT_BUS.register(this); + } + + @Override + public void init(FMLInitializationEvent event) { + super.init(event); + WorldSettingsExtensionRegistry.registerConfigScreen(BaseMinerals.MODID, BaseMineralsConfigScreen::new); + } + + @SubscribeEvent + public void modelRegistry(ModelRegistryEvent event) { + Items.getItemRegistry().forEach((name, item) -> { + if (owned(item.getRegistryName())) RegistrationHelper.registerItemRender(name); + }); + Blocks.getBlockRegistry().forEach((name, block) -> { + if (owned(block.getRegistryName())) RegistrationHelper.registerBlockRender(name); + }); + Fluids.getFluidBlockRegistry().forEach((name, block) -> { + if (owned(block.getRegistryName())) RegistrationHelper.registerFluidRender(name); + }); + } + + private static boolean owned(ResourceLocation name) { + return name != null && BaseMinerals.MODID.equals(name.getNamespace()); + } +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/proxy/CommonProxy.java b/src/main/java/zone/moddev/mc/baseminerals/proxy/CommonProxy.java new file mode 100644 index 0000000..59beadb --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/proxy/CommonProxy.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.baseminerals.proxy; + + +import zone.moddev.mc.baseminerals.data.MaterialNames; +import zone.moddev.mc.baseminerals.init.ItemGroups; +import zone.moddev.mc.baseminerals.init.Recipes; +import zone.moddev.mc.baseminerals.init.VillagerTrades; +import zone.moddev.mc.baseminerals.util.BMiConfig; +import zone.moddev.mc.baseminerals.worldgen.BaseMineralsWorldgenProvider; + +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; + +/** + * Base Minerals Common Proxy + * + * @author Jasmine Iwanek + * + */ +public class CommonProxy { + + @SuppressWarnings("deprecation") + public void preInit(FMLPreInitializationEvent event) { + VillagerTrades.init(); + ItemGroups.init(); + } + + + public void init(FMLInitializationEvent event) { + BaseMineralsWorldgenProvider.enqueue(); + Recipes.init(); + + ItemGroups.setupIcons(MaterialNames.SILICON); + } + + public void postInit(FMLPostInitializationEvent event) { + BMiConfig.postInit(); + } +} diff --git a/src/main/java/com/mcmoddev/baseminerals/proxy/ServerProxy.java b/src/main/java/zone/moddev/mc/baseminerals/proxy/ServerProxy.java similarity index 77% rename from src/main/java/com/mcmoddev/baseminerals/proxy/ServerProxy.java rename to src/main/java/zone/moddev/mc/baseminerals/proxy/ServerProxy.java index 27e8e98..ace93fa 100644 --- a/src/main/java/com/mcmoddev/baseminerals/proxy/ServerProxy.java +++ b/src/main/java/zone/moddev/mc/baseminerals/proxy/ServerProxy.java @@ -1,4 +1,4 @@ -package com.mcmoddev.baseminerals.proxy; +package zone.moddev.mc.baseminerals.proxy; /** * Base Minerals Server Proxy diff --git a/src/main/java/zone/moddev/mc/baseminerals/util/BMiConfig.java b/src/main/java/zone/moddev/mc/baseminerals/util/BMiConfig.java new file mode 100644 index 0000000..559f571 --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/util/BMiConfig.java @@ -0,0 +1,70 @@ +package zone.moddev.mc.baseminerals.util; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import com.mcmoddev.lib.registry.CrusherRecipeRegistry; +import com.mcmoddev.lib.util.Config; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.common.config.Property; +import net.minecraftforge.fml.client.event.ConfigChangedEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.data.MaterialNames; + +/** Saves requested settings and latches registration choices until restart. */ +public class BMiConfig extends Config { + public static final String MATERIALS = "Metals"; + public static final String VANILLA = "Vanilla"; + public static final String FLUIDS = "Fluids"; + private static final Map active = new HashMap<>(); + private static Configuration configuration; + + public static void init() { + if (configuration != null) return; + configuration = new Configuration(new File("config/BaseMinerals.cfg")); + load(configuration); + if (configuration.hasChanged()) configuration.save(); + MinecraftForge.EVENT_BUS.register(new BMiConfig()); + } + + static void load(Configuration value) { + for (String name : new String[]{MaterialNames.CHARCOAL, MaterialNames.LITHIUM, + MaterialNames.NITER, MaterialNames.PHOSPHORUS, MaterialNames.POTASH, MaterialNames.SALT, + MaterialNames.SALTPETER, MaterialNames.SILICON, MaterialNames.SULFUR}) { + String title = Character.toUpperCase(name.charAt(0)) + name.substring(1); + boolean charcoal = MaterialNames.CHARCOAL.equals(name); + Property material = value.get(charcoal ? VANILLA : MATERIALS, "Enable" + title, true); + material.setRequiresMcRestart(true); + material.setLanguageKey("config.baseminerals.enable_" + name); + boolean enabled = material.getBoolean(); + active.put(name, enabled); + if (!charcoal) Options.materialEnabled(name, enabled); + if (MaterialNames.LITHIUM.equals(name) || MaterialNames.SILICON.equals(name)) { + Property fluid = value.get(FLUIDS, "Enabled " + title, true); + fluid.setRequiresMcRestart(true); + fluid.setLanguageKey("config.baseminerals.fluid_" + name); + Options.fluidEnabled(name, enabled && fluid.getBoolean()); + } + } + } + + public static boolean isEnabled(String name) { + return Boolean.TRUE.equals(active.get(name)); + } + + public static Configuration getConfiguration() { + if (configuration == null) init(); + return configuration; + } + + @SubscribeEvent + public void onConfigChange(ConfigChangedEvent.OnConfigChangedEvent event) { + if (BaseMinerals.MODID.equals(event.getModID())) configuration.save(); + } + + public static void postInit() { + CrusherRecipeRegistry.getInstance().clearCache(); + } +} diff --git a/src/main/java/com/mcmoddev/baseminerals/util/EventHandler.java b/src/main/java/zone/moddev/mc/baseminerals/util/EventHandler.java similarity index 79% rename from src/main/java/com/mcmoddev/baseminerals/util/EventHandler.java rename to src/main/java/zone/moddev/mc/baseminerals/util/EventHandler.java index 5b8e367..5e94566 100644 --- a/src/main/java/com/mcmoddev/baseminerals/util/EventHandler.java +++ b/src/main/java/zone/moddev/mc/baseminerals/util/EventHandler.java @@ -1,57 +1,57 @@ -package com.mcmoddev.baseminerals.util; - -import com.mcmoddev.baseminerals.BaseMinerals; -import com.mcmoddev.baseminerals.init.Blocks; -import com.mcmoddev.baseminerals.init.Fluids; -import com.mcmoddev.baseminerals.init.Items; -import com.mcmoddev.baseminerals.init.Materials; -import com.mcmoddev.lib.events.*; - -import net.minecraftforge.event.RegistryEvent; -import net.minecraftforge.fml.common.Mod.EventBusSubscriber; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; - -import net.minecraft.block.Block; -import net.minecraft.item.Item; - -@EventBusSubscriber -public class EventHandler { - @SubscribeEvent - public static void mmdlibRegisterMaterials(MMDLibRegisterMaterials ev) { - Materials.init(); - } - - @SubscribeEvent - public static void mmdlibRegisterBlocks(MMDLibRegisterBlocks ev) { - Blocks.init(); - } - - @SubscribeEvent - public static void mmdlibRegisterItems(MMDLibRegisterItems ev) { - Items.init(); - } - - @SubscribeEvent - public static void mmdlibRegisterFluids(MMDLibRegisterFluids ev) { - Fluids.init(); - } - - @SubscribeEvent - public static void onRemapBlock(RegistryEvent.MissingMappings event) { - for (final RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { - if (mapping.key.getNamespace().equals(BaseMinerals.MODID)) { - // dummy - } - } - } - - @SubscribeEvent - public static void onRemapItem(RegistryEvent.MissingMappings event) { - for (final RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { - if (mapping.key.getNamespace().equals(BaseMinerals.MODID)) { - // dummy - } - } - } - -} +package zone.moddev.mc.baseminerals.util; + +import zone.moddev.mc.baseminerals.BaseMinerals; +import zone.moddev.mc.baseminerals.init.Blocks; +import zone.moddev.mc.baseminerals.init.Fluids; +import zone.moddev.mc.baseminerals.init.Items; +import zone.moddev.mc.baseminerals.init.Materials; +import com.mcmoddev.lib.events.*; + +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import net.minecraft.block.Block; +import net.minecraft.item.Item; + +@EventBusSubscriber +public class EventHandler { + @SubscribeEvent + public static void mmdlibRegisterMaterials(MMDLibRegisterMaterials ev) { + Materials.init(); + } + + @SubscribeEvent + public static void mmdlibRegisterBlocks(MMDLibRegisterBlocks ev) { + Blocks.init(); + } + + @SubscribeEvent + public static void mmdlibRegisterItems(MMDLibRegisterItems ev) { + Items.init(); + } + + @SubscribeEvent + public static void mmdlibRegisterFluids(MMDLibRegisterFluids ev) { + Fluids.init(); + } + + @SubscribeEvent + public static void onRemapBlock(RegistryEvent.MissingMappings event) { + for (final RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + if (mapping.key.getNamespace().equals(BaseMinerals.MODID)) { + // dummy + } + } + } + + @SubscribeEvent + public static void onRemapItem(RegistryEvent.MissingMappings event) { + for (final RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + if (mapping.key.getNamespace().equals(BaseMinerals.MODID)) { + // dummy + } + } + } + +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/worldgen/BaseMineralsWorldgenProvider.java b/src/main/java/zone/moddev/mc/baseminerals/worldgen/BaseMineralsWorldgenProvider.java new file mode 100644 index 0000000..af72066 --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/worldgen/BaseMineralsWorldgenProvider.java @@ -0,0 +1,71 @@ +package zone.moddev.mc.baseminerals.worldgen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.Consumer; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import zone.moddev.mc.orespawn.api.*; + +/** Declarations only: OreSpawn resolves, bakes and executes all placement. */ +public final class BaseMineralsWorldgenProvider { + private BaseMineralsWorldgenProvider() { } + private static final Ore[] ORES={ + new Ore("lithium",8,47,4,12,18,OrePattern.VEIN,OreHeightDistribution.TRIANGLE,8,4,3,8,GeologyFamily.IGNEOUS_INTRUSIVE), + new Ore("niter",16,63,5,8,16,OrePattern.CLUSTERS,OreHeightDistribution.UNIFORM,4,2,3,8,GeologyFamily.SEDIMENTARY), + new Ore("salt",24,79,2.5,16,32,OrePattern.CLUSTERS,OreHeightDistribution.UNIFORM,8,0,8,8,GeologyFamily.SEDIMENTARY), + new Ore("sulfur",4,31,3,12,18,OrePattern.CLUSTERS,OreHeightDistribution.BOTTOM_TRIANGLE,4,2,4,8,GeologyFamily.SEDIMENTARY,GeologyFamily.IGNEOUS_VOLCANIC), + new Ore("phosphorus",16,63,3,12,18,OrePattern.CLUSTERS,OreHeightDistribution.TRIANGLE,6,1,6,8,GeologyFamily.SEDIMENTARY), + new Ore("potash",8,47,2.5,12,24,OrePattern.CLUSTERS,OreHeightDistribution.UNIFORM,6,0,6,8,GeologyFamily.SEDIMENTARY), + new Ore("saltpeter",24,79,3.75,8,16,OrePattern.CLUSTERS,OreHeightDistribution.UNIFORM,4,1,3,8,GeologyFamily.SEDIMENTARY) + }; + public static void enqueue() { + build(ForgeRegistries.BLOCKS::containsKey).ifPresent(provider->{ + if(!OreSpawnApi.enqueue(provider)) throw new IllegalStateException("Base Minerals provider registered twice"); + }); + } + public static Optional build(Predicate available) { + List ores=new ArrayList<>(); + for(Ore rule:ORES) { + ResourceLocation block=id("baseminerals",rule.name+"_ore"); + if(!available.test(block)) continue; + WorldgenProvider.OreDefinition.Builder ore=WorldgenProvider.OreDefinition.builder(id("baseminerals","legacy/"+rule.name+"_ore"),block) + .material(id("orespawn",rule.name)).nativeGeneration(false).retrogen(false).output(block,100D); + Consumer placement=p->{ + p.yRange(rule.minY,rule.maxY).attempts(rule.frequency).quantityRange(rule.minQuantity,rule.maxQuantity) + .pattern(rule.pattern).heightDistribution(rule.height).placementChannel(id("orespawn","standard")) + .spread(rule.horizontal,rule.vertical).nodeSize(rule.node).hostBlock(id("minecraft","stone")); + // API major 1 exposes vein length through the standard pattern codec. + if(rule.pattern==OrePattern.VEIN) { + com.google.gson.JsonObject settings=new com.google.gson.JsonObject(); + settings.addProperty("length",rule.length); + settings.addProperty("node_size",rule.node); + settings.addProperty("spread",rule.horizontal); + settings.addProperty("vertical_spread",rule.vertical); + p.pattern(id("orespawn","vein"),settings); + } + for(GeologyFamily family:rule.families) p.hostFamily(family); + }; + // Exact-domain siblings participate in the same Overworld source group. + // The explicit rule overrides this fallback, so placement still runs once. + ore.dimension(id("minecraft","overworld"),placement); + ore.dimensionSelector(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END,placement); + ores.add(ore.build()); + } + if(ores.isEmpty())return Optional.empty(); + WorldgenProvider.Builder provider=WorldgenProvider.builder("baseminerals",1); + ores.forEach(provider::ore); + return Optional.of(provider.build()); + } + private static ResourceLocation id(String mod,String name){return new ResourceLocation(mod,name);} + private static final class Ore { + final String name; final int minY,maxY,minQuantity,maxQuantity,horizontal,vertical,node,length; + final double frequency; final OrePattern pattern; final OreHeightDistribution height; final GeologyFamily[] families; + Ore(String name,int minY,int maxY,double frequency,int minQuantity,int maxQuantity,OrePattern pattern,OreHeightDistribution height,int horizontal,int vertical,int node,int length,GeologyFamily...families) { + this.name=name;this.minY=minY;this.maxY=maxY;this.frequency=frequency;this.minQuantity=minQuantity;this.maxQuantity=maxQuantity; + this.pattern=pattern;this.height=height;this.horizontal=horizontal;this.vertical=vertical;this.node=node;this.length=length;this.families=families; + } + } +} diff --git a/src/main/resources/META-INF/baseminerals-legacy-api.list b/src/main/resources/META-INF/baseminerals-legacy-api.list new file mode 100644 index 0000000..d642ac0 --- /dev/null +++ b/src/main/resources/META-INF/baseminerals-legacy-api.list @@ -0,0 +1,20 @@ +BaseMinerals +data/MaterialNames +init/Blocks +init/Fluids +init/ItemGroups +init/Items +init/Materials +init/Recipes +init/VillagerTrades +integration/plugins/BMiDenseOres +integration/plugins/BMiEnderIO +integration/plugins/BMiMekanism +integration/plugins/BMiThaumcraft +integration/plugins/BMiThermalExpansion +integration/plugins/BMiTinkersConstruct +proxy/ClientProxy +proxy/CommonProxy +proxy/ServerProxy +util/BMiConfig +util/EventHandler diff --git a/src/main/resources/assets/baseminerals/lang/de_DE.lang b/src/main/resources/assets/baseminerals/lang/de_DE.lang index efd3de5..77ce05a 100644 --- a/src/main/resources/assets/baseminerals/lang/de_DE.lang +++ b/src/main/resources/assets/baseminerals/lang/de_DE.lang @@ -123,4 +123,5 @@ fluid.baseminerals.lithium=Geschmolzenes Lithium fluid.baseminerals.silicon=Geschmolzenes Silizium tile.baseminerals.lithium.name=Geschmolzenes Lithium -tile.baseminerals.lithium.name=Geschmolzenes Silizium \ No newline at end of file +tile.baseminerals.lithium.name=Geschmolzenes Siliziumconfig.baseminerals.fluid_lithium=Flüssiges Lithium +config.baseminerals.fluid_silicon=Flüssiges Silizium diff --git a/src/main/resources/assets/baseminerals/lang/en_US.lang b/src/main/resources/assets/baseminerals/lang/en_US.lang index 8fed9c9..5f599ed 100644 --- a/src/main/resources/assets/baseminerals/lang/en_US.lang +++ b/src/main/resources/assets/baseminerals/lang/en_US.lang @@ -125,3 +125,17 @@ fluid.baseminerals.silicon=Molten Silicon tile.baseminerals.lithium.name=Molten Lithium tile.baseminerals.lithium.name=Molten Silicon + +config.baseminerals.title=Base Minerals +config.baseminerals.description=Material switches take effect after restarting Minecraft. +config.baseminerals.enable_charcoal=Enable Charcoal +config.baseminerals.enable_lithium=Enable Lithium +config.baseminerals.enable_niter=Enable Niter +config.baseminerals.enable_phosphorus=Enable Phosphorus +config.baseminerals.enable_potash=Enable Potash +config.baseminerals.enable_salt=Enable Salt +config.baseminerals.enable_saltpeter=Enable Saltpeter +config.baseminerals.enable_silicon=Enable Silicon +config.baseminerals.enable_sulfur=Enable Sulfur +config.baseminerals.fluid_lithium=Lithium fluid +config.baseminerals.fluid_silicon=Silicon fluid diff --git a/src/main/resources/assets/baseminerals/models/block/lithium_ore.json b/src/main/resources/assets/baseminerals/models/block/lithium_ore.json deleted file mode 100644 index acdbf7f..0000000 --- a/src/main/resources/assets/baseminerals/models/block/lithium_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/lithium_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/niter_ore.json b/src/main/resources/assets/baseminerals/models/block/niter_ore.json deleted file mode 100644 index 5699b39..0000000 --- a/src/main/resources/assets/baseminerals/models/block/niter_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/niter_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/phosphorus_ore.json b/src/main/resources/assets/baseminerals/models/block/phosphorus_ore.json deleted file mode 100644 index 485fa4f..0000000 --- a/src/main/resources/assets/baseminerals/models/block/phosphorus_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/phosphorus_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/potash_ore.json b/src/main/resources/assets/baseminerals/models/block/potash_ore.json deleted file mode 100644 index 6e00183..0000000 --- a/src/main/resources/assets/baseminerals/models/block/potash_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/potash_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/salt_ore.json b/src/main/resources/assets/baseminerals/models/block/salt_ore.json deleted file mode 100644 index 046de71..0000000 --- a/src/main/resources/assets/baseminerals/models/block/salt_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/salt_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/saltpeter_ore.json b/src/main/resources/assets/baseminerals/models/block/saltpeter_ore.json deleted file mode 100644 index 2669156..0000000 --- a/src/main/resources/assets/baseminerals/models/block/saltpeter_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/saltpeter_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/sulfur_ore.json b/src/main/resources/assets/baseminerals/models/block/sulfur_ore.json deleted file mode 100644 index f342e04..0000000 --- a/src/main/resources/assets/baseminerals/models/block/sulfur_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/sulfur_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/recipes/_factories.json b/src/main/resources/assets/baseminerals/recipes/_factories.json new file mode 100644 index 0000000..f5cc434 --- /dev/null +++ b/src/main/resources/assets/baseminerals/recipes/_factories.json @@ -0,0 +1,5 @@ +{ + "conditions": { + "material_enabled": "zone.moddev.mc.baseminerals.init.MaterialEnabledCondition" + } +} diff --git a/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_potash.json b/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_potash.json index bcf0b3f..b6b5c5f 100644 --- a/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_potash.json +++ b/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_potash.json @@ -1,23 +1,37 @@ { - "type":"forge:ore_shapeless", - "ingredients": [ - { - "type":"forge:ore_dict", - "ore":"dustCharcoal" + "type": "forge:ore_shapeless", + "ingredients": [ + { + "type": "forge:ore_dict", + "ore": "dustCharcoal" + }, + { + "type": "forge:ore_dict", + "ore": "dustSulfur" + }, + { + "type": "forge:ore_dict", + "ore": "dustPotash" + } + ], + "result": { + "type": "minecraft:item", + "item": "minecraft:gunpowder", + "count": 3 }, - { - "type":"forge:ore_dict", - "ore":"dustSulfur" - }, - { - "type":"forge:ore_dict", - "ore":"dustPotash" - } - ], - "result": { - "type":"minecraft:item", - "item":"minecraft:gunpowder", - "count": 3 - }, - "group": "baseminerals_extra" + "group": "baseminerals_extra", + "conditions": [ + { + "type": "baseminerals:material_enabled", + "material": "sulfur" + }, + { + "type": "baseminerals:material_enabled", + "material": "potash" + }, + { + "type": "baseminerals:material_enabled", + "material": "charcoal" + } + ] } diff --git a/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_saltpeter.json b/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_saltpeter.json index f58ae44..937f026 100644 --- a/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_saltpeter.json +++ b/src/main/resources/assets/baseminerals/recipes/gunpowder_charcoal_saltpeter.json @@ -1,23 +1,37 @@ { - "type":"forge:ore_shapeless", - "ingredients": [ - { - "type":"forge:ore_dict", - "ore":"dustCharcoal" + "type": "forge:ore_shapeless", + "ingredients": [ + { + "type": "forge:ore_dict", + "ore": "dustCharcoal" + }, + { + "type": "forge:ore_dict", + "ore": "dustSulfur" + }, + { + "type": "forge:ore_dict", + "ore": "dustSaltpeter" + } + ], + "result": { + "type": "minecraft:item", + "item": "minecraft:gunpowder", + "count": 3 }, - { - "type":"forge:ore_dict", - "ore":"dustSulfur" - }, - { - "type":"forge:ore_dict", - "ore":"dustSaltpeter" - } - ], - "result": { - "type":"minecraft:item", - "item":"minecraft:gunpowder", - "count": 3 - }, - "group": "baseminerals_extra" + "group": "baseminerals_extra", + "conditions": [ + { + "type": "baseminerals:material_enabled", + "material": "sulfur" + }, + { + "type": "baseminerals:material_enabled", + "material": "saltpeter" + }, + { + "type": "baseminerals:material_enabled", + "material": "charcoal" + } + ] } diff --git a/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_potash.json b/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_potash.json index 41cab01..f0988e1 100644 --- a/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_potash.json +++ b/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_potash.json @@ -1,23 +1,33 @@ { - "type":"forge:ore_shapeless", - "ingredients": [ - { - "type":"forge:ore_dict", - "ore":"dustCoal" + "type": "forge:ore_shapeless", + "ingredients": [ + { + "type": "forge:ore_dict", + "ore": "dustCoal" + }, + { + "type": "forge:ore_dict", + "ore": "dustSulfur" + }, + { + "type": "forge:ore_dict", + "ore": "dustPotash" + } + ], + "result": { + "type": "minecraft:item", + "item": "minecraft:gunpowder", + "count": 3 }, - { - "type":"forge:ore_dict", - "ore":"dustSulfur" - }, - { - "type":"forge:ore_dict", - "ore":"dustPotash" - } - ], - "result": { - "type":"minecraft:item", - "item":"minecraft:gunpowder", - "count": 3 - }, - "group": "baseminerals_extra" + "group": "baseminerals_extra", + "conditions": [ + { + "type": "baseminerals:material_enabled", + "material": "sulfur" + }, + { + "type": "baseminerals:material_enabled", + "material": "potash" + } + ] } diff --git a/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_saltpeter.json b/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_saltpeter.json index 34da789..e719fe1 100644 --- a/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_saltpeter.json +++ b/src/main/resources/assets/baseminerals/recipes/gunpowder_coal_saltpeter.json @@ -1,23 +1,33 @@ { - "type":"forge:ore_shapeless", - "ingredients": [ - { - "type":"forge:ore_dict", - "ore":"dustCoal" + "type": "forge:ore_shapeless", + "ingredients": [ + { + "type": "forge:ore_dict", + "ore": "dustCoal" + }, + { + "type": "forge:ore_dict", + "ore": "dustSulfur" + }, + { + "type": "forge:ore_dict", + "ore": "dustSaltpeter" + } + ], + "result": { + "type": "minecraft:item", + "item": "minecraft:gunpowder", + "count": 3 }, - { - "type":"forge:ore_dict", - "ore":"dustSulfur" - }, - { - "type":"forge:ore_dict", - "ore":"dustSaltpeter" - } - ], - "result": { - "type":"minecraft:item", - "item":"minecraft:gunpowder", - "count": 3 - }, - "group": "baseminerals_extra" + "group": "baseminerals_extra", + "conditions": [ + { + "type": "baseminerals:material_enabled", + "material": "sulfur" + }, + { + "type": "baseminerals:material_enabled", + "material": "saltpeter" + } + ] } diff --git a/src/main/resources/assets/baseminerals/recipes/silicon_blend_sand_charcoal.json b/src/main/resources/assets/baseminerals/recipes/silicon_blend_sand_charcoal.json index 2644cec..3b5296d 100644 --- a/src/main/resources/assets/baseminerals/recipes/silicon_blend_sand_charcoal.json +++ b/src/main/resources/assets/baseminerals/recipes/silicon_blend_sand_charcoal.json @@ -30,6 +30,10 @@ "optionValue": "Basics" } ] + }, + { + "type": "baseminerals:material_enabled", + "material": "charcoal" } ], "group": "baseminerals_extra" diff --git a/src/main/resources/assets/baseminerals/recipes/silicon_charcoal.json b/src/main/resources/assets/baseminerals/recipes/silicon_charcoal.json index 5b5872c..2333cea 100644 --- a/src/main/resources/assets/baseminerals/recipes/silicon_charcoal.json +++ b/src/main/resources/assets/baseminerals/recipes/silicon_charcoal.json @@ -29,6 +29,10 @@ "optionValue": "Basics" } ] + }, + { + "type": "baseminerals:material_enabled", + "material": "charcoal" } ], "group": "baseminerals_extra" diff --git a/src/main/resources/assets/baseminerals/textures/blocks/lithium_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/lithium_ore.png deleted file mode 100644 index 4289b79..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/lithium_ore.png and /dev/null differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/niter_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/niter_ore.png deleted file mode 100644 index 61e1286..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/niter_ore.png and /dev/null differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/lithium_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/lithium_ore.png new file mode 100644 index 0000000..09ca742 Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/lithium_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/niter_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/niter_ore.png new file mode 100644 index 0000000..192119d Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/niter_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/phosphorus_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/phosphorus_ore.png new file mode 100644 index 0000000..4cae97c Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/phosphorus_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/potash_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/potash_ore.png new file mode 100644 index 0000000..46b144e Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/potash_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/salt_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/salt_ore.png new file mode 100644 index 0000000..cb27893 Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/salt_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/saltpeter_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/saltpeter_ore.png new file mode 100644 index 0000000..166b391 Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/saltpeter_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/sulfur_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/sulfur_ore.png new file mode 100644 index 0000000..b59699b Binary files /dev/null and b/src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/sulfur_ore.png differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/phosphorus_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/phosphorus_ore.png deleted file mode 100644 index e72a8e2..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/phosphorus_ore.png and /dev/null differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/potash_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/potash_ore.png deleted file mode 100644 index 8f9e70b..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/potash_ore.png and /dev/null differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/salt_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/salt_ore.png deleted file mode 100644 index f5171d5..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/salt_ore.png and /dev/null differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/saltpeter_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/saltpeter_ore.png deleted file mode 100644 index 294214c..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/saltpeter_ore.png and /dev/null differ diff --git a/src/main/resources/assets/baseminerals/textures/blocks/sulfur_ore.png b/src/main/resources/assets/baseminerals/textures/blocks/sulfur_ore.png deleted file mode 100644 index f199e65..0000000 Binary files a/src/main/resources/assets/baseminerals/textures/blocks/sulfur_ore.png and /dev/null differ diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 1cbd107..10939fa 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack":{ - "pack_format":2, + "pack_format":3, "description":"Base Minerals" } -} \ No newline at end of file +} diff --git a/src/test/java/zone/moddev/mc/baseminerals/NativeProviderTests.java b/src/test/java/zone/moddev/mc/baseminerals/NativeProviderTests.java new file mode 100644 index 0000000..7713de2 --- /dev/null +++ b/src/test/java/zone/moddev/mc/baseminerals/NativeProviderTests.java @@ -0,0 +1,63 @@ +package zone.moddev.mc.baseminerals; + +import static org.junit.jupiter.api.Assertions.*; +import com.google.gson.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.Optional; +import java.util.function.Predicate; +import net.minecraft.util.ResourceLocation; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.orespawn.api.WorldgenProvider; + +class NativeProviderTests { + @SuppressWarnings("unchecked") + private Optional build(Predicate available) throws Exception { + return (Optional) Class.forName("zone.moddev.mc.baseminerals.worldgen.BaseMineralsWorldgenProvider") + .getMethod("build", Predicate.class).invoke(null, available); + } + @Test void implementsApprovedBalanceAndExportsSchema() throws Exception { + JsonObject provider = build(id -> true).get().toJson(); + Files.createDirectories(Paths.get("build/reports")); + Files.write(Paths.get("build/reports/native-provider.json"), (provider.toString()+"\n").getBytes(StandardCharsets.UTF_8)); + assertEquals(5,provider.get("schema_version").getAsInt()); + JsonObject ores=provider.getAsJsonObject("ores"); + assertEquals(7,ores.entrySet().size()); + String[] names={"lithium","niter","salt","sulfur","phosphorus","potash","saltpeter"}; + double[][] rules={{8,47,4,12,18},{16,63,5,8,16},{24,79,2.5,16,32},{4,31,3,12,18},{16,63,3,12,18},{8,47,2.5,12,24},{24,79,3.75,8,16}}; + int[][] shapes={{8,4,3},{4,2,3},{8,0,8},{4,2,4},{6,1,6},{6,0,6},{4,1,3}}; + String[] heights={"triangle","uniform","uniform","bottom_triangle","triangle","uniform","uniform"}; + for(int i=0;i false).isPresent()); + JsonObject ores=build(id -> !id.getPath().equals("lithium_ore")).get().toJson().getAsJsonObject("ores"); + assertEquals(6,ores.entrySet().size()); + assertFalse(ores.has("baseminerals:legacy/lithium_ore")); + } +} diff --git a/src/test/java/zone/moddev/mc/baseminerals/compat/LegacyNamespaceTransformerTests.java b/src/test/java/zone/moddev/mc/baseminerals/compat/LegacyNamespaceTransformerTests.java new file mode 100644 index 0000000..473938e --- /dev/null +++ b/src/test/java/zone/moddev/mc/baseminerals/compat/LegacyNamespaceTransformerTests.java @@ -0,0 +1,87 @@ +package zone.moddev.mc.baseminerals.compat; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodNode; + +class LegacyNamespaceTransformerTests { + + @Test + void linksOldTypesFieldsInheritanceAndReflectionToCurrentClasses() { + final byte[] output = new LegacyNamespaceTransformer().transform("example.Consumer", null, fixture()); + final ClassNode type = new ClassNode(); + new ClassReader(output).accept(type, 0); + assertEquals("example/Consumer", type.name); + assertEquals("zone/moddev/mc/baseminerals/proxy/CommonProxy", type.superName); + assertEquals("Lzone/moddev/mc/baseminerals/BaseMinerals;", ((FieldNode) type.fields.get(0)).desc); + + final MethodNode method = (MethodNode) type.methods.get(0); + assertEquals("zone/moddev/mc/baseminerals/BaseMinerals", ((FieldInsnNode) method.instructions.get(0)).owner); + assertEquals("zone.moddev.mc.baseminerals.init.Materials", ((LdcInsnNode) method.instructions.get(2)).cst); + assertEquals(Type.getObjectType("zone/moddev/mc/baseminerals/util/BMiConfig$Options"), + ((LdcInsnNode) method.instructions.get(4)).cst); + assertEquals("other.mod/textures/ore.png", ((LdcInsnNode) method.instructions.get(6)).cst); + } + + @Test + void doesNotRelocateAddonOwnedClassesInsideTheHistoricalPackage() { + final ClassWriter source = new ClassWriter(0); + source.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/mcmoddev/baseminerals/addon/Extra", null, + "com/mcmoddev/baseminerals/proxy/CommonProxy", null); + source.visitEnd(); + + final ClassNode type = new ClassNode(); + new ClassReader(new LegacyNamespaceTransformer().transform("com.mcmoddev.baseminerals.addon.Extra", null, + source.toByteArray())).accept(type, 0); + assertEquals("com/mcmoddev/baseminerals/addon/Extra", type.name); + assertEquals("zone/moddev/mc/baseminerals/proxy/CommonProxy", type.superName); + } + + @Test + void leavesCanonicalClassesAndTheCyanoBridgeAlone() { + final LegacyNamespaceTransformer transformer = new LegacyNamespaceTransformer(); + final byte[] bytes = fixture(); + assertSame(bytes, transformer.transform("zone.moddev.mc.baseminerals.BaseMinerals", null, bytes)); + assertNull(transformer.transform("missing", "missing", null)); + + final ClassWriter source = new ClassWriter(0); + source.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "example/Golem", null, "java/lang/Object", null); + source.visitField(Opcodes.ACC_PUBLIC, "old", "Lcyano/baseminerals/init/Blocks;", null, null).visitEnd(); + source.visitEnd(); + final byte[] golem = source.toByteArray(); + assertSame(golem, transformer.transform("example.Golem", null, golem)); + } + + private static byte[] fixture() { + final ClassWriter type = new ClassWriter(0); + type.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "example/Consumer", null, + "com/mcmoddev/baseminerals/proxy/CommonProxy", null); + type.visitField(Opcodes.ACC_PUBLIC, "mod", "Lcom/mcmoddev/baseminerals/BaseMinerals;", null, null).visitEnd(); + + final MethodVisitor method = type.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "link", "()V", null, null); + method.visitCode(); + method.visitFieldInsn(Opcodes.GETSTATIC, "com/mcmoddev/baseminerals/BaseMinerals", "instance", "Lcom/mcmoddev/baseminerals/BaseMinerals;"); + method.visitInsn(Opcodes.POP); + method.visitLdcInsn("com.mcmoddev.baseminerals.init.Materials"); + method.visitInsn(Opcodes.POP); + method.visitLdcInsn(Type.getObjectType("com/mcmoddev/baseminerals/util/BMiConfig$Options")); + method.visitInsn(Opcodes.POP); + method.visitLdcInsn("other.mod/textures/ore.png"); + method.visitInsn(Opcodes.POP); + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(1, 0); + method.visitEnd(); + type.visitEnd(); + return type.toByteArray(); + } +} diff --git a/src/test/java/zone/moddev/mc/baseminerals/util/MaterialConfigurationTests.java b/src/test/java/zone/moddev/mc/baseminerals/util/MaterialConfigurationTests.java new file mode 100644 index 0000000..81bdb66 --- /dev/null +++ b/src/test/java/zone/moddev/mc/baseminerals/util/MaterialConfigurationTests.java @@ -0,0 +1,54 @@ +package zone.moddev.mc.baseminerals.util; + +import static org.junit.jupiter.api.Assertions.*; +import java.nio.file.Path; +import net.minecraftforge.common.config.Configuration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; + +class MaterialConfigurationTests { + @BeforeEach void freshSharedStartupState() throws Exception { + for(String key:new String[]{"materialEnabled","fluidEnabled"}) { + java.lang.reflect.Field field=com.mcmoddev.lib.util.Config.Options.class.getDeclaredField(key); + field.setAccessible(true);((java.util.Map)field.get(null)).clear(); + } + } + @Test void initializesMaterialAndFluidSwitchesTogether() { + Configuration config=new Configuration(); + BMiConfig.load(config); + for(String name:new String[]{"lithium","silicon"}) { + assertTrue(BMiConfig.isEnabled(name)); + assertTrue(com.mcmoddev.lib.util.Config.Options.isFluidEnabled(name)); + } + assertTrue(config.getCategory(BMiConfig.MATERIALS).get("EnableLithium").requiresMcRestart()); + } + @Test void disabledMaterialAlsoDisablesItsFluidAtStartup() { + Configuration config=new Configuration(); + config.get("Metals","EnableLithium",true).set(false); + BMiConfig.load(config); + assertFalse(BMiConfig.isEnabled("lithium")); + assertFalse(com.mcmoddev.lib.util.Config.Options.isFluidEnabled("lithium")); + } + @Test void charcoalSwitchDoesNotDisableBaseMetalsSharedMaterial() { + com.mcmoddev.lib.util.Config.Options.materialEnabled("charcoal",true); + Configuration config=new Configuration(); + config.get("Vanilla","EnableCharcoal",true).set(false); + BMiConfig.load(config); + assertFalse(BMiConfig.isEnabled("charcoal")); + assertTrue(com.mcmoddev.lib.util.Config.Options.isMaterialEnabled("charcoal")); + } + @Test void historicalFluidSwitchDoesNotDisableTheMaterial() { + Configuration config = new Configuration(); + config.get(BMiConfig.FLUIDS, "Enabled Lithium", true).set(false); + BMiConfig.load(config); + assertTrue(BMiConfig.isEnabled("lithium")); + assertFalse(com.mcmoddev.lib.util.Config.Options.isFluidEnabled("lithium")); + } + @Test void editingRequestedSwitchDoesNotChangeActiveRegistration() { + Configuration config=new Configuration(); + BMiConfig.load(config); + config.get("Metals","EnableSalt",true).set(false); + assertTrue(BMiConfig.isEnabled("salt")); + } +} diff --git a/tools/validation/legacy112/EclipseModelCheck.java b/tools/validation/legacy112/EclipseModelCheck.java new file mode 100644 index 0000000..ebc1885 --- /dev/null +++ b/tools/validation/legacy112/EclipseModelCheck.java @@ -0,0 +1,99 @@ +package review.ide; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.stream.Collectors; +import org.gradle.tooling.GradleConnector; +import org.gradle.tooling.ProjectConnection; +import org.gradle.tooling.model.build.BuildEnvironment; +import org.gradle.tooling.model.eclipse.EclipseProject; +import org.gradle.tooling.model.eclipse.EclipseSourceDirectory; +import org.gradle.tooling.model.gradle.GradleBuild; + +/** Requests the same project models used by Eclipse Buildship's import wizard. */ +public final class EclipseModelCheck { + + public static void main(final String[] args) throws Exception { + final File project = new File(args[0]); + final File cache = new File(args[1]); + final File javaHome = new File(args[2]); + final String[] modelArguments = args.length > 4 + ? new String[]{"--offline", "-Dorg.gradle.java.installations.paths=" + args[4], + "-Dorg.gradle.java.installations.auto-detect=false", "-Dorg.gradle.java.installations.auto-download=false"} + : new String[]{"--offline"}; + + try (ProjectConnection connection = GradleConnector.newConnector() + .forProjectDirectory(project).useGradleUserHomeDir(cache) + .useBuildDistribution().connect()) { + final BuildEnvironment environment = connection.model(BuildEnvironment.class) + .setJavaHome(javaHome).withArguments(modelArguments).get(); + final GradleBuild build = connection.model(GradleBuild.class) + .setJavaHome(javaHome).withArguments(modelArguments).get(); + final EclipseProject eclipse = connection.model(EclipseProject.class) + .setJavaHome(javaHome).withArguments(modelArguments).get(); + + if (!"9.6.1".equals(environment.getGradle().getGradleVersion()) + || !"BaseMinerals".equals(build.getRootProject().getName()) + || !"BaseMinerals".equals(eclipse.getName()) + || eclipse.getSourceDirectories().stream().noneMatch(source -> "build/resources/main".equals(source.getPath())) + || !"1.8".equals(eclipse.getJavaSourceSettings().getSourceLanguageLevel().toString())) { + throw new AssertionError("Wrong wrapper, project or Java source level in the Eclipse model"); + } + eclipse.getClasspath().forEach(dependency -> { + if (!dependency.getFile().isFile()) { + throw new AssertionError("Missing Eclipse library: " + dependency.getFile()); + } + }); + if (eclipse.getSourceDirectories().stream().map(source -> source.getPath()).distinct().count() + != eclipse.getSourceDirectories().size()) { + throw new AssertionError("Duplicate Eclipse source directories"); + } + + for (final EclipseSourceDirectory source : eclipse.getSourceDirectories()) { + verifySourcePackages(new File(project, source.getPath()).toPath()); + } + + if (args.length > 3) { + final List libraries = new ArrayList<>(); + eclipse.getClasspath().forEach(dependency -> libraries.add(dependency.getFile().getAbsolutePath())); + Files.write(new File(args[3]).toPath(), libraries, StandardCharsets.UTF_8); + } + + System.out.println("BASE_MINERALS_ECLIPSE_MODEL_PASS gradle=" + + environment.getGradle().getGradleVersion() + " source=1.8 libraries=" + + eclipse.getClasspath().size() + " gradleJava=" + + environment.getJava().getJavaHome()); + } + } + + private static void verifySourcePackages(final Path root) throws IOException { + if (!Files.isDirectory(root)) return; + + final Pattern packageDeclaration = Pattern.compile("(?m)^\\s*package\\s+([\\w.]+)\\s*;"); + final List sources; + try (Stream files = Files.walk(root)) { + sources = files.filter(path -> path.toString().endsWith(".java")) + .collect(Collectors.toList()); + } + + for (final Path file : sources) { + final Path parent = root.relativize(file).getParent(); + final String expected = parent == null ? "" : parent.toString().replace(File.separatorChar, '.'); + final String text = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + final Matcher declaration = packageDeclaration.matcher(text); + final String declared = declaration.find() ? declaration.group(1) : ""; + + if (!expected.equals(declared)) { + throw new AssertionError("Package does not match the Eclipse source folder: " + file); + } + } + } +} diff --git a/tools/validation/legacy112/README.md b/tools/validation/legacy112/README.md new file mode 100644 index 0000000..5db4a5f --- /dev/null +++ b/tools/validation/legacy112/README.md @@ -0,0 +1,67 @@ +# Base Minerals 1.12 qualification + +Probes are separate mods under `build/test-mods`. Exclude them from distribution +and ordinary Eclipse profiles. All game directories/worlds are disposable. +Suites refuse to reuse output directories and fingerprint copied fixtures. + +`test` checks the provider settings, missing blocks, startup material +and fluid switches, restart latching, charcoal ownership and namespace relinking. +`verifyProjectResources` validates JSON and the exported provider against schemas +inside the pinned OreSpawn jar, configuration labels and workflow pins. + +```text +python tools/validation/legacy112/run_packaged_suite.py --java --template --mirror --output +``` + +The template contains only Forge 1.12.2-14.23.5.2859, vanilla server and libraries. +The suite covers all Y levels across 256 interior chunks per dimension for four +fixed seeds, with a populated border. It checks Nether and End exclusion, repeat +and reload digests, later chunks, disabled materials and generation, native +overrides, a saved OS1 import, direct OS3 imports, saved profiles and stacks, and Tinkers molten +forms. It also checks negative hosts, records attempts, quantities and selected +heights, and runs three paired timing comparisons. +The later-chunk case also recenses the original area. Every individual disabled +material is tested through actual generation, rather than registration alone. +Seed zero needs explicit fresh metadata because server.properties treats `0` +as random. `--quick` is only an early smoke. + +The packaged suite uses a saved lithium rule extracted from an OreSpawn 4 +world on Minecraft 1.11.2. For the complete-world check, run +`reproduce_os1_reload.py` with `--source-world` pointing to an already imported +1.10.2 or 1.11.2 save, plus `--java`, `--template`, `--mirror` and a new +`--output`. The runner works on a copy and fingerprints its source. See +[known issues](../../../docs/KNOWN_ISSUES.md) for raw OS1 files and edited profiles. + +`run_overlap_suite.py` stages the public 1.12.2 Base Electric, Base Power and +Mineralogy jars pinned in `overlap_dependencies.json` and captures their source +policies on fresh and reopened worlds. Use `--sample --with-base-metals` to +census 256 chunks per dimension with the pinned Base Metals jar installed. +The runner preserves failures per integration and continues to collect the +other cases, then exits unsuccessfully if any public integration failed. + +`packagedSourcePolicyTest` runs eight cases with a clearly synthetic output-only +sibling. It checks Balanced, Single, Custom weights, Keep Original, physical +placement-budget equality and saved choices across removal/restoration. + +After the full suite, `run_continuity_checks.py` accepts `--suite` and +`--selector-fixture` in addition to `--java`, `--template`, `--output`. It checks +copied OS1/OS3 worlds, saved selector-only profiles, all six vanilla stone +metadata hosts, bedrock/air/water/obsidian exclusions and decoded pattern settings. +`run_mineralogy_policy.py` tests reviewed sulfur output choices on copied worlds +when the fixture supplies an OreSpawn-managed Mineralogy source. + +`run_packaged_client.py` exercises both configuration entries, save/cancel/restart, +baked models and integrated-world fresh/reload checks against the installed +obfuscated client. It verifies assets in a disposable directory, saves screenshots +and rejects texture errors. Supply `--java`, `--cache`, `--installer`, `--template`, +`--mirror`, `--output` and `--phase first|reload`. Add `--with-base-metals` to +the first launch to include the pinned Base Metals jar. + +Inspect complete logs as well as JSON reports. Forge's unsigned-distribution +warning, the pinned Base Metals fingerprint warning, offline Realms checks and normal +sound shutdown messages are distinct from loading/generation errors. Manual +visual acceptance, hosted CI and publication remain separate gates. +Tinkers' historical Base Metals registrations emit alternative-prefix warnings +for its own molten alloys. Missing-source tests deliberately exercise Forge's +missing-mod and mapping warnings. A Forge-only control reproduces the vanilla +Nether population warnings seen while the census probe loads chunks. diff --git a/tools/validation/legacy112/audit_resources.py b/tools/validation/legacy112/audit_resources.py new file mode 100644 index 0000000..7eba4df --- /dev/null +++ b/tools/validation/legacy112/audit_resources.py @@ -0,0 +1,67 @@ +"""Parse resources and validate exported native declarations against the pinned engine.""" +import argparse,json,re,zipfile +from pathlib import Path +from referencing import Registry,Resource +from jsonschema import Draft202012Validator +import yaml +from run_runtime_checks import ROOT,dependency + +def main(): + parser=argparse.ArgumentParser();parser.add_argument('--mirror',type=Path);args=parser.parse_args() + count=0 + resources=ROOT/'build/resources/main' + for path in resources.rglob('*'): + if path.is_file() and (path.suffix in ('.json','.mcmeta') or path.name=='mcmod.info'): + json.loads(path.read_text(encoding='utf-8-sig'));count+=1 + assert json.loads((resources/'pack.mcmeta').read_text())['pack']['pack_format']==3 + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + with zipfile.ZipFile(dependency(next(s for s in specs if s['id']=='orespawn'),args.mirror)) as jar: + schemas=[json.loads(jar.read(n)) for n in jar.namelist() if n.startswith('META-INF/orespawn/docs/schemas/') and n.endswith('.json')] + registry=Registry().with_resources((s['$id'],Resource.from_contents(s)) for s in schemas) + schema=next(s for s in schemas if s['$id'].endswith('/orespawn-provider.schema.json')) + provider=json.loads((ROOT/'build/reports/native-provider.json').read_text()) + Draft202012Validator(schema,registry=registry).validate(provider) + assert len(provider['ores'])==7 + assert all(not provider.get(section) for section in ['rocks','terrain_dimensions','geomes','fluid_deposits','biome_palettes']) + assets=resources/'assets/baseminerals' + source_textures=ROOT/'src/main/resources/assets/baseminerals/textures/blocks' + for ore in provider['ores'].values(): + name=ore['block'].split(':',1)[1] + state=json.loads((assets/'blockstates'/f'{name}.json').read_text()) + item=json.loads((assets/'models/item'/f'{name}.json').read_text()) + model=json.loads((assets/'models/block'/f'{name}.json').read_text()) + assert state['variants']['normal']['model']==f'baseminerals:{name}',name + assert item['parent']==f'baseminerals:block/{name}',name + textures=assets/'textures/blocks' + if (textures/'ore_overlays'/f'{name}.png').is_file(): + assert model['parent']=='baseminerals:block/ore_overlay',name + assert (assets/'models/block/ore_overlay.json').is_file(),name + assert model['textures']=={ + 'particle':'minecraft:blocks/stone','base':'minecraft:blocks/stone', + 'overlay':f'baseminerals:blocks/ore_overlays/{name}' + },name + if not (source_textures/f'{name}.png').is_file(): + assert not (textures/f'{name}.png').exists(),f'{name}: stale solid texture' + else: + assert (textures/f'{name}.png').is_file(),f'{name}: missing solid texture' + assert model['parent']=='block/cube_all',name + assert model['textures']=={'all':f'baseminerals:blocks/{name}'},name + assert {'en_us.lang','de_de.lang'} <= {p.name for p in (assets/'lang').glob('*.lang')} + language=dict(line.split('=',1) for line in (assets/'lang/en_us.lang').read_text(encoding='utf-8').splitlines() if '=' in line and not line.startswith('#')) + for name in ['charcoal','lithium','niter','phosphorus','potash','salt','saltpeter','silicon','sulfur']: + assert 'config.baseminerals.enable_'+name in language,name + for name in ['lithium','silicon']: + assert 'config.baseminerals.fluid_'+name in language,name + for path in (ROOT/'.github/workflows').glob('*.yml'): + workflow=yaml.load(path.read_text(),Loader=yaml.BaseLoader);assert workflow.get('on') and workflow.get('jobs'),path + for job in workflow['jobs'].values(): + for step in job.get('steps',[]): + if 'uses' in step:assert re.fullmatch(r'[^@]+@[0-9a-f]{40}',step['uses']),step['uses'] + release=(ROOT/'.github/workflows/deploy-release.yml').read_text() + assert all(value in release for value in [ + 'confirm_live_publication','270972','112021) minecraft_version=1.12.2; branch=master-1.12', + '261744(required)','240967(optional)','240967(required)','245586(required)' + ]) + print(f'BASE_MINERALS_RESOURCES_PASS json={count} provider_schema=5 workflows=5') + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/audit_runtime_logs.py b/tools/validation/legacy112/audit_runtime_logs.py new file mode 100644 index 0000000..a097ed2 --- /dev/null +++ b/tools/validation/legacy112/audit_runtime_logs.py @@ -0,0 +1,43 @@ +"""Audit full successful-run logs, with narrow, documented legacy exceptions.""" +import argparse,collections,json,re +from pathlib import Path +from run_runtime_checks import write_json,digest + +def category(line,path): + if 'FML appears to be missing any signature data.' in line:return 'unsigned Forge distribution' + if 'Invalid fingerprint detected!' in line and any('['+m+']' in line for m in ('basemetals','mineralogy')):return 'pinned dependency fingerprint' + if any(t in line for t in ('SERVER IS RUNNING IN OFFLINE/INSECURE MODE','server will make no attempt to authenticate','While this makes the game possible','To change this, set "online-mode"')):return 'isolated offline server' + if '[Client thread/WARN]: Author: Paul Lamb' in line:return 'legacy sound shutdown' + if 'client-' in str(path) and "Can't keep up!" in line:return 'automated client startup tick delay' + if re.search(r'\[FML\]: Missing English translation for (?:minecraft|mcp): minecraft\.jar ',line):return 'legacy Forge server resource lookup' + if '[FML]: Missing English translation for FML: assets/fml/lang/en_us.lang' in line:return 'legacy Forge server resource lookup' + if re.search(r'\[FML\]: Missing English translation for (basemineralsprobe|basemineralsclientprobe): assets/\1/lang/en_us\.lang$',line):return 'test fixture without language resources' + if ('[FML]: Missing English translation for electricadvantage: assets/electricadvantage/lang/en_us.lang' in line + and (path.parent/'mods/synthetic-source-policy-fixture-packaged.jar').is_file()):return 'test fixture without language resources' + if ('[FML]: Missing English translation for mantle: assets/mantle/lang/en_us.lang' in line + and (path.parent/'mods/mantle-74924-2413993.jar').is_file()):return 'pinned Mantle jar without language resources' + # The same census with a Forge-only control reproduces vanilla Nether population warnings. + if re.search(r'\[FML\]: Base Minerals checks loaded a new chunk \(-?\d+, -?\d+ Dimension: -1\) during chunk population',line):return 'probe-driven vanilla Nether population' + if 'tinkers' in str(path) and ('[FML]: ***' in line or '[FML]: * at ' in line or ('Dangerous alternative prefix `tconstruct`' in line and 'expected `basemetals`' in line)):return 'Base Metals Tinkers registration' + if 'missing-source' in str(path) and any(t in line for t in ('Ignoring invalid OreSpawn-managed ore \'electricadvantage:', 'saved with mod electricadvantage', 'unidentified mappings', 'Unidentified block: electricadvantage:', 'Unidentified item: electricadvantage:', 'mappings that may cause world breakage')):return 'intentional missing-source fixture' + return None + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--directories',type=Path,nargs='+',required=True);parser.add_argument('--output',type=Path,required=True) + args=parser.parse_args();counts=collections.Counter();unknown=[];files={} + for directory in args.directories: + for path in directory.rglob('*-console.log'): + text=path.read_text(errors='replace');files[str(path)]={'sha256':digest(path),'bytes':path.stat().st_size} + for line in text.splitlines(): + if re.search(r'\[(?:[^\]]*/)?(?:WARN|ERROR|FATAL)\]',line): + kind=category(line,path) + if kind:counts[kind]+=1 + else:unknown.append({'path':str(path),'line':line}) + for marker in ('BASE_MINERALS_RUNTIME_FAIL','Exception in server tick loop','Exception caught during firing event','java.lang.NullPointerException','[TEXTURE ERRORS]'): + if marker in text:unknown.append({'path':str(path),'fatal_marker':marker}) + result={'passed':not unknown,'logs':files,'documented_messages':dict(counts),'unclassified':unknown};write_json(args.output,result) + assert not unknown,('Unclassified runtime diagnostics',unknown[:10]) + print('BASE_MINERALS_FULL_LOG_AUDIT_PASS',len(files),'logs',flush=True) + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/dependency_requirements.py b/tools/validation/legacy112/dependency_requirements.py new file mode 100644 index 0000000..442242f --- /dev/null +++ b/tools/validation/legacy112/dependency_requirements.py @@ -0,0 +1,53 @@ +"""Reject missing/old public dependencies before any disposable world is loaded.""" +import hashlib +import json +import subprocess +import urllib.request +import argparse +from pathlib import Path +from run_runtime_checks import ROOT, FORGE, dependency, prepare, write_json + +OLD = { + 'mmdlib':('mmdlib-261744','2699843','c60917d368629976afe439aa15dce15b46345c1e473f0b04e901db2a47cadf37'), + 'os':('mmd-orespawn-245586','9343209','5b5929ccdeb055679fd6d7b9629bf94f06085a6ecd791fd08d0500f666c079dd') +} + +def verify(java,template,output,mmdlib,orespawn,minerals): + fixtures=output/'dependency-fixtures';fixtures.mkdir() + old={} + for key,(artifact,version,expected) in OLD.items(): + target=fixtures/f'{artifact}-{version}.jar' + request=urllib.request.Request(f'https://www.cursemaven.com/curse/maven/{artifact}/{version}/{target.name}',headers={'User-Agent':'Mozilla/5.0'}) + with urllib.request.urlopen(request,timeout=45) as response:target.write_bytes(response.read()) + assert hashlib.sha256(target.read_bytes()).hexdigest()==expected,'Public fixture checksum mismatch' + old[key]=target + scenarios={ + 'missing-mmdlib':([orespawn,minerals],'mmdlib','1.0.0-rc2'), + 'old-mmdlib':([old['mmdlib'],orespawn,minerals],'mmdlib','1.0.0-rc2'), + 'missing-orespawn':([mmdlib,minerals],'orespawn','4.1.6.112021'), + 'old-orespawn':([mmdlib,old['os'],minerals],'orespawn','4.1.6.112021') + } + result={} + for name,(jars,modid,version) in scenarios.items(): + target=output/name;prepare(target,template,jars) + log=target/'rejection-console.log' + with log.open('wb') as stream: + subprocess.run([str(java),'-Xmx1G','-jar',FORGE,'nogui'],cwd=target,stdout=stream,stderr=subprocess.STDOUT,timeout=90) + text=log.read_text(errors='replace') + rejected = ('MissingModsException' in text or 'Missing mandatory dependencies' in text + or 'MultipleModsErrored' in text) + assert rejected and f'{modid}@[{version},' in text,log + assert not (target/'world/level.dat').exists(),'Incompatible dependencies reached world loading' + result[name]={'rejected_before_world':True,'required_mod':modid,'minimum':version} + print(name,'PASS',flush=True) + write_json(output/'dependency-requirements.json',result) + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + for name in ('java','template','output'):parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--mirror',type=Path);args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=False) + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + selected={s['id']:dependency(s,args.mirror) for s in specs if s['id'] in ('mmdlib','orespawn')} + verify(args.java,args.template,args.output,selected['mmdlib'],selected['orespawn'],ROOT/'build/libs/BaseMinerals-2.5.1.112021.jar') + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/fixtures/orespawn1-imported-111-profile.json b/tools/validation/legacy112/fixtures/orespawn1-imported-111-profile.json new file mode 100644 index 0000000..92bc1d4 --- /dev/null +++ b/tools/validation/legacy112/fixtures/orespawn1-imported-111-profile.json @@ -0,0 +1,49 @@ +{ + "schema_version": 7, + "ores": { + "baseminerals:legacy/baseminerals_lithium_ore_0": { + "enabled": true, + "block": "baseminerals:lithium_ore", + "source_mod": "baseminerals", + "native_generation": false, + "retrogen": false, + "legacy_import": "os1", + "outputs": [ + { + "block": "baseminerals:lithium_ore", + "weight": 100 + } + ], + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "min_y": 8, + "max_y": 39, + "frequency": 1.5, + "quantity": 4, + "pattern": "orespawn:default", + "height_distribution": "uniform", + "spread": 8, + "vertical_spread": 0, + "node_size": 4, + "length": 16, + "fluid": "minecraft:water", + "host_blocks": [ + "minecraft:stone", + "minecraft:netherrack", + "minecraft:end_stone" + ], + "host_tags": [], + "host_families": [], + "biome_ids": [], + "excluded_biome_ids": [], + "biome_dictionary": [], + "excluded_biome_dictionary": [], + "geomes": {} + } + }, + "source_provider": "baseminerals", + "dimension_selectors": {} + } + } +} diff --git a/src/main/resources/assets/baseminerals/orespawn/baseminerals.json b/tools/validation/legacy112/fixtures/orespawn3-baseminerals.json similarity index 100% rename from src/main/resources/assets/baseminerals/orespawn/baseminerals.json rename to tools/validation/legacy112/fixtures/orespawn3-baseminerals.json diff --git a/tools/validation/legacy112/overlap_dependencies.json b/tools/validation/legacy112/overlap_dependencies.json new file mode 100644 index 0000000..0a6a93b --- /dev/null +++ b/tools/validation/legacy112/overlap_dependencies.json @@ -0,0 +1,23 @@ +[ + { + "id": "baseelectric", + "coordinate": "curse.maven:baseelectric-1783006:9452435", + "version": "2.2.6.112021", + "sha256": "9DB6800CAF1795BBA56C483C1A177C09F0893413644B6C62ADE5978C7FF61E78", + "repository": "https://www.cursemaven.com" + }, + { + "id": "basepower", + "coordinate": "curse.maven:basepower-1780140:9452029", + "version": "2.4.4.112021", + "sha256": "126E9D0969CE6146F45A59EE4D6CA2B5FDF29ECF559BA595DCC1A9CF29A743DB", + "repository": "https://www.cursemaven.com" + }, + { + "id": "mineralogy", + "coordinate": "curse.maven:minecraft-mineralogy-240974:8722772", + "version": "6.0.1.112021", + "sha256": "558AD3403486F3B5CF575494BDF0A36B9F5967EA8D58E94BDBC2C2B3B6AF5D22", + "repository": "https://www.cursemaven.com" + } +] diff --git a/tools/validation/legacy112/policy-fixture/review/minerals/SourceFixture.java b/tools/validation/legacy112/policy-fixture/review/minerals/SourceFixture.java new file mode 100644 index 0000000..cc77cb3 --- /dev/null +++ b/tools/validation/legacy112/policy-fixture/review/minerals/SourceFixture.java @@ -0,0 +1,50 @@ +package review.minerals; + +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.item.Item; +import net.minecraft.item.ItemBlock; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.*; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.oredict.OreDictionary; +import zone.moddev.mc.orespawn.api.*; + +/** Synthetic provider contract fixture. This is not the Electric Advantage mod. */ +@Mod(modid="electricadvantage",name="Synthetic source-policy fixture",version="test",dependencies="required-after:baseminerals") +public final class SourceFixture { + private Block lithium; + private Block sulfur; + + @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent public void registerBlocks(RegistryEvent.Register event) { + lithium = new Block(Material.ROCK).setRegistryName("electricadvantage", "li_ore"); + sulfur = new Block(Material.ROCK).setRegistryName("electricadvantage", "sulfur_ore"); + event.getRegistry().registerAll(lithium, sulfur); + } + + @SubscribeEvent public void registerItems(RegistryEvent.Register event) { + for (Block block : new Block[]{lithium, sulfur}) { + Item item = new ItemBlock(block).setRegistryName(block.getRegistryName()); + event.getRegistry().register(item); + OreDictionary.registerOre(block == lithium ? "oreLithium" : "oreSulfur", item); + } + } + @Mod.EventHandler public void init(FMLInitializationEvent event) { + WorldgenProvider.Builder provider=WorldgenProvider.builder("electricadvantage",1); + for(String material:new String[]{"lithium","sulfur"}) { + String block="lithium".equals(material)?"li_ore":"sulfur_ore"; + provider.ore(new ResourceLocation("electricadvantage","ore/"+material),new ResourceLocation("electricadvantage",block),ore->ore + .enabled(false).material(new ResourceLocation("orespawn",material)).nativeGeneration(false).retrogen(false) + .dimension(new ResourceLocation("minecraft","overworld"),p->p.yRange(1,31).attempts(1).quantity(8).pattern(OrePattern.DEFAULT) + .placementChannel(new ResourceLocation("orespawn","standard")).hostBlock(new ResourceLocation("minecraft","stone")))); + } + if(!OreSpawnApi.enqueue(provider.build()))throw new IllegalStateException("Synthetic provider rejected"); + } +} diff --git a/tools/validation/legacy112/reproduce_os1_reload.py b/tools/validation/legacy112/reproduce_os1_reload.py new file mode 100644 index 0000000..c92bdbf --- /dev/null +++ b/tools/validation/legacy112/reproduce_os1_reload.py @@ -0,0 +1,33 @@ +"""Move a world with an already imported OS1 profile from 1.10.2 or 1.11.2.""" +import argparse,json +from pathlib import Path +from run_runtime_checks import ROOT,dependency,prepare,run,write_json +from run_packaged_suite import fingerprint,subset + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + for name in ('java','template','output','source-world'):parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--mod-jar',type=Path,default=ROOT/'build/libs/BaseMinerals-2.5.1.112021.jar') + parser.add_argument('--probe-jar',type=Path,default=ROOT/'build/test-mods/baseminerals-probe-packaged.jar') + parser.add_argument('--with-base-metals',action='store_true') + parser.add_argument('--mirror',type=Path);args=parser.parse_args() + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + jars=[dependency(s,args.mirror) for s in specs if s.get('runtime') + or (args.with_base_metals and s['id']=='basemetals')]+[args.mod_jar,args.probe_jar] + args.output.mkdir(parents=True,exist_ok=False) + source_files=fingerprint(args.source_world) + profile=json.loads((args.source_world/'serverconfig/orespawn-worldgen.json').read_text()) + before={k:v for k,v in profile['ores'].items() if v.get('block')=='baseminerals:lithium_ore'} + assert len(before)==1 and next(iter(before.values())).get('legacy_import')=='os1',before + write_json(args.output/'source-fixtures.json',{'path':str(args.source_world),'sha256':source_files}) + target=args.output/'profile-only';prepare(target,args.template,jars,args.source_world) + reload=run(args.java,target,'later',{'minerals.sample':'true','minerals.width':'16'}) + after={k:v for k,v in reload['profile']['ores'].items() if v.get('block')=='baseminerals:lithium_ore'} + passed=set(before)==set(after) + write_json(args.output/'required-migration-gate.json',{'passed':passed,'before':before,'after':after,'counts':reload['dimensions']['0']['counts']}) + assert passed,('OS1 reload adds native placement alongside the imported rule',list(before),list(after)) + subset(before,after) + assert fingerprint(args.source_world)==source_files,'Copied source world was modified' + print('BASE_MINERALS_OS1_RELOAD_PASS',flush=True) + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/run_continuity_checks.py b/tools/validation/legacy112/run_continuity_checks.py new file mode 100644 index 0000000..75c194e --- /dev/null +++ b/tools/validation/legacy112/run_continuity_checks.py @@ -0,0 +1,57 @@ +"""Census copied imported/saved profiles and explicit positive/negative hosts.""" +import argparse,copy,json +from pathlib import Path +from run_runtime_checks import ROOT,dependency,prepare,run,write_json +from run_packaged_suite import PREFIX,NAMES,placements,fingerprint,subset + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + for name in ('java','template','output','suite','selector-fixture'):parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--mirror',type=Path);args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=False) + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + jars=[dependency(s,args.mirror) for s in specs if s.get('runtime')]+[ROOT/'build/libs/BaseMinerals-2.5.1.112021.jar',ROOT/'build/test-mods/baseminerals-probe-packaged.jar'] + results={};fixtures={};failures={} + for name,source in [('os1',args.suite/'os1-profile-reload/world'),('os3',args.suite/'os3-import/world'),('saved-selector',args.selector_fixture)]: + before=fingerprint(source);target=args.output/name;prepare(target,args.template,jars,source) + original=json.loads((source/'serverconfig/orespawn-worldgen.json').read_text()) + if name=='saved-selector': + for ore in original['ores'].values(): + if ore.get('source_mod')=='baseminerals':assert not ore.get('dimensions'),ore + override=json.loads((ROOT/'build/reports/native-provider.json').read_text()) + for p in placements(override['ores'][PREFIX+'salt_ore']):p['frequency']=9 + write_json(target/'config/baseminerals-orespawn.json',override) + report=run(args.java,target,'later',{'minerals.sample':'true','minerals.width':'16'}) + for block in {ore.get('block') for ore in original['ores'].values() if ore.get('source_mod')=='baseminerals'}: + old_ids={key for key,ore in original['ores'].items() if ore.get('block')==block} + new_ids={key for key,ore in report['profile']['ores'].items() if ore.get('block')==block} + if old_ids!=new_ids:failures[name]={'block':block,'original_ids':sorted(old_ids),'reloaded_ids':sorted(new_ids)} + for key,ore in original['ores'].items(): + if ore.get('source_mod')=='baseminerals': + actual=report['profile']['ores'][key];subset(ore,actual) + for section in ('dimensions','dimension_selectors'): + assert ore.get(section,{})==actual.get(section,{}),('Saved dimension declarations changed',name,key,section) + assert fingerprint(source)==before,'Source fixture changed' + results[name]=report;fixtures[name]={'path':str(source),'files':before} + native=json.loads((ROOT/'build/reports/native-provider.json').read_text()) + for name,properties in [('stone-variants',{'minerals.variantHosts':'true'}),('negative-hosts',{'minerals.negativeHosts':'true'}),('decoded-shapes',{'minerals.record':'true'})]: + target=args.output/name;prepare(target,args.template,jars) + if name=='decoded-shapes': + definition=copy.deepcopy(native) + for material in NAMES: + for p in placements(definition['ores'][PREFIX+material+'_ore']): + settings={k:p[k] for k in ('spread','vertical_spread','node_size')};settings['length']=8 + p['pattern']={'type':'basemineralsprobe:'+material,'settings':settings} + write_json(target/'config/baseminerals-orespawn.json',definition) + report=run(args.java,target,'first',{'minerals.custom':'true','minerals.sample':'true','minerals.width':'16',**properties}) + if name=='decoded-shapes': + for material,stats in report['recording'].items(): + p=placements(native['ores'][PREFIX+material+'_ore'])[0] + subset({**{k:p[k] for k in ('spread','vertical_spread','node_size')},'length':8},stats['settings']) + results[name]=report + write_json(args.output/'source-fixtures.json',fixtures) + write_json(args.output/'results.json',results) + write_json(args.output/'continuity-gates.json',{'passed':not failures,'failures':failures}) + assert not failures,('Imported rules gained an extra placement budget',failures) + print('BASE_MINERALS_CONTINUITY_PASS',len(results),'runs',flush=True) + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/run_eclipse_profile.py b/tools/validation/legacy112/run_eclipse_profile.py new file mode 100644 index 0000000..a379d69 --- /dev/null +++ b/tools/validation/legacy112/run_eclipse_profile.py @@ -0,0 +1,72 @@ +"""Run a generated Eclipse profile in an isolated directory, with a test-only probe.""" + +import argparse +import json +import os +import shlex +import shutil +import subprocess +import time +import xml.etree.ElementTree as ET +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("profile", type=Path) + parser.add_argument("java", type=Path) + parser.add_argument("directory", type=Path) + parser.add_argument("phase", choices=["first", "reload"]) + parser.add_argument("--probe", type=Path, required=True) + parser.add_argument("--production-output", type=Path, + help="Use a disposable Eclipse build without touching the editor's bin directory") + args = parser.parse_args() + profile = args.profile.resolve() + root = ET.parse(profile).getroot() + strings = {node.get("key"): node.get("value") for node in root.findall("stringAttribute")} + environment = os.environ.copy() + for node in root.findall("mapAttribute/mapEntry"): + environment[node.get("key")] = node.get("value") + production_output = (args.production_output or profile.parent / "bin/main").resolve() + environment["MOD_CLASSES"] = str(production_output) + + archives = [] + for node in root.findall("listAttribute[@key='org.eclipse.jdt.launching.CLASSPATH']/listEntry"): + entry = ET.fromstring(node.get("value")) + path = entry.get("externalArchive") + if path: + archives.append(path) + elif entry.get("projectName"): + archives.append(str(production_output)) + + directory = args.directory.resolve() + (directory / "mods").mkdir(parents=True, exist_ok=True) + shutil.copy2(args.probe, directory / "mods" / args.probe.name) + (directory / "eula.txt").write_text("eula=true\n", encoding="utf-8") + if not (directory / "server.properties").exists(): + (directory / "server.properties").write_text("server-ip=127.0.0.1\nserver-port=0\nonline-mode=false\nlevel-type=FLAT\nview-distance=3\n", encoding="utf-8") + + command = [str(args.java), "-Xmx3G", "-Dminerals.phase=" + args.phase, "-Dminerals.saved=true"] + client = "Client" in profile.stem + if client: + command += ["-Dminerals.client=true"] + command += shlex.split(strings.get("org.eclipse.jdt.launching.VM_ARGUMENTS", "")) + command += ["-cp", os.pathsep.join(dict.fromkeys(archives)), strings["org.eclipse.jdt.launching.MAIN_TYPE"]] + command += shlex.split(strings["org.eclipse.jdt.launching.PROGRAM_ARGUMENTS"]) + started = time.time_ns() + with (directory / (args.phase + "-console.log")).open("w", encoding="utf-8") as log: + process = subprocess.run(command, cwd=directory, env=environment, stdout=log, + stderr=subprocess.STDOUT, timeout=300) + + report = directory / (args.phase + ("-client.json" if client else "-minerals.json")) + if (process.returncode or not report.exists() or report.stat().st_mtime_ns < started + or not json.loads(report.read_text())["passed"]): + raise SystemExit(f"Launch failed; inspect {directory / (args.phase + '-console.log')}") + print(report.read_text(), flush=True) + if client: + world_report=json.loads((directory/(args.phase+'-minerals.json')).read_text()) + if not world_report['passed']:raise SystemExit(str(world_report['failures'])) + + +if __name__ == "__main__": + main() diff --git a/tools/validation/legacy112/run_mineralogy_policy.py b/tools/validation/legacy112/run_mineralogy_policy.py new file mode 100644 index 0000000..d48cb4a --- /dev/null +++ b/tools/validation/legacy112/run_mineralogy_policy.py @@ -0,0 +1,46 @@ +"""Review real public Mineralogy sulfur policies on disposable world copies.""" +import argparse,json +from pathlib import Path +from run_runtime_checks import ROOT,dependency,prepare,run,write_json,digest +from run_packaged_suite import fingerprint + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + for name in ('java','template','output','integration'):parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--mirror',type=Path);args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=False) + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + jars=[dependency(s,args.mirror) for s in specs if s.get('runtime')]+[ROOT/'build/libs/BaseMinerals-2.5.1.112021.jar',ROOT/'build/test-mods/baseminerals-probe-packaged.jar'] + mineralogy=next(p for p in (args.integration/'mods').glob('*.jar') if 'mineralogy' in p.name.lower()) + pin=next(s for s in json.loads((ROOT/'tools/validation/legacy112/overlap_dependencies.json').read_text()) if s['id']=='mineralogy') + assert digest(mineralogy).upper()==pin['sha256'];jars.append(mineralogy) + source=args.integration/'world';before=fingerprint(source);results={} + key='orespawn:sulfur|minecraft:overworld' + for label in ('balanced','single-base','single-mineralogy','weighted'): + target=args.output/label;prepare(target,args.template,jars,source) + path=target/'world/serverconfig/orespawn-worldgen.json';profile=json.loads(path.read_text());policy=profile['ore_source_policies'][key] + candidates={c['owner']:c['source_id'] for c in policy['candidates'] if c['loaded'] and not c['external']} + assert set(candidates)=={'baseminerals','mineralogy'},('A managed Mineralogy sulfur provider is required',candidates) + outputs={candidates['baseminerals']:1,candidates['mineralogy']:3 if label=='weighted' else 1} + if label.startswith('single-'):outputs={candidates['baseminerals' if label=='single-base' else 'mineralogy']:1} + policy.update(mode='consolidated',output_mode='balanced' if label=='balanced' else 'custom' if label=='weighted' else 'single',outputs=outputs, + placement_sources={'orespawn:standard':candidates['mineralogy']},review_required=False) + write_json(path,profile) + report=run(args.java,target,'later',{'minerals.sample':'true','minerals.shared':'true','minerals.width':'16'}) + active=report['profile']['ore_source_policies'][key] + assert active['mode']=='consolidated' and active['placement_sources']==policy['placement_sources'],active + physical=report['dimensions']['0']['sharedOutputs'];own=physical.get('baseminerals:sulfur_ore',0);other=physical.get('mineralogy:sulfur_ore',0) + assert own+other>0,physical + if label=='single-base':assert own>0 and other==0 + if label=='single-mineralogy':assert own==0 and other>0 + if label=='balanced':assert own>0 and other>0 and 0.70 and 2.5started,report + assert json.loads(report.read_text())['passed'],report + assert result.returncode==0 + console=(args.output/(args.phase+'-console.log')).read_text(errors='replace') + assert '[TEXTURE ERRORS]' not in console,'Missing textures in the installed client' + assert "Can't find the resource index file" not in console,'Minecraft assets were not loaded' + print('BASE_MINERALS_PACKAGED_CLIENT_PASS',args.phase,flush=True) + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/run_packaged_suite.py b/tools/validation/legacy112/run_packaged_suite.py new file mode 100644 index 0000000..650590c --- /dev/null +++ b/tools/validation/legacy112/run_packaged_suite.py @@ -0,0 +1,195 @@ +"""Qualify installed Base Minerals in disposable Forge servers; never use a played world.""" +import argparse, copy, gzip, hashlib, json, math, shutil, struct +from pathlib import Path +from run_runtime_checks import ROOT, dependency, prepare, run, write_json, digest + +PREFIX='baseminerals:legacy/' +SELECTOR='orespawn:all_except_nether_end' +NAMES=['lithium','niter','salt','sulfur','phosphorus','potash','saltpeter'] +def fingerprint(path): + return {str(p.relative_to(path)):digest(p) for p in path.rglob('*') if p.is_file()} +def seed_zero(directory): + # A dedicated server treats level-seed=0 as random. Supply valid fresh world + # metadata with RandomSeed=0 so this test actually exercises seed zero. + def name(text): + value=text.encode();return struct.pack('>H',len(value))+value + def tag(kind,key,value):return bytes([kind])+name(key)+value + data=tag(4,'RandomSeed',struct.pack('>q',0))+tag(8,'generatorName',name('default')) + for key,value in [('generatorVersion',1),('GameType',1),('version',19133),('SpawnX',0),('SpawnY',64),('SpawnZ',0)]:data+=tag(3,key,struct.pack('>i',value)) + data+=tag(8,'LevelName',name('Base Minerals seed zero'))+tag(1,'MapFeatures',b'\0')+tag(1,'initialized',b'\x01')+b'\0' + world=directory/'world';world.mkdir(exist_ok=True) + with gzip.open(world/'level.dat','wb') as stream:stream.write(b'\x0a\0\0'+tag(10,'Data',data)+b'\0') +def placement(ore):return ore['dimension_selectors'][SELECTOR] +def placements(ore):return [placement(ore)]+list(ore.get('dimensions',{}).values()) +def subset(expected,actual): + if isinstance(expected,dict): + for key,value in expected.items():assert key in actual,key;subset(value,actual[key]) + else:assert expected==actual,(expected,actual) + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + for name in ('java','template','output'):parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--mirror',type=Path);parser.add_argument('--quick',action='store_true');parser.add_argument('--with-base-metals',action='store_true') + parser.add_argument('--mod-jar',type=Path,default=ROOT/'build/libs/BaseMinerals-2.5.1.112021.jar') + parser.add_argument('--probe-jar',type=Path,default=ROOT/'build/test-mods/baseminerals-probe-packaged.jar') + parser.add_argument('--prior-suite',type=Path,help='Reuse the completed census and compatibility reports through OS3 with identical input jars.') + args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=False) + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + base=[dependency(s,args.mirror) for s in specs if s.get('runtime') or (args.with_base_metals and s['id']=='basemetals')] + base += [args.mod_jar,args.probe_jar] + native=json.loads((ROOT/'build/reports/native-provider.json').read_text()) + fixtures=ROOT/'tools/validation/legacy112/fixtures' + os1_fixture=fixtures/'orespawn1-imported-111-profile.json' + os3_fixture=fixtures/'orespawn3-baseminerals.json' + write_json(args.output/'source-fixtures.json',{str(p.relative_to(ROOT)):digest(p) for p in + [Path(__file__).resolve(),ROOT/'build/reports/native-provider.json',os1_fixture,os3_fixture]}) + results={} + migration_failures=[] + def verify_os1_reload(report): + matches=[key for key,ore in report['profile']['ores'].items() if ore.get('block')=='baseminerals:lithium_ore'] + if len(matches)!=1: + migration_failures.append({'message':'OS1 reload reinserted the native lithium budget','rules':matches}) + write_json(args.output/'migration-failures.json',migration_failures) + def setup(name,seed=8675309,extra=(),world=None,override=None,config=None): + directory=args.output/name;prepare(directory,args.template,base+list(extra),world) + properties=directory/'server.properties';text=properties.read_text().replace('level-seed=8675309','level-seed='+str(seed));properties.write_text(text) + if seed==0 and world is None:seed_zero(directory) + if override is not None:write_json(directory/'config/baseminerals-orespawn.json',override) + if config is not None: + path=directory/'config/BaseMinerals.cfg';path.parent.mkdir(parents=True,exist_ok=True);path.write_text(config) + return directory + def check(directory,phase='first',properties=None): + result=run(args.java,directory,phase,properties or {}) + results[directory.name+':'+phase]=result + write_json(args.output/'results.json',results) + return result + if args.prior_suite: + prior=json.loads((args.prior_suite/'results.json').read_text()) + pre_migration=[ + name for seed in (0,1,8675309,123456789) + for name in (f'seed-{seed}:first',f'seed-{seed}:reload',f'repeat-{seed}:first') + ] + pre_migration += ['later-chunks:later','later-chunks:reload','generation-disabled:first','tinkers:first', + 'os3-import:first','os3-import:reload'] + pre_migration += [f'disabled-{material}:first' for material in ['all',*NAMES,'silicon','charcoal']] + results={name:prior[name] for name in pre_migration} + assert all(r['passed'] for r in results.values()),'A reused report failed' + expected={p.name:digest(p) for p in base} + for directory in args.prior_suite.iterdir(): + if directory.is_dir() and (directory/'input-jars.json').exists(): + inputs=json.loads((directory/'input-jars.json').read_text()) + assert all(inputs.get(name)==sha for name,sha in expected.items()),('Changed input jar',directory) + source=args.prior_suite/'seed-8675309/world';before=fingerprint(source) + write_json(args.output/'reused-evidence.json',{'path':str(args.prior_suite),'results_sha256':digest(args.prior_suite/'results.json'),'source_files':before}) + else: + width=3 if args.quick else 16 + properties={'minerals.sample':'true','minerals.width':str(width),'minerals.saved':'true'} + for seed in ([8675309] if args.quick else [0,1,8675309,123456789]): + directory=setup('seed-'+str(seed),seed) + first=check(directory,properties=properties) + assert first['seed']==seed,('Wrong world seed',seed,first['seed']) + subset(native['ores'],first['profile']['ores']) + reload=check(directory,'reload',properties) + assert {d:v['digest'] for d,v in first['dimensions'].items()}=={d:v['digest'] for d,v in reload['dimensions'].items()},'Reload changed ore census' + repeat=check(setup('repeat-'+str(seed),seed),properties=properties) + assert {d:v['digest'] for d,v in first['dimensions'].items()}=={d:v['digest'] for d,v in repeat['dimensions'].items()},'Seed is not deterministic' + if args.quick: + print('BASE_MINERALS_QUICK_SUITE_PASS',flush=True);return + source=args.output/'seed-8675309'/'world';before=fingerprint(source) + later=setup('later-chunks',world=source);check(later,'later',properties) + unchanged=check(later,'reload',properties) + original=results['seed-8675309:first'] + assert {d:v['digest'] for d,v in original['dimensions'].items()}=={d:v['digest'] for d,v in unchanged['dimensions'].items()},'Later generation changed old ore coordinates' + assert fingerprint(source)==before,'Copied source fixture was modified' + override=copy.deepcopy(native) + for ore in override['ores'].values():ore['enabled']=False + empty=check(setup('generation-disabled',override=override),properties={'minerals.sample':'true','minerals.noOres':'true','minerals.width':'16'}) + assert all(not value['counts'] for value in empty['dimensions'].values()) + for name in ['all']+NAMES+['silicon','charcoal']: + disabled=NAMES+['silicon','charcoal'] if name=='all' else [name] + config='Metals {\n'+''.join(' B:Enable'+n.capitalize()+'=false\n' for n in disabled if n!='charcoal')+'}\n' + if 'charcoal' in disabled:config+='Vanilla {\n B:EnableCharcoal=false\n}\n' + check(setup('disabled-'+name,config=config),properties={'minerals.sample':'true','minerals.width':'16','minerals.noOres':str(name=='all').lower()}) + optional=[dependency(s,args.mirror) for s in specs if s['id'] in ['tconstruct','mantle']] + check(setup('tinkers',extra=optional)) + # Imported OS3 rules must retain historical defaults/custom values, not rebalance. + legacy=json.loads(os3_fixture.read_text()) + legacy['spawns']['lithium_ore']['parameters'].update(size=5,variation=0,frequency=2.75,minHeight=10,maxHeight=50) + legacy['spawns']['sulfur_ore']['enabled']=False + imported=setup('os3-import');write_json(imported/'config/orespawn3/baseminerals.json',legacy) + old_hash=digest(imported/'config/orespawn3/baseminerals.json') + result=check(imported) + p=placement(result['profile']['ores'][PREFIX+'lithium_ore']);subset({'min_y':10,'max_y':49,'frequency':2.75,'quantity':5},p) + assert not result['profile']['ores'][PREFIX+'sulfur_ore']['enabled'] + assert digest(imported/'config/orespawn3/baseminerals.json')==old_hash + check(imported,'reload') + # OS1 imports on 1.10/1.11. In 1.12, exercise the already imported world profile. + imported_ores=json.loads(os1_fixture.read_text())['ores'] + os1=setup('os1-profile-reload',world=source) + saved=os1/'world/serverconfig/orespawn-worldgen.json' + imported_profile=json.loads(saved.read_text()) + del imported_profile['ores'][PREFIX+'lithium_ore'] + imported_profile['ores'].update(imported_ores) + write_json(saved,imported_profile) + reloaded=check(os1,'later',{'minerals.sample':'true','minerals.width':'16'}) + verify_os1_reload(reloaded) + legacy_key=next(iter(imported_ores)) + assert legacy_key in reloaded['profile']['ores'],'Saved OS1 rule ID changed' + expected=imported_ores[legacy_key]['dimensions']['minecraft:overworld'] + actual=reloaded['profile']['ores'][legacy_key]['dimensions']['minecraft:overworld'] + subset({k:expected[k] for k in ('min_y','max_y','frequency','quantity')},actual) + + legacy=json.loads(os3_fixture.read_text()) + existing=setup('saved-profile-precedence',world=source) + profile=existing/'world/serverconfig/orespawn-worldgen.json';value=json.loads(profile.read_text()) + for p in placements(value['ores'][PREFIX+'salt_ore']):p['frequency']=1.25 + write_json(profile,value) + override=copy.deepcopy(native) + for p in placements(override['ores'][PREFIX+'salt_ore']):p['frequency']=9 + write_json(existing/'config/baseminerals-orespawn.json',override);write_json(existing/'config/orespawn3/baseminerals.json',legacy) + result=check(existing);assert placement(result['profile']['ores'][PREFIX+'salt_ore'])['frequency']==1.25 + future=check(setup('native-precedence',override=override));assert placement(future['profile']['ores'][PREFIX+'salt_ore'])['frequency']==9 + # Record the actual baked attempts while delegating shape placement to OS4. + recorded=copy.deepcopy(native) + for name in NAMES: + for p in placements(recorded['ores'][PREFIX+name+'_ore']): + settings={k:p[k] for k in ('spread','vertical_spread','node_size')} + settings['length']=8;p['pattern']={'type':'basemineralsprobe:'+name,'settings':settings} + overworld=check(setup('recorded-overworld',override=recorded),properties={'minerals.record':'true','minerals.sample':'true','minerals.width':'16'}) + fixture=check(setup('recorded-host-fixture',override=recorded),properties={'minerals.custom':'true','minerals.record':'true','minerals.sample':'true','minerals.negativeHosts':'true','minerals.width':'16'}) + for name in NAMES: + p=placement(native['ores'][PREFIX+name+'_ore']);rate=p['frequency'];fraction=rate%1 + tolerance=5*math.sqrt(256*fraction*(1-fraction)) if fraction else 0 + # Both exact-domain and selector routes receive one placement budget. + # Different dimension population paths need not consume identical RNG. + for report in (overworld,fixture): + stats=report['recording'][name] + assert abs(stats['attempts']-256*rate)<=tolerance,(name,stats['attempts'],rate) + assert p['min_quantity']<=stats['minQuantity']<=stats['maxQuantity']<=p['max_quantity'] + assert not any(n for y,n in enumerate(stats['heights']) if yp['max_y']) + if p['height_distribution']=='triangle': + low,high=p['min_y'],p['max_y'];middle=(high-low+1)//3 + assert sum(stats['heights'][low+middle:high-middle+1])>sum(stats['heights'][low:low+middle]),name + if p['height_distribution']=='bottom_triangle': + midpoint=(p['min_y']+p['max_y'])//2 + assert sum(stats['heights'][:midpoint+1])>sum(stats['heights'][midpoint+1:]),name + # Same engine/JVM/seed, three isolated runs for both sets of declarations. + historical=copy.deepcopy(native) + for name in NAMES: + for p in placements(historical['ores'][PREFIX+name+'_ore']):p.update(min_y=0,max_y=31 if name=='sulfur' else 95,frequency=8 if name in ('lithium','niter','salt') else 6,min_quantity=4,max_quantity=11,pattern='default',height_distribution='uniform') + timings={} + import statistics + rules = {'historical': historical, 'rebalanced': native} + samples = {label: [] for label in rules} + for label in ('historical','rebalanced','rebalanced','historical','historical','rebalanced'): + index = len(samples[label]) + result=check(setup('timing-'+label+'-'+str(index),override=rules[label]),properties={'minerals.custom':'true','minerals.sample':'true','minerals.width':'16'}) + samples[label].append(result['dimensions']['42']['generationNanos']) + timings = {label: {'runs': times, 'median': statistics.median(times)} for label, times in samples.items()} + timings['ratio']=timings['rebalanced']['median']/timings['historical']['median'];write_json(args.output/'timings.json',timings) + assert timings['ratio']<=1.2,('Generation regression needs investigation',timings) + assert fingerprint(source)==before + assert not migration_failures,migration_failures + print('BASE_MINERALS_PACKAGED_SUITE_PASS',len(results),'runs',flush=True) + +if __name__=='__main__':main() diff --git a/tools/validation/legacy112/run_policy_fixture.py b/tools/validation/legacy112/run_policy_fixture.py new file mode 100644 index 0000000..da17981 --- /dev/null +++ b/tools/validation/legacy112/run_policy_fixture.py @@ -0,0 +1,64 @@ +"""Test the OS4 managed-source contract using an explicitly synthetic sibling.""" +import argparse,copy,json +from pathlib import Path +from run_runtime_checks import ROOT,dependency,prepare,run,write_json +from run_packaged_suite import fingerprint + +def main(): + parser=argparse.ArgumentParser() + for name in ('java','template','output'):parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--mirror',type=Path) + args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=False) + specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) + jars=[dependency(s,args.mirror) for s in specs if s.get('runtime')]+[ROOT/'build/libs/BaseMinerals-2.5.1.112021.jar',ROOT/'build/test-mods/baseminerals-probe-packaged.jar'] + fixture=ROOT/'build/test-mods/synthetic-source-policy-fixture-packaged.jar' + properties={'minerals.sample':'true','minerals.shared':'true','minerals.width':'16'} + balanced=args.output/'balanced';prepare(balanced,args.template,jars+[fixture]) + result=run(args.java,balanced,'first',properties) + policies=result['profile']['ore_source_policies'];keys=['orespawn:'+m+'|minecraft:overworld' for m in ('sulfur','lithium')] + for key in keys: + policy=policies[key];assert policy['mode']=='consolidated' and policy['output_mode']=='balanced',policy + assert len(policy['outputs'])==2 and len(policy['placement_sources'])==1,policy + assert next(iter(policy['placement_sources'].values())).startswith('baseminerals:'),policy + source=balanced/'world';before=fingerprint(source);results={'balanced':result} + for label,mode,owner in [('single-base','single','baseminerals'),('single-sibling','single','electricadvantage'),('custom','custom','electricadvantage'),('weighted','custom','electricadvantage'),('original','custom','baseminerals')]: + target=args.output/label;prepare(target,args.template,jars+[fixture],source) + path=target/'world/serverconfig/orespawn-worldgen.json';profile=json.loads(path.read_text()) + for key in keys: + policy=profile['ore_source_policies'][key] + selected=next(c['source_id'] for c in policy['candidates'] if c['owner']==owner) + policy.update(mode='keep_separate' if label=='original' else 'consolidated',output_mode=mode,outputs={selected:1},review_required=False) + if label=='weighted': + policy['outputs']={c['source_id']:3 if c['owner']=='electricadvantage' else 1 for c in policy['candidates']} + write_json(path,profile);report=run(args.java,target,'later',properties);results[label]=report + outputs=report['dimensions']['0']['sharedOutputs'] + if label not in ('original','weighted'): + assert any(key.startswith(owner+':') and count>0 for key,count in outputs.items()),outputs + assert not any(not key.startswith(owner+':') and count>0 for key,count in outputs.items()),outputs + for material,foreign in [('lithium','li'),('sulfur','sulfur')]: + def total(label): + values=results[label]['dimensions']['0']['sharedOutputs'] + return values.get('baseminerals:'+material+'_ore',0)+values.get('electricadvantage:'+foreign+'_ore',0) + assert total('single-base')==total('single-sibling')==total('original'),material + weighted=results['weighted']['dimensions']['0']['sharedOutputs'] + own=weighted.get('baseminerals:'+material+'_ore',0);sibling=weighted.get('electricadvantage:'+foreign+'_ore',0) + assert own>0 and 2.5 oldConsumer=Class.forName("review.minerals.LegacyConsumer"); + require(oldConsumer.getMethod("material").invoke(null)==Materials.getMaterialByName("lithium"),"Old binary linked to a different material registry",failures); + require(oldConsumer.getMethod("proxy").invoke(null)==zone.moddev.mc.baseminerals.BaseMinerals.proxy,"Old binary linked to a different proxy",failures); + require(oldConsumer.getMethod("reflected").invoke(null)==zone.moddev.mc.baseminerals.BaseMinerals.class,"Old reflection name did not relink",failures); + require(com.mcmoddev.lib.util.Config.class.isAssignableFrom(Class.forName("review.minerals.LegacyConfiguration")),"Old configuration subclass lost its shared API",failures); + report.addProperty("legacyBinarySharedState",failures.size()==0); + for(String name:ORES) { + boolean enabled=BMiConfig.isEnabled(name); + require(Materials.hasMaterial(name)==enabled,"Material switch: "+name,failures); + require(ForgeRegistries.BLOCKS.containsKey(new ResourceLocation("baseminerals",name+"_ore"))==enabled,"Ore registry: "+name,failures); + if(enabled)require(Materials.getMaterialByName(name).hasItem(Names.POWDER),"Missing powder: "+name,failures); + } + for(String name:new String[]{"lithium","silicon"}) if(BMiConfig.isEnabled(name)) { + MMDMaterial material=Materials.getMaterialByName(name); + require(material.hasItem(Names.INGOT)&&material.hasItem(Names.NUGGET),"Metal forms missing: "+name,failures); + boolean fluidEnabled=com.mcmoddev.lib.util.Config.Options.isFluidEnabled(name); + if(Boolean.getBoolean("minerals.noFluids"))require(!fluidEnabled,"Fluid switch not applied: "+name,failures); + if (fluidEnabled) { + require(material.getFluid()!=null&&material.getFluidBlock()!=null,"Molten forms missing: "+name,failures); + } else { + require(material.getFluid()==null&&material.getFluidBlock()==null,"Disabled molten forms exist: "+name,failures); + } + if(material.getFluid()!=null) { + net.minecraftforge.fluids.FluidStack stack = new net.minecraftforge.fluids.FluidStack(material.getFluid(), 1000); + ItemStack bucket = net.minecraftforge.fluids.FluidUtil.getFilledBucket(stack); + net.minecraftforge.fluids.FluidStack contents = net.minecraftforge.fluids.FluidUtil.getFluidContained(bucket); + require(!bucket.isEmpty() && contents != null && contents.isFluidEqual(stack) && contents.amount == 1000, + "Bucket missing or cannot hold molten " + name, failures); + } + } + require(!ForgeRegistries.BLOCKS.containsKey(new ResourceLocation("baseminerals","charcoal_ore")),"Charcoal ore registered",failures); + require(!ForgeRegistries.BLOCKS.containsKey(new ResourceLocation("baseminerals","silicon_ore")),"Silicon ore registered",failures); + require(!ForgeRegistries.ITEMS.containsKey(new ResourceLocation("baseminerals","charcoal_powder")), + "Base Minerals registered shared charcoal",failures); + boolean charcoal=BMiConfig.isEnabled("charcoal"); + boolean sulfur=BMiConfig.isEnabled("sulfur"); + for(String mineral:new String[]{"potash","saltpeter"}) { + boolean enabled=sulfur&&BMiConfig.isEnabled(mineral); + checkRecipe("gunpowder_coal_"+mineral,enabled,failures); + checkRecipe("gunpowder_charcoal_"+mineral,enabled&&charcoal,failures); + } + boolean charcoalSilicon=charcoal&&BMiConfig.isEnabled("silicon"); + checkRecipe("silicon_blend_sand_charcoal",charcoalSilicon,failures); + checkRecipe("silicon_charcoal",charcoalSilicon,failures); + if (Loader.isModLoaded("basemetals")) require(Materials.hasMaterial("charcoal"),"Shared charcoal missing",failures); + if(Loader.isModLoaded("tconstruct")) CastingChecks.check(failures); + require(OreSpawnApi.getActiveProfile(server).isPresent(),"No active OreSpawn profile",failures); + if(OreSpawnApi.getActiveProfile(server).isPresent()) { + JsonObject profile=OreSpawnApi.getActiveProfile(server).get().toJson(); report.add("profile",profile); + if(!Loader.isModLoaded("mineralogy"))require(profile.getAsJsonObject("terrain_dimensions").entrySet().size()==0,"Ore-only mods activated strata",failures); + if(Boolean.getBoolean("minerals.sample"))sample(server,profile,report,failures); + } + if(Boolean.getBoolean("minerals.saved"))saved(server.getWorld(0),phase,failures); + report.add("recording",RecordingPatterns.report(failures)); + report.add("failures",failures);report.addProperty("passed",failures.size()==0); + Files.write(Paths.get(phase+"-minerals.json"),(new GsonBuilder().setPrettyPrinting().create().toJson(report)+"\n").getBytes(StandardCharsets.UTF_8)); + System.out.println("BASE_MINERALS_RUNTIME_"+(failures.size()==0?"PASS":"FAIL")+" "+failures); + } + private static void checkRecipe(String name,boolean expected,JsonArray failures) { + require(ForgeRegistries.RECIPES.containsKey(new ResourceLocation("baseminerals",name))==expected, + "Recipe availability: "+name,failures); + } + private static void sample(MinecraftServer server,JsonObject profile,JsonObject report,JsonArray failures) throws Exception { + JsonObject dimensions=new JsonObject(); int width=Integer.getInteger("minerals.width",16); + int origin="later".equals(System.getProperty("minerals.phase"))?128:64; + int[] ids=Boolean.getBoolean("minerals.custom")?new int[]{42}:new int[]{0,-1,1}; + for(int id:ids) { + if(DimensionManager.getWorld(id)==null)DimensionManager.initDimension(id); + WorldServer world=server.getWorld(id); + long start=System.nanoTime(); + for(int x=origin-2;x=p.get("min_y").getAsInt()&&y<=p.get("max_y").getAsInt(),"Ore outside height: "+name+" at "+y,failures); + } + if(Boolean.getBoolean("minerals.negativeHosts"))require(y<21||y>28,"Ore replaced protected host at Y="+y,failures); + if(Boolean.getBoolean("minerals.variantHosts"))increment(variants,String.valueOf(1+(y-1)%6)); + } + } + if((id==0||id==42)&&!Boolean.getBoolean("minerals.noOres")) for(String name:ORES) + if(BMiConfig.isEnabled(name)) { + JsonObject rule=rule(profile,name+"_ore"); + if(rule!=null && rule.get("enabled").getAsBoolean() && !(Boolean.getBoolean("minerals.shared") && ("lithium".equals(name)||"sulfur".equals(name)))) require(counts.has(name+"_ore"),"No generated sample: "+name,failures); + } + JsonObject result=new JsonObject();result.add("counts",counts);result.add("sharedOutputs",shared);result.add("heights",heights);result.addProperty("chunks",width*width); + if(Boolean.getBoolean("minerals.variantHosts")) { + for(int variant=1;variant<=6;variant++)require(variants.has(String.valueOf(variant)),"No positive stone metadata sample: "+variant,failures); + result.add("hostVariants",variants); + } + result.addProperty("generationNanos",generated);result.addProperty("digest",hex(digest.digest()));dimensions.add(String.valueOf(id),result); + result.addProperty("sharedDigest",hex(sharedDigest.digest())); + System.out.println("BASE_MINERALS_CENSUS dimension="+id+" chunks="+(width*width)+" counts="+counts); + } + report.add("dimensions",dimensions); + } + private static JsonObject rule(JsonObject profile,String block) { + JsonObject ores=profile.getAsJsonObject("ores"); + JsonObject standard=ores.getAsJsonObject("baseminerals:legacy/"+block); + if(standard!=null)return standard; + // OS1 owns its converted IDs; match that saved declaration by its output. + for(Map.Entry entry:ores.entrySet()) { + JsonObject candidate=entry.getValue().getAsJsonObject(); + if(candidate.has("block")&&("baseminerals:"+block).equals(candidate.get("block").getAsString()))return candidate; + } + return null; + } + private static JsonObject placement(JsonObject rule,int dimension) { + String id=dimension==0?"minecraft:overworld":dimension==-1?"minecraft:the_nether":dimension==1?"minecraft:the_end":"minerals_fixture"; + if(rule.has("dimensions")&&rule.getAsJsonObject("dimensions").has(id))return rule.getAsJsonObject("dimensions").getAsJsonObject(id); + return rule.getAsJsonObject("dimension_selectors").getAsJsonObject("orespawn:all_except_nether_end"); + } + private static JsonObject activePlacement(JsonObject profile,JsonObject rule,int dimension) { + if(Boolean.getBoolean("minerals.shared")&&rule.has("material")) { + String domain=dimension==0?"minecraft:overworld":dimension==-1?"minecraft:the_nether":dimension==1?"minecraft:the_end":"minerals_fixture"; + JsonObject policies=profile.getAsJsonObject("ore_source_policies"); + JsonObject policy=policies==null?null:policies.getAsJsonObject(rule.get("material").getAsString()+"|"+domain); + if(policy!=null&&"consolidated".equals(policy.get("mode").getAsString())) { + JsonElement selected=policy.getAsJsonObject("placement_sources").get("orespawn:standard"); + if(selected!=null) { + JsonObject source=profile.getAsJsonObject("ores").getAsJsonObject(selected.getAsString()); + if(source!=null)return placement(source,dimension); + } + } + } + return placement(rule,dimension); + } + private static void increment(JsonObject object,String key){object.addProperty(key,object.has(key)?object.get(key).getAsInt()+1:1);} + private static String hex(byte[] data){StringBuilder s=new StringBuilder();for(byte b:data)s.append(String.format("%02x",b));return s.toString();} + private static void saved(WorldServer world,String phase,JsonArray failures) { + BlockPos position=new BlockPos(0,180,0); + if("first".equals(phase)) { + world.setBlockState(position,Blocks.CHEST.getDefaultState());TileEntityChest chest=(TileEntityChest)world.getTileEntity(position);int slot=0; + for(String name:ORES)if(BMiConfig.isEnabled(name)) { + ItemStack stack=Materials.getMaterialByName(name).getItemStack(Names.POWDER,3); + stack.setStackDisplayName("Saved "+name);stack.getTagCompound().setString("BaseMineralsCheck",name);chest.setInventorySlotContents(slot++,stack); + } + } + TileEntityChest chest=(TileEntityChest)world.getTileEntity(position);require(chest!=null,"Saved chest missing",failures);if(chest==null)return;int slot=0; + for(String name:ORES)if(BMiConfig.isEnabled(name)) { + ItemStack stack=chest.getStackInSlot(slot++); + require(!stack.isEmpty()&&stack.getCount()==3&&("Saved "+name).equals(stack.getDisplayName())&&name.equals(stack.getTagCompound().getString("BaseMineralsCheck")),"Saved stack changed: "+name,failures); + } + } + private static final class CastingChecks { + static void check(JsonArray failures) { + for(String name:new String[]{"lithium","silicon"})if(BMiConfig.isEnabled(name)) { + MMDMaterial material=Materials.getMaterialByName(name); + net.minecraftforge.fluids.Fluid fluid=material.getFluid(); + require(fluid!=null,"Casting fluid missing: "+name,failures); + if(fluid!=null) { + java.util.List table = + slimeknights.tconstruct.library.TinkerRegistry.getAllTableCastingRecipes(); + require(hasCasting(table,slimeknights.tconstruct.smeltery.TinkerSmeltery.castIngot, + material.getItemStack(Names.INGOT),fluid,144),"Ingot casting missing: "+name,failures); + require(hasCasting(table,slimeknights.tconstruct.smeltery.TinkerSmeltery.castNugget, + material.getItemStack(Names.NUGGET),fluid,16),"Nugget casting missing: "+name,failures); + require(hasCasting(slimeknights.tconstruct.library.TinkerRegistry.getAllBasinCastingRecipes(), + ItemStack.EMPTY,material.getBlockItemStack(Names.BLOCK),fluid,1296),"Block casting missing: "+name,failures); + } + slimeknights.tconstruct.library.smeltery.MeltingRecipe melting = + slimeknights.tconstruct.library.TinkerRegistry.getMelting(material.getItemStack(Names.INGOT)); + require(melting!=null&&melting.getResult().getFluid()==fluid&&melting.getResult().amount==144, + "Ingot melting missing: "+name,failures); + } + } + + private static boolean hasCasting(java.util.List recipes, + ItemStack cast, ItemStack output, net.minecraftforge.fluids.Fluid fluid, int amount) { + return recipes.stream().anyMatch(recipe -> recipe.matches(cast,fluid) + && recipe.getFluidAmount()==amount + && ItemStack.areItemsEqual(recipe.getResult(cast,fluid),output)); + } + } +} diff --git a/tools/validation/legacy112/runtime-probe/review/minerals/ClientChecks.java b/tools/validation/legacy112/runtime-probe/review/minerals/ClientChecks.java new file mode 100644 index 0000000..3206d11 --- /dev/null +++ b/tools/validation/legacy112/runtime-probe/review/minerals/ClientChecks.java @@ -0,0 +1,102 @@ +package review.minerals; + +import com.google.gson.*; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Future; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.*; +import net.minecraft.item.ItemStack; +import net.minecraft.world.*; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.client.FMLClientHandler; +import net.minecraftforge.fml.client.IModGuiFactory; +import net.minecraftforge.fml.client.config.GuiConfig; +import net.minecraftforge.fml.client.config.GuiConfigEntries.BooleanEntry; +import net.minecraftforge.fml.client.config.IConfigElement; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.Mod; +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.baseminerals.util.BMiConfig; +import zone.moddev.mc.orespawn.api.client.*; + +/** Automated UI checks, separate from the distributable mod and manual acceptance. */ +@Mod(modid="basemineralsclientprobe",name="Base Minerals client checks",version="1",acceptableRemoteVersions="*",dependencies="required-after:basemineralsprobe") +public final class ClientChecks { + @Mod.EventHandler public void init(FMLInitializationEvent event){if(Boolean.getBoolean("minerals.client"))MinecraftForge.EVENT_BUS.register(new Checks());} + static void require(boolean value,String detail){if(!value)throw new AssertionError(detail);} + public static final class Checks { + long started=System.currentTimeMillis();int state,ticks;boolean finished;GuiScreen parent;GuiConfig screen;Future check;net.minecraft.server.MinecraftServer integrated; + @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) throws Exception { + if(event.phase!=TickEvent.Phase.END||finished)return;Minecraft mc=Minecraft.getMinecraft(); + try { + if(state==0&&mc.currentScreen instanceof GuiMainMenu&&++ticks>=20) { + parent=mc.currentScreen; + IModGuiFactory factory=FMLClientHandler.instance().getGuiFactoryFor(Loader.instance().getIndexedModList().get("baseminerals")); + require(factory!=null&&factory.hasConfigGui(),"Forge config factory missing"); + screen=(GuiConfig)factory.createConfigGui(parent);mc.displayGuiScreen(screen);state=1;ticks=0; + } else if(state==1&&++ticks>=20) { + verifyConfig(screen,parent); + screenshot(mc,"forge-mods-config.png");entry(screen).valueButtonPressed(0); + invoke(screen,"keyTyped",new Class[]{char.class,int.class},'\0',1); + require(mc.currentScreen==parent,"Escape lost parent");require(BMiConfig.getConfiguration().get("Metals","EnableLithium",true).getBoolean(),"Escape saved a cancelled switch"); + WorldSettingsExtension extension=WorldSettingsExtensionRegistry.extensions().stream().filter(e->"baseminerals".equals(e.id().getNamespace())).findFirst().orElse(null); + require(extension!=null,"OreSpawn config entry missing");screen=(GuiConfig)extension.createScreen(parent);mc.displayGuiScreen(screen);state=2;ticks=0; + } else if(state==2&&++ticks>=20) { + verifyConfig(screen,parent);screenshot(mc,"orespawn-mods-config.png"); + entry(screen).valueButtonPressed(0);invoke(screen,"actionPerformed",new Class[]{GuiButton.class},new GuiButton(2000,0,0,"Done")); + require(!BMiConfig.getConfiguration().get("Metals","EnableLithium",true).getBoolean(),"Done did not save requested switch"); + require(BMiConfig.isEnabled("lithium")&&com.mcmoddev.lib.init.Materials.hasMaterial("lithium"),"GUI changed active registries"); + require(new String(Files.readAllBytes(Paths.get("config/BaseMinerals.cfg")),StandardCharsets.UTF_8).contains("B:EnableLithium=false"),"Switch not persisted"); + screen=new zone.moddev.mc.baseminerals.client.BaseMineralsConfigScreen(parent);mc.displayGuiScreen(screen);entry(screen).valueButtonPressed(0); + invoke(screen,"actionPerformed",new Class[]{GuiButton.class},new GuiButton(2000,0,0,"Done"));mc.displayGuiScreen(parent); + mc.launchIntegratedServer("minerals-check","Base Minerals disposable checks",new WorldSettings(8675309L,GameType.CREATIVE,false,false,WorldType.DEFAULT));state=3; + } else if(state==3&&mc.world!=null&&mc.player!=null&&check==null) { + for(net.minecraft.block.Block block:net.minecraftforge.fml.common.registry.ForgeRegistries.BLOCKS.getValues()) { + if(!"baseminerals".equals(block.getRegistryName().getNamespace()))continue; + for(net.minecraft.block.state.IBlockState variant:block.getBlockState().getValidStates()) + require(mc.getBlockRendererDispatcher().getBlockModelShapes().getModelForState(variant)!=mc.getRenderItem().getItemModelMesher().getModelManager().getMissingModel(),"Missing baked block: "+variant); + } + for(net.minecraft.item.Item item:net.minecraftforge.fml.common.registry.ForgeRegistries.ITEMS.getValues()) { + if(!"baseminerals".equals(item.getRegistryName().getNamespace()))continue; + require(mc.getRenderItem().getItemModelMesher().getItemModel(new ItemStack(item))!=mc.getRenderItem().getItemModelMesher().getModelManager().getMissingModel(),"Missing baked item: "+item.getRegistryName()); + } + OreModelChecks.verify(mc); + check=mc.getIntegratedServer().addScheduledTask(()->{try{BaseMineralsProbe.run(mc.getIntegratedServer());}catch(Exception e){throw new RuntimeException(e);}}); + } else if(state==3&&check!=null&&check.isDone()) { + check.get();screenshot(mc,"integrated-world.png");integrated=mc.getIntegratedServer();integrated.initiateShutdown();state=4; + } else if(state==4&&integrated.isServerStopped()) { + report(true,"Config save/cancel/restart, both parents, baked models and integrated saved stacks");finished=true;mc.shutdown(); + } + if(System.currentTimeMillis()-started>240000)throw new AssertionError("Client timeout, state="+state); + } catch(Throwable failure){report(false,failure.toString());failure.printStackTrace();finished=true;mc.shutdown();} + } + static void verifyConfig(GuiConfig config,GuiScreen expectedParent) { + require(config.parentScreen==expectedParent,"Config opened with the wrong parent"); + require(config.allRequireMcRestart,"Switches must require restart"); + Set names=new HashSet<>(); + for(IConfigElement element:config.configElements) { + names.add(element.getName()); + require(net.minecraft.client.resources.I18n.hasKey(element.getLanguageKey()),"Untranslated switch: "+element.getName()); + } + require(names.size()==11&&names.contains("Enabled Lithium")&&names.contains("Enabled Silicon"), + "Unexpected 1.12 switches: "+names); + } + static BooleanEntry entry(GuiConfig config){for(int i=0;i[] types,Object...args)throws Exception { + Method method;try{method=GuiConfig.class.getDeclaredMethod(name,types);}catch(NoSuchMethodException e){method=GuiConfig.class.getDeclaredMethod("keyTyped".equals(name)?"func_73869_a":"func_146284_a",types);} + method.setAccessible(true);method.invoke(config,args); + } + static void screenshot(Minecraft mc,String name){net.minecraft.util.ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.displayWidth,mc.displayHeight,mc.getFramebuffer());} + static void report(boolean pass,String detail)throws Exception { + JsonObject result=new JsonObject();result.addProperty("passed",pass);result.addProperty("detail",detail); + Files.write(Paths.get(System.getProperty("minerals.phase")+"-client.json"),(new GsonBuilder().setPrettyPrinting().create().toJson(result)+"\n").getBytes(StandardCharsets.UTF_8)); + System.out.println("BASE_MINERALS_CLIENT_"+(pass?"PASS":"FAIL")+" "+detail); + } + } +} diff --git a/tools/validation/legacy112/runtime-probe/review/minerals/LegacyFixtureGenerator.java b/tools/validation/legacy112/runtime-probe/review/minerals/LegacyFixtureGenerator.java new file mode 100644 index 0000000..2ec99f3 --- /dev/null +++ b/tools/validation/legacy112/runtime-probe/review/minerals/LegacyFixtureGenerator.java @@ -0,0 +1,27 @@ +package review.minerals; + +import java.nio.file.*; +import org.objectweb.asm.*; + +/** Creates a compiled old-namespace consumer without bundling a second library. */ +public final class LegacyFixtureGenerator implements Opcodes { + public static void main(String[] arguments) throws Exception { + ClassWriter writer=new ClassWriter(0); + writer.visit(V1_8,ACC_PUBLIC,"review/minerals/LegacyConsumer",null,"com/mcmoddev/baseminerals/proxy/CommonProxy",null); + MethodVisitor method=writer.visitMethod(ACC_PUBLIC|ACC_STATIC,"material","()Ljava/lang/Object;",null,null); + method.visitCode();method.visitLdcInsn("lithium"); + method.visitMethodInsn(INVOKESTATIC,"com/mcmoddev/baseminerals/init/Materials","getMaterialByName","(Ljava/lang/String;)Lcom/mcmoddev/lib/material/MMDMaterial;",false); + method.visitInsn(ARETURN);method.visitMaxs(1,0);method.visitEnd(); + method=writer.visitMethod(ACC_PUBLIC|ACC_STATIC,"proxy","()Ljava/lang/Object;",null,null); + method.visitCode();method.visitFieldInsn(GETSTATIC,"com/mcmoddev/baseminerals/BaseMinerals","proxy","Lcom/mcmoddev/baseminerals/proxy/CommonProxy;"); + method.visitInsn(ARETURN);method.visitMaxs(1,0);method.visitEnd(); + method=writer.visitMethod(ACC_PUBLIC|ACC_STATIC,"reflected","()Ljava/lang/Class;",null,new String[]{"java/lang/ClassNotFoundException"}); + method.visitCode();method.visitLdcInsn("com.mcmoddev.baseminerals.BaseMinerals"); + method.visitMethodInsn(INVOKESTATIC,"java/lang/Class","forName","(Ljava/lang/String;)Ljava/lang/Class;",false); + method.visitInsn(ARETURN);method.visitMaxs(1,0);method.visitEnd();writer.visitEnd(); + Path file=Paths.get(arguments[0],"review/minerals/LegacyConsumer.class");Files.createDirectories(file.getParent());Files.write(file,writer.toByteArray()); + ClassWriter config=new ClassWriter(0); + config.visit(V1_8,ACC_PUBLIC,"review/minerals/LegacyConfiguration",null,"com/mcmoddev/baseminerals/util/BMiConfig",null);config.visitEnd(); + Files.write(file.resolveSibling("LegacyConfiguration.class"),config.toByteArray()); + } +} diff --git a/tools/validation/legacy112/runtime-probe/review/minerals/OreModelChecks.java b/tools/validation/legacy112/runtime-probe/review/minerals/OreModelChecks.java new file mode 100644 index 0000000..8e3d9bd --- /dev/null +++ b/tools/validation/legacy112/runtime-probe/review/minerals/OreModelChecks.java @@ -0,0 +1,86 @@ +package review.minerals; + +import com.mcmoddev.lib.init.Materials; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.IBakedModel; +import net.minecraft.client.resources.IResource; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.BlockRenderLayer; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import net.minecraftforge.oredict.OreDictionary; +import zone.moddev.mc.baseminerals.util.BMiConfig; + +import static review.minerals.ClientChecks.require; + +/** Checks the texture and geometry of the baked block and inventory models. */ +final class OreModelChecks { + static void verify(Minecraft minecraft) throws IOException { + int overlays = 0; + int solids = 0; + for (String material : BaseMineralsProbe.ORES) { + if (!BMiConfig.isEnabled(material)) continue; + String name = material + "_ore"; + ResourceLocation id = new ResourceLocation("baseminerals", name); + Block ore = ForgeRegistries.BLOCKS.getValue(id); + require(ore != null && id.equals(ore.getRegistryName()), "Wrong ore block ID: " + id); + Item item = Item.getItemFromBlock(ore); + require(id.equals(item.getRegistryName()), "Wrong ore item ID: " + id); + String oreKey = "ore" + Materials.getMaterialByName(material).getCapitalizedName(); + require(OreDictionary.getOres(oreKey).stream().anyMatch(stack -> stack.getItem() == item), + "Missing Ore Dictionary entry: " + oreKey); + require(ore.getRenderLayer() == BlockRenderLayer.CUTOUT_MIPPED, "Wrong ore render layer: " + id); + + String overlay = "baseminerals:blocks/ore_overlays/" + name; + boolean layered = hasOverlay(minecraft, name); + String base = layered ? "minecraft:blocks/stone" : "baseminerals:blocks/" + name; + IBlockState state = ore.getDefaultState(); + IBakedModel placed = minecraft.getBlockRendererDispatcher().getBlockModelShapes().getModelForState(state); + IBakedModel inventory = minecraft.getRenderItem().getItemModelMesher().getItemModel(new ItemStack(item)); + verifyModel(placed, state, base, layered ? overlay : null); + verifyModel(inventory, null, base, layered ? overlay : null); + if (layered) overlays++; + else solids++; + } + System.out.println("BASE_MINERALS_ORE_MODELS_PASS solids=" + solids + " overlays=" + overlays); + } + + private static boolean hasOverlay(Minecraft minecraft, String name) throws IOException { + ResourceLocation path = new ResourceLocation("baseminerals", "textures/blocks/ore_overlays/" + name + ".png"); + try (IResource resource = minecraft.getResourceManager().getResource(path)) { + return true; + } catch (FileNotFoundException missing) { + return false; + } + } + + private static void verifyModel(IBakedModel model, IBlockState state, String base, String overlay) { + require(base.equals(model.getParticleTexture().getIconName()), "Wrong ore particles: " + base); + for (EnumFacing face : EnumFacing.values()) { + List quads = model.getQuads(state, face, 0); + require(quads.size() == (overlay == null ? 1 : 2), "Wrong ore face count: " + base + " " + face); + verifyFace(quads, face, base, 0.0F); + if (overlay != null) verifyFace(quads, face, overlay, 0.05F / 16.0F); + } + } + + private static void verifyFace(List quads, EnumFacing face, String texture, float offset) { + BakedQuad quad = quads.stream().filter(candidate -> texture.equals(candidate.getSprite().getIconName())) + .findFirst().orElseThrow(() -> new AssertionError("Missing ore sprite: " + texture)); + int axis = face.getAxis().ordinal(); + float expected = face.getAxisDirection() == EnumFacing.AxisDirection.POSITIVE ? 1.0F + offset : -offset; + int[] data = quad.getVertexData(); + for (int vertex = 0; vertex < 4; vertex++) { + float depth = Float.intBitsToFloat(data[vertex * (data.length / 4) + axis]); + require(Math.abs(depth - expected) < 0.0001F, "Wrong ore face depth: " + texture + " " + face); + } + } +} diff --git a/tools/validation/legacy112/runtime-probe/review/minerals/RecordingPatterns.java b/tools/validation/legacy112/runtime-probe/review/minerals/RecordingPatterns.java new file mode 100644 index 0000000..512ba58 --- /dev/null +++ b/tools/validation/legacy112/runtime-probe/review/minerals/RecordingPatterns.java @@ -0,0 +1,41 @@ +package review.minerals; + +import com.google.gson.*; +import java.util.*; +import net.minecraft.util.ResourceLocation; +import zone.moddev.mc.orespawn.api.*; + +/** Test-only counting wrapper; delegates placement to the published built-in codecs. */ +final class RecordingPatterns { + private static final Map COUNTS=new LinkedHashMap<>(); + private static final Map SETTINGS=new LinkedHashMap<>(); + static void register() { + for(String name:BaseMineralsProbe.ORES) { + OrePatternType type=OrePatternType.create(StandardPatternSettings.CODEC,settings->{ + JsonObject json=new JsonObject();json.addProperty("spread",settings.spread());json.addProperty("vertical_spread",settings.verticalSpread()); + json.addProperty("node_size",settings.nodeSize());json.addProperty("length",settings.length()); + SETTINGS.put(name,new JsonParser().parse(json.toString()).getAsJsonObject()); + CompiledOrePattern delegate=OreSpawnPatternRegistry.registry().getValue(new ResourceLocation("orespawn","lithium".equals(name)?"vein":"clusters")).decode(json); + return context->{ + int cx=Math.floorDiv(context.originX(),16),cz=Math.floorDiv(context.originZ(),16); + if(cx>=64&&cx<80&&cz>=64&&cz<80) { + Stats stats=COUNTS.computeIfAbsent(name,key->new Stats());stats.attempts++;stats.heights[context.originY()]++; + stats.min=Math.min(stats.min,context.quantity());stats.max=Math.max(stats.max,context.quantity()); + } + return delegate.place(context); + }; + }); + type.setRegistryName(new ResourceLocation("basemineralsprobe",name));OreSpawnPatternRegistry.registry().register(type); + } + } + static JsonObject report(JsonArray failures) { + JsonObject result=new JsonObject(); + for(Map.Entry entry:COUNTS.entrySet()) { + Stats stats=entry.getValue();JsonObject value=new JsonObject();value.addProperty("attempts",stats.attempts);value.addProperty("minQuantity",stats.min);value.addProperty("maxQuantity",stats.max); + JsonArray heights=new JsonArray();for(int n:stats.heights)heights.add(new JsonPrimitive(n));value.add("heights",heights);result.add(entry.getKey(),value); + value.add("settings",SETTINGS.get(entry.getKey())); + } + return result; + } + private static final class Stats {int attempts,min=Integer.MAX_VALUE,max;final int[] heights=new int[256];} +} diff --git a/tools/validation/requirements.txt b/tools/validation/requirements.txt new file mode 100644 index 0000000..ad5d466 --- /dev/null +++ b/tools/validation/requirements.txt @@ -0,0 +1,2 @@ +jsonschema==4.23.0 +PyYAML==6.0.3