diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c75e8c..db34f62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,61 +1,73 @@ -name: Base Gems 1.12 CI +name: Base Gems 1.13.2 CI on: push: - branches: - - master-1.12 - - 'feature/**' + branches: [master-1.13.2, 'feature/**'] pull_request: - branches: - - master-1.12 + branches: [master-1.13.2] permissions: contents: read concurrency: - group: basegems-1.12-${{ github.workflow }}-${{ github.ref }} + group: basegems-113-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: - name: Build and verify + name: Build, test, and audit runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 35 steps: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Validate the Gradle wrapper uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - - name: Install Java 25 for Mavenizer uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - - name: Install Java 8 for Forge + - name: Install Java 8 for compilation uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '17.0.1+12' - - - name: Stage checksum-verified dependencies + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Stage checksum-verified published dependencies run: python3 gradle/stage-legacy-dependencies.py .gradle/legacy-dependencies - - - name: Build, audit jars and generate Eclipse launches - shell: bash + - name: Build, audit and generate Eclipse launches run: | - set -euo pipefail - ./gradlew check build verifyReleaseArtifacts writeReleaseChecksums prepareEclipse \ - -PreleaseBuild=true --no-daemon --stacktrace --max-workers=2 \ + bash ./gradlew clean check build javadoc verify113ReleaseArtifacts write113ReleaseChecksums prepareEclipse \ + -PreleaseBuild=true -PlegacyDependencyVerificationRepository="$PWD/.gradle/legacy-dependencies" \ + --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 - sha256sum build/libs/*.jar + - name: Stage the flat release bundle + run: | + mkdir -p build/release-bundle + cp build/libs/*.jar build/release/SHA256SUMS CHANGELOG.txt build/release-bundle/ + - name: Upload audited release candidate + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: BaseGems-1.13.2-${{ github.sha }} + if-no-files-found: error + retention-days: 30 + path: build/release-bundle/ + - name: Retain failure diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: BaseGems-1.13.2-diagnostics-${{ github.sha }} + if-no-files-found: ignore + retention-days: 14 + path: | + build/test-results/** + build/reports/** diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index f00287b..54ff227 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -1,15 +1,19 @@ -name: Release Base Gems +name: Release Base Gems 1.13.2 run-name: Base Gems ${{ inputs.release_version }} (${{ inputs.confirm_live_publication && 'publish' || 'verify' }}) on: workflow_dispatch: inputs: release_version: - description: Base Gems version for Minecraft 1.11.2 + description: Four-part version for Minecraft 1.13.2 + required: true + type: string + release_commit: + description: Full SHA of the checked MMD master-1.13.2 commit required: true type: string tested_jar_sha256: - description: SHA-256 of the game jar that passed manual testing + description: SHA-256 of the qualified game jar required: true type: string curseforge_release_level: @@ -17,14 +21,11 @@ on: required: true default: release type: choice - options: - - release - - beta - - alpha + options: [release, beta, alpha] confirm_live_publication: - description: Publish to CurseForge and GitHub after verification - default: false + description: Publish the verified CI files to Maven, CurseForge and GitHub required: true + default: false type: boolean permissions: @@ -32,130 +33,207 @@ permissions: contents: read concurrency: - group: basegems-release-1.11.2 + group: basegems-release-113 cancel-in-progress: false jobs: - release: - name: Verify merged CI bundle and release - if: github.repository == 'MinecraftModDevelopmentMods/BaseGems' && github.ref == 'refs/heads/master-1.12' + qualify: + name: Verify merged CI candidate + if: github.repository == 'MinecraftModDevelopmentMods/BaseGems' && github.ref == 'refs/heads/master-1.13.2' runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 10 permissions: actions: read contents: write - env: - RELEASE_BRANCH: master-1.11.2 - RELEASE_VERSION: ${{ inputs.release_version }} - TESTED_JAR_SHA256: ${{ inputs.tested_jar_sha256 }} + outputs: + ci_run: ${{ steps.candidate.outputs.ci_run }} steps: - - name: Locate the release bundle for the current MMD branch tip + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ inputs.release_commit }} + fetch-depth: 0 + - name: Verify branch, version and successful merged CI id: candidate env: GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.release_version }} + RELEASE_SHA: ${{ inputs.release_commit }} + TESTED_SHA: ${{ inputs.tested_jar_sha256 }} run: | set -euo pipefail - [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.111021$ ]] - [[ "$TESTED_JAR_SHA256" =~ ^[[:xdigit:]]{64}$ ]] - - sha="$(gh api "repos/$GITHUB_REPOSITORY/branches/$RELEASE_BRANCH" --jq .commit.sha)" - props="$(gh api "repos/$GITHUB_REPOSITORY/contents/gradle.properties?ref=$sha" --jq .content | base64 --decode)" - grep -Fxq "mod_version=$RELEASE_VERSION" <<<"$props" - grep -Fxq 'minecraft_version=1.11.2' <<<"$props" - grep -Fxq 'loader_name=forge' <<<"$props" - - run="$(gh api --method GET "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" \ - -f branch="$RELEASE_BRANCH" -f head_sha="$sha" -f event=push -f status=success \ + [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.113021$ ]] + [[ "$RELEASE_SHA" =~ ^[[:xdigit:]]{40}$ ]] + [[ "$TESTED_SHA" =~ ^[[:xdigit:]]{64}$ ]] + test "$(git rev-parse HEAD)" = "$RELEASE_SHA" + test "$(gh api "repos/$GITHUB_REPOSITORY/branches/master-1.13.2" --jq .commit.sha)" = "$RELEASE_SHA" + grep -Fxq "mod_version=$RELEASE_VERSION" gradle.properties + grep -Fxq 'minecraft_version=1.13.2' gradle.properties + ci_run="$(gh api --method GET "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" \ + -f branch=master-1.13.2 -f head_sha="$RELEASE_SHA" -f event=push -f status=success \ --jq '.workflow_runs[0].id // empty')" - if [[ -z "$run" ]]; then - echo "No successful push CI run for $RELEASE_BRANCH at $sha" >&2 - exit 1 - fi - echo "sha=$sha" >> "$GITHUB_OUTPUT" - echo "run=$run" >> "$GITHUB_OUTPUT" - - - name: Download the audited CI artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + test -n "$ci_run" + echo "ci_run=$ci_run" >> "$GITHUB_OUTPUT" + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: BaseGems-1.11.2-${{ steps.candidate.outputs.sha }} - path: ${{ runner.temp }}/release-input - run-id: ${{ steps.candidate.outputs.run }} + name: BaseGems-1.13.2-${{ inputs.release_commit }} + run-id: ${{ steps.candidate.outputs.ci_run }} github-token: ${{ github.token }} - - - name: Verify jar checksums and manual test match + path: ${{ runner.temp }}/release-input + - name: Verify sealed files and tested game jar working-directory: ${{ runner.temp }}/release-input env: - RELEASE_SHA: ${{ steps.candidate.outputs.sha }} - CI_RUN: ${{ steps.candidate.outputs.run }} + RELEASE_VERSION: ${{ inputs.release_version }} + TESTED_SHA: ${{ inputs.tested_jar_sha256 }} run: | set -euo pipefail - diff -u SHA256SUMS <(sha256sum --binary "BaseGems-$RELEASE_VERSION"{,-sources,-javadoc}.jar | sed 's/ \*/ /') - printf '%s BaseGems-%s.jar\n' "${TESTED_JAR_SHA256,,}" "$RELEASE_VERSION" | sha256sum --check --strict - grep -Fxq "$RELEASE_VERSION for Minecraft 1.11.2" CHANGELOG.txt - { - echo "Verified $RELEASE_VERSION at $RELEASE_SHA from CI run $CI_RUN." - echo - echo 'The game jar matches the manually tested SHA-256.' - } >> "$GITHUB_STEP_SUMMARY" - - - name: Verify CurseForge upload token + python3 - "$RELEASE_VERSION" "$TESTED_SHA" <<'PY' + from pathlib import Path + import hashlib, sys + version, tested = sys.argv[1:] + names = [f'BaseGems-{version}{suffix}.jar' for suffix in ('', '-sources', '-javadoc')] + assert {p.name for p in Path('.').iterdir()} == set(names + ['SHA256SUMS', 'CHANGELOG.txt']) + actual = {name: hashlib.sha256(Path(name).read_bytes()).hexdigest() for name in names} + declared = dict((name, sha) for sha, name in (line.split(' ', 1) for line in Path('SHA256SUMS').read_text().splitlines())) + assert actual == declared + assert actual[names[0]] == tested.lower() + assert version in Path('CHANGELOG.txt').read_text().splitlines()[0] + PY + - name: Verify live publication credentials + if: inputs.confirm_live_publication env: CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }} + MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }} + MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }} run: | set -euo pipefail - if [[ -z "$CURSEFORGE_TOKEN" ]]; then - echo 'Configure CURSEFORGE_TOKEN in this repository or share the organization secret with it.' >&2 - exit 1 - fi + for name in CURSEFORGE_TOKEN MAVEN_UPLOAD_URL MAVEN_UPLOAD_USERNAME MAVEN_UPLOAD_PASSWORD; do + test -n "${!name}" || { echo "Configure $name for this repository" >&2; exit 1; } + done + [[ "$MAVEN_UPLOAD_URL" == https://* ]] curl --fail --silent --show-error --max-time 30 \ - -H "X-Api-Token: $CURSEFORGE_TOKEN" \ - https://minecraft.curseforge.com/api/game/versions >/dev/null - - - name: Reserve the release tag + -H "X-Api-Token: $CURSEFORGE_TOKEN" https://minecraft.curseforge.com/api/game/versions >/dev/null + - name: Reserve the version for this commit if: inputs.confirm_live_publication env: GH_TOKEN: ${{ github.token }} - RELEASE_SHA: ${{ steps.candidate.outputs.sha }} + RELEASE_VERSION: ${{ inputs.release_version }} + RELEASE_SHA: ${{ inputs.release_commit }} run: | set -euo pipefail - if [[ "$(gh api "repos/$GITHUB_REPOSITORY/branches/$RELEASE_BRANCH" --jq .commit.sha)" != "$RELEASE_SHA" ]]; then - echo "$RELEASE_BRANCH moved during verification. Dispatch again for the new tip." >&2 - exit 1 - fi - if ! gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ - -f ref="refs/tags/$RELEASE_VERSION" -f sha="$RELEASE_SHA" >/dev/null; then - echo 'Tag creation failed. If the tag already exists, inspect the earlier release before trying again.' >&2 - exit 1 + if existing="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_VERSION" --jq .object.sha 2>/dev/null)"; then + test "$existing" = "$RELEASE_SHA" + else + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$RELEASE_VERSION" -f sha="$RELEASE_SHA" >/dev/null fi - - name: Publish on CurseForge - if: inputs.confirm_live_publication + publish_maven: + name: Publish MMD Maven + needs: qualify + if: inputs.confirm_live_publication + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ inputs.release_commit }} + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.13.2-${{ inputs.release_commit }} + run-id: ${{ needs.qualify.outputs.ci_run }} + github-token: ${{ github.token }} + path: ${{ runner.temp }}/release-input + - name: Stage checksum-verified published dependencies + run: python3 gradle/stage-legacy-dependencies.py .gradle/legacy-dependencies + - name: Publish the sealed Maven jars + env: + MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }} + MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }} + MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }} + run: | + bash ./gradlew publishMavenJavaPublicationToReleaseRepository \ + -PpreparedReleaseDir="${{ runner.temp }}/release-input" -PlegacyDependencyVerificationRepository="$PWD/.gradle/legacy-dependencies" \ + --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 + + publish_curseforge: + name: Publish CurseForge + needs: [qualify, publish_maven] + if: inputs.confirm_live_publication + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.13.2-${{ inputs.release_commit }} + run-id: ${{ needs.qualify.outputs.ci_run }} + github-token: ${{ github.token }} + path: ${{ runner.temp }}/release-input + - name: Verify the sealed jars + working-directory: ${{ runner.temp }}/release-input + run: sha256sum --check --strict SHA256SUMS + - name: Publish Base Gems uses: Kira-NT/mc-publish@52307b03863581dec6b652b83e597aec02ebb075 # v3.3.1 with: curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} curseforge-id: '1793939' - name: Base Gems ${{ inputs.release_version }} for Minecraft 1.11.2 (Forge) + name: Base Gems ${{ inputs.release_version }} for Minecraft 1.13.2 (Forge) version: ${{ inputs.release_version }} version-type: ${{ inputs.curseforge_release_level }} changelog-file: ${{ runner.temp }}/release-input/CHANGELOG.txt loaders: forge - game-versions: '1.11.2' + game-versions: '1.13.2' game-version-filter: none environment: client | server java: Java 8 curseforge-dependencies: | - 240967(required) 245586(required) + 240967(optional) + 244357(optional) fail-mode: fail - files: ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}.jar + files: | + ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}.jar + ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}-sources.jar + ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}-javadoc.jar - - name: Publish GitHub Release - if: inputs.confirm_live_publication + publish_github: + name: Publish GitHub Release + needs: [qualify, publish_curseforge] + if: inputs.confirm_live_publication + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.13.2-${{ inputs.release_commit }} + run-id: ${{ needs.qualify.outputs.ci_run }} + github-token: ${{ github.token }} + path: ${{ runner.temp }}/release-input + - name: Create the GitHub release from the sealed files working-directory: ${{ runner.temp }}/release-input env: GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.release_version }} run: | set -euo pipefail + sha256sum --check --strict SHA256SUMS gh release create "$RELEASE_VERSION" --repo "$GITHUB_REPOSITORY" --verify-tag \ - --title "Base Gems $RELEASE_VERSION for Minecraft 1.11.2 (Forge)" \ + --title "Base Gems $RELEASE_VERSION for Minecraft 1.13.2 (Forge)" \ --notes-file CHANGELOG.txt "BaseGems-$RELEASE_VERSION"{,-sources,-javadoc}.jar SHA256SUMS diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml new file mode 100644 index 0000000..caa48fd --- /dev/null +++ b/.github/workflows/release-on-tag.yml @@ -0,0 +1,20 @@ +name: Validate Base Gems 1.13.2 tag +on: + push: + tags: ['*.*.*.113021'] +permissions: + contents: read + actions: read +jobs: + validate: + if: github.repository == 'MinecraftModDevelopmentMods/BaseGems' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Check the tag against the release metadata + run: | + set -euo pipefail + test "$GITHUB_REF_NAME" = "$(sed -n 's/^mod_version=//p' gradle.properties)" + grep -Fxq 'minecraft_version=1.13.2' gradle.properties + echo 'Tag validated. Publication uses deploy-release.yml with explicit confirmation.' >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 3f67a97..4dc0c8f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,13 @@ run /README.txt /forge-*-changelog.txt -secret.json \ No newline at end of file +secret.json + +# Local agent context and verification evidence +/AGENTS.md +/agent-notes/ +/.codex/ +/.verification*/ +/release-candidates/ +__pycache__/ +*.pyc diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4debee7..c9806ff 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,22 @@ +2.5.0.113021 for Minecraft 1.13.2 + +- Pin optional Base Metals compatibility to 3.0.3, with its shared processing and mining fixes. +- Restore the 1.12 mining requirements for gem ores and blocks, separately from gem tool strength. +- Use released OreSpawn 4.1.7, including the generation-boundary fix. +- Fix missing fluids and startup hangs with other fluid-adding mods during parallel loading. + +All 33 gems come across to Forge 1.13.2. OreSpawn still handles deposits, +and gems still give mining XP, turn up in smith trades and teach recipes +when you pick them up. + +- Base Gems reads the original Forge registry when upgrading a world from + 1.10.2, 1.11.2 or 1.12.2. Old block metadata becomes named states, and + filled buckets and worn equipment keep their contents and durability. +- Moved recipes and advancements to data packs, language files to JSON and + Ore Dictionary entries to tags. The existing gem block and item names stay. +- Base Gems no longer needs MMDLib on this version. Fantasy Level and gem + switches are available through Forge and OreSpawn. + 2.5.0.112021 for Minecraft 1.12.2 All 33 gems come across from 1.11.2, along with the OreSpawn deposits, mining diff --git a/README.md b/README.md index ea0a1a6..a57ec5d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Base Gems for Minecraft 1.12.2 +# Base Gems for Minecraft 1.13.2 Base Gems adds 33 gemstones to Minecraft. Mine agate and amethyst in upper caves, look deeper for ruby and sapphire, or head down near bedrock for black @@ -7,22 +7,41 @@ pieces, tools, weapons and shields. ## Installation -Use Java 8 and Forge **14.23.5.2859 or later for Minecraft 1.12.2**. Put these -jars in the `mods` folder on both the client and server. +Use Java 8 and Forge **25.0.223 for Minecraft 1.13.2** on both client and +server. Base Gems needs MMD OreSpawn and runs without MMDLib. -- Base Gems `2.5.0.112021`. -- [MMDLib 1.0.0 RC3](https://www.curseforge.com/minecraft/mc-mods/mmdlib/files/9414723). -- [MMD OreSpawn 4.1.6.112021](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9527784), - or a compatible 4.x update. +Use [MMD OreSpawn 4.1.7.113021](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9611746) +or a later compatible 1.13.2 release. It includes the fix for checking cave air +at generation boundaries; no separate local engine is needed. -You can play with just those three mods. If you also use Base Metals, this -build was checked with [Base Metals 2.6.2.112021](https://www.curseforge.com/minecraft/mc-mods/base-metals/files/9532211). +Base Metals is optional for a new world. When installed together, use +[Base Metals 3.0.3.113021](https://github.com/MinecraftModDevelopmentMods/BaseMetals/releases/tag/3.0.3.113021) +or a later compatible 1.13.2 release. If your old +world contains Base Metals blocks or items, keep it installed for the upgrade. + +Gem crushing works with the Base Gems crackhammer on its own. With Base +Metals installed its tools and crushers can use the same recipes. With +Modern Metals alone, its crackhammers can use them too. + +## Upgrading a world + +Make a separate copy before opening a 1.10.2, 1.11.2 or 1.12.2 world in +1.13.2. Keep the original Forge `level.dat` with the copy. Base Gems uses +that save's registry to restore the gems and their block states during +Minecraft's flattening. [World upgrade details](docs/MIGRATION.md) ## Mining and trading +Use a tool strong enough for the gem. Agate and jade need stone level; +jasper, ruby and sapphire need iron level; black diamond needs diamond level. +Modern Metals' galvanized steel, stainless steel and titanium hammers are +iron level. A weaker tool breaks the ore +without a drop. These are the same mining requirements as in 1.12. + Gem ores give XP when mined. Fortune can increase the gem yield, and Silk Touch lets you take the ore block home. Smiths buy and sell gems for emeralds, with rarer gems appearing as their careers advance. +In High Fantasy, toolsmiths and weaponsmiths also sell gem equipment. OreSpawn handles placement in the Overworld. Amber and moldavite turn up in shallow sediment pockets, while gems such as topaz and beryl tend to appear @@ -37,10 +56,12 @@ changes stone, the background of the gem ores changes with it. Pick up a gem to learn what you can make with it in the recipe book. Some recipes appear when you pick up a processed part, such as a gem plate. The recipes you learn follow your Fantasy Level and gem settings. +You can smelt gem ore or powder in a furnace to get the gem back. Open **Mods → Base Gems → Config** or choose **Base Gems** from OreSpawn's -**Mods** list. Both screens save to `config/BaseGems.cfg`. Restart the game or -server after changing Fantasy Level or switching a gem on or off. +**Mods** list. Both screens save to `config/basegems-common.toml`. Restart the +game or server after changing Fantasy Level or switching a gem on or off. +Gem switches keep existing blocks and items available in old worlds. **High Fantasy** is the default and includes gem tools, weapons, shields and decorative pieces. Player and horse armour are left to add-ons. @@ -51,13 +72,15 @@ client in multiplayer. ## Mod compatibility -Other mods can use the Ore Dictionary names, such as `gemRuby`, `oreRuby`, -`blockRuby`, `dustRuby` and `gearRuby`. The block and item IDs, config keys -and Ore Dictionary names carry over from 1.11.2. +The Ore Dictionary names have moved to tags such as `forge:gems/ruby`, +`forge:ores/ruby`, `forge:storage_blocks/ruby`, `forge:dusts/ruby` and +`forge:gears/ruby`. The existing Base Gems block and item names carry over. +The older MMDLib machine integrations, equipment recycling recipes and shield +upgrade recipes are not part of this port. ## Development and credits -The 1.12.2 branch uses ForgeGradle 7 and builds a Java 8 mod. +The 1.13.2 branch uses ForgeGradle 7 and builds a Java 8 mod. See [building and Eclipse setup](docs/BUILDING.md) and the [changelog](CHANGELOG.txt). diff --git a/build.gradle b/build.gradle index 0c2480b..a32a511 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ import org.apache.tools.ant.filters.FixCrLfFilter plugins { id 'java' + id 'maven-publish' id 'eclipse' id 'idea' id 'net.minecraftforge.renamer' version '1.1.5' @@ -12,8 +13,8 @@ group = project.mod_group version = project.mod_version base.archivesName = 'BaseGems' -if (!(project.version ==~ /\d+\.\d+\.\d+\.112021/)) { - throw new GradleException('Use Major.Minor.Patch.112021 for Minecraft 1.12.2 on Forge.') +if (!(project.version ==~ /\d+\.\d+\.\d+\.113021/)) { + throw new GradleException('Use Major.Minor.Patch.113021 for Minecraft 1.13.2 on Forge.') } // Gradle and Mavenizer use modern JDKs; the mod and game still use Java 8. @@ -54,13 +55,11 @@ def normalizeText = { details -> eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) } -def developmentOutput = layout.buildDirectory.dir('forge12-development/main') +def developmentOutput = layout.buildDirectory.dir('forge-run-mod-classes/main') +def modPath = developmentOutput.get().asFile.absolutePath 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 + from sourceSets.main.output into developmentOutput } @@ -69,29 +68,27 @@ minecraft { if (gradle.startParameter.offline) mavenizerArguments.add('--offline') runs { configureEach { + mainClass = 'net.minecraftforge.userdev.LaunchTesting' workingDir.convention layout.projectDirectory.dir( providers.gradleProperty('baseGemsRunDirectory').getOrElse('run')) + mods { basegems { source sourceSets.main } } + environment 'target', name == 'client' ? 'fmluserdevclient' : 'fmluserdevserver' environment 'MC_VERSION', project.minecraft_version - environment 'MOD_CLASSES', developmentOutput.get().asFile.absolutePath - systemProperty 'log4j.skipJansi', 'true' + environment 'MCP_VERSION', project.mcp_version + environment 'MCP_MAPPINGS', "${project.mappings_channel}_${project.mappings_version}" + environment 'FORGE_VERSION', project.forge_version + environment 'FORGE_GROUP', 'net.minecraftforge' + environment 'MOD_CLASSES', "${mod_id}%%${modPath}${File.pathSeparator}${mod_id}%%${modPath}" systemProperty 'forge.logging.console.level', 'info' - systemProperty 'log4j.configurationFile', file('gradle/ide/log4j2.xml').toURI().toString() } register('client') - register('server') { args 'nogui' } + register('server') { args '--nogui' } } } - -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 +tasks.withType(JavaExec).configureEach { + if (name in ['runClient', 'runServer']) { + dependsOn prepareDevelopmentMod + doFirst { new File(workingDir, 'mods').mkdirs() } } } @@ -113,6 +110,21 @@ repositories { forRepository { maven { url = legacyRepository('https://www.cursemaven.com'); metadataSources { artifact() } } } filter { includeGroup 'curse.maven' } } + exclusiveContent { + forRepository { + maven { + name = 'PublishedBaseMetals' + url = legacyRepository('https://maven.moddev.zone/releases') + metadataSources { artifact() } + } + } + filter { includeModule('zone.moddev.mc.basemetals', 'BaseMetals') } + } + exclusiveContent { + forRepository { maven { url = 'https://repo.spongepowered.org/repository/maven-public' } } + filter { includeGroupAndSubgroups('org.spongepowered') } + } + maven { url = 'https://libraries.minecraft.net/' } mavenCentral() } @@ -129,11 +141,11 @@ tasks.named('processResources', ProcessResources) { exclude '**/desktop.ini', '**/Thumbs.db', '**/.DS_Store' exclude armourAssets inputs.property 'version', project.version - inputs.property 'mcversion', project.minecraft_version - filesMatching('mcmod.info') { expand version: project.version, mcversion: project.minecraft_version } - // Pack format 3 looks up locale filenames in lowercase. - filesMatching('assets/basegems/lang/*.lang') { name = name.toLowerCase(Locale.ROOT) } - rename '(.+_at.cfg)', 'META-INF/$1' + inputs.property 'oreSpawnRange', project.orespawn_version_range + filesMatching('META-INF/mods.toml') { + expand version: project.version, oreSpawnRange: project.orespawn_version_range + } + exclude 'assets/basegems/lang/*.lang' filesMatching(textPatterns, normalizeText) } @@ -164,7 +176,20 @@ tasks.named('sourcesJar', Jar) { exclude armourAssets } } ext.baseGemsBuild = [developmentOutput: developmentOutput].asImmutable() -apply from: 'gradle/ore-models.gradle' -apply from: 'gradle/recipe-advancements.gradle' +apply from: 'gradle/resources.gradle' apply from: 'gradle/release/artifacts.gradle' -apply from: 'gradle/ide/eclipse.gradle' +apply from: 'gradle/eclipse.gradle' +tasks.register('prepareEclipse') { + dependsOn 'genEclipseRuns', 'eclipse', 'prepareDevelopmentMod' +} + +tasks.register('verifyFluidRegistration', Exec) { + group = 'verification' + description = 'Checks that fluid registration runs before blocks, outside parallel construction.' + commandLine 'node', '--test', 'tools/test_fluid_registration.js' +} +tasks.named('check') { dependsOn 'verifyFluidRegistration' } + +apply from: 'gradle/release-113.gradle' + +apply from: 'gradle/normalize-javadoc-trees.gradle' diff --git a/docs/BUILDING.md b/docs/BUILDING.md index ad0657f..67fd720 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -1,24 +1,28 @@ -# Building Base Gems for Minecraft 1.12.2 +# Building Base Gems for Minecraft 1.13.2 Use Java 17 to run Gradle 9.6.1, Java 25 for ForgeGradle's Mavenizer, and Java 8 for compilation and Minecraft. Put Java 17 on your command path and make the other JDKs available to Gradle's toolchain detection. Python 3 -stages the pinned mod dependencies and checks their SHA-256 hashes. +stages the pinned OreSpawn and optional Base Metals jars, checks their SHA-256 +and generates the resource pack and data pack. From the inner Git checkout on Windows: ```powershell python gradle/stage-legacy-dependencies.py .gradle/legacy-dependencies -.\gradlew.bat check build verifyReleaseArtifacts writeReleaseChecksums prepareEclipse +.\gradlew.bat check build verify113ReleaseArtifacts write113ReleaseChecksums prepareEclipse ``` On Linux or macOS, use `python3` and `./gradlew`. The game jar is -`build/libs/BaseGems-2.5.0.112021.jar`. Gradle builds the ore models from -the tracked textures during resource processing. +`build/libs/BaseGems-2.5.0.113021.jar`. The build uses Forge 25.0.223 and +MCP snapshot 20180921-1.13. Model, recipe, tag, language and advancement data +is generated during resource processing. `prepareEclipse` generates the Eclipse project and Java 8 launches with processed resources and the pinned runtime dependencies. Use the same Gradle cache for Eclipse and command-line builds. +The optional Base Metals integration is pinned to its public MMD Maven release +3.0.3.113021. Run `prepareEclipse` again after changing sources or resources. Use a fresh test world when checking the OreSpawn defaults. In Low Fantasy, try mining and trading a gem, then check that storage blocks are diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md new file mode 100644 index 0000000..5c49c36 --- /dev/null +++ b/docs/MIGRATION.md @@ -0,0 +1,30 @@ +# Upgrading Base Gems worlds to 1.13.2 + +1. Close Minecraft or stop the old server. Copy the whole world directory + before upgrading. Keep the original save and its mods available. +2. Use Java 8 and Forge 25.0.223. Install the 1.13.2 versions of Base Gems and + MMD OreSpawn, plus 1.13.2 ports of the other mods that placed blocks or + items in the world. The migration only handles Base Gems content. For + worlds with Base Metals, use its 1.13.2 version too. +3. Open the copy. Use Optimize World or the server's `--forceUpgrade` option + to convert every region at once. Normal loading converts chunks as you + visit them. Keep the `basegems-legacy-registry.dat` file that Base Gems + writes in the save, especially if some old chunks are still unvisited. +4. Check the places and inventories that matter to you and keep the old + save until you're happy with the upgrade. + +Minecraft 1.13 replaces numeric block IDs and metadata with named block +states. Base Gems reads the IDs assigned to the world, including the +Forge snapshot in `level.dat_old` when needed. It preserves the old gem +names and restores settings such as slab halves, stair facing, plate +orientation, fluid levels and the hinge and opening on both halves of a +door. Old universal buckets containing molten gems become named gem +buckets. Item counts, tool damage and existing item tags are carried over. + +Base Gems creates a `*.basegems-legacy-backup` next to any level or player +file it needs to rewrite. Minecraft writes converted chunks in place. The +untouched copy of the original world is what you need if you want to go back. + +Set Fantasy Level and gem switches in the new Mods config screen after +the upgrade. The old `BaseGems.cfg` is a reference for the settings you used +before. OreSpawn owns its saved world profiles. diff --git a/docs/ORE_GENERATION.md b/docs/ORE_GENERATION.md index a39da8d..58ebdce 100644 --- a/docs/ORE_GENERATION.md +++ b/docs/ORE_GENERATION.md @@ -37,7 +37,7 @@ than its budget if it runs out of suitable host rock. The provider covers the Overworld and leaves terrain replacement and retrogen off. In existing worlds, use new chunks to find the ores and review -the saved OreSpawn settings for that world. When a material is disabled in -`BaseGems.cfg`, its ore block and default rule are omitted on restart. -Remove the corresponding rule from any pack-wide override when disabling a -gem, since the override can only refer to registered blocks. +the saved OreSpawn settings for that world. When a gem is disabled in +`basegems-common.toml`, Base Gems stops supplying its default ore rule on +restart. The block stays registered so it survives upgrades. Turn off the +rule in OreSpawn too if it is already part of a saved world or pack override. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..8d29cc1 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,28 @@ +# Releasing Base Gems for Minecraft 1.13.2 + +Build with Java 17, Temurin 8u502-b07 for compilation and Java 25 for +Mavenizer. Run check, build, verify113ReleaseArtifacts, +write113ReleaseChecksums, genEclipseRuns and eclipse. Keep the packaged +gameplay and saved-world checks local, then complete manual testing. + +Open a PR from the fork to MMD's master-1.13.2. After merging, wait for the +successful push CI run and download BaseGems-1.13.2-. Compare +all three jars with the locally qualified bundle. The CI bundle contains +the player, sources and Javadoc jars, SHA256SUMS and CHANGELOG.txt. + +Run deploy-release.yml on master-1.13.2 with the release version, full merged +commit and qualified game-jar SHA-256. Leave confirm_live_publication false +for verification. Set it true only when publication is authorised. The +workflow checks the branch tip, successful merged CI and bundle hashes, +then publishes those exact files to MMD Maven, CurseForge and GitHub. +It does not rebuild the jars during publication. + +MMD needs CURSEFORGE_TOKEN, MAVEN_UPLOAD_URL, MAVEN_UPLOAD_USERNAME and +MAVEN_UPLOAD_PASSWORD as repository or shared organisation secrets. Maven +publication requires HTTPS and validates the prepared bundle again. + +Verify anonymous downloads and hashes on all three services, including the +CurseForge dependency metadata. If a publication job fails, preserve its +logs and retry only the failed job; do not duplicate an accepted upload. +Base Metals is optional and OreSpawn is required. Publish Base Metals first +and keep the compatibility pin on its public 1.13.2 release. diff --git a/gradle.properties b/gradle.properties index 022720f..f6c034a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,15 +4,13 @@ org.gradle.configuration-cache=false org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false - -minecraft_version=1.12.2 -forge_version=14.23.5.2859 -mappings_channel=stable -mappings_version=39-1.12 - +minecraft_version=1.13.2 +forge_version=25.0.223 +mappings_channel=snapshot +mappings_version=20180921-1.13 mod_id=basegems mod_group=zone.moddev.mc.basegems -mod_version=2.5.0.112021 +mod_version=2.5.0.113021 mod_artifact=BaseGems loader_name=forge loader_code=1 @@ -22,9 +20,6 @@ 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 -orespawn_version=4.1.6.112021 -orespawn_version_range=[4.1.6.112021,5.0.0) - -mmdlib_version=1.0.0-rc3 -# The published RC3 jar still reports RC2 to Forge. -mmdlib_version_range=[1.0.0-rc2,2.0.0) +orespawn_version=4.1.7.113021 +orespawn_version_range=[4.1.7.113021,5.0.0) +mcp_version=20190213.203750 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index a0d1742..1d9fcae 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -1,72 +1,35 @@ 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)] } - +def pins = new JsonSlurper().parse(file('gradle/dependencies.json')) +def pin = pins.find { it.id == 'orespawn' } configurations { - legacyModArtifacts { canBeConsumed = false; transitive = false } - java8LauncherArtifact { canBeConsumed = false; transitive = false } + oreSpawnRelease { canBeConsumed = false; transitive = false } + compatibilityReleases { canBeConsumed = false; transitive = false } } dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") - java8LauncherArtifact 'net.minecraftforge:slime-launcher:0.2.2' - 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 - } -} - -// 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' + implementation renamer.dependency(pin.coordinate) + oreSpawnRelease pin.coordinate + pins.findAll { it.id != 'orespawn' }.each { compatibilityReleases it.coordinate } } - -// 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 - def orespawn = specs.find { it.id == 'orespawn' } - if (orespawn.coordinate != 'curse.maven:mmd-orespawn-245586:9527784' - || orespawn.sha256 != 'DA6B243317E3365371C397CC9A895D108312F3437FB1797FBB5E8241525EA75F') { - throw new GradleException('Use the verified OreSpawn 4.1.6.112021 release.') - } - def mmdlib = specs.find { it.id == 'mmdlib' } - if (mmdlib.coordinate != 'curse.maven:mmdlib-261744:9414723' - || mmdlib.sha256 != 'B35CD5A4454B03D43744EE263BD92A4B718660557C360F961B65A6303A80F88C') { - throw new GradleException('Use the verified MMDLib 1.0.0 RC3 jar.') - } - 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.') - } - } + def jar = configurations.oreSpawnRelease.singleFile + def digest = MessageDigest.getInstance('SHA-256').digest(jar.bytes).encodeHex().toString() + if (!digest.equalsIgnoreCase(pin.sha256)) throw new GradleException("Wrong OreSpawn release: ${jar.name}") + def artifacts = configurations.compatibilityReleases.resolvedConfiguration.resolvedArtifacts + pins.findAll { it.id != 'orespawn' }.each { compatibility -> + def coordinate = compatibility.coordinate.split(':') + def archive = artifacts.find { it.moduleVersion.id.name == coordinate[1] + && it.moduleVersion.id.version == coordinate[2] }.file + def hash = MessageDigest.getInstance('SHA-256').digest(archive.bytes).encodeHex().toString() + if (!hash.equalsIgnoreCase(compatibility.sha256)) { + throw new GradleException("Wrong compatibility release: ${archive.name}") } } } } -tasks.named('compileJava') { dependsOn tasks.named('verifyReleaseDependencies') } -tasks.named('check') { dependsOn tasks.named('verifyReleaseDependencies') } +tasks.named('compileJava') { dependsOn 'verifyReleaseDependencies' } +tasks.named('check') { dependsOn 'verifyReleaseDependencies' } diff --git a/gradle/dependencies.json b/gradle/dependencies.json index 45e9a1a..da51165 100644 --- a/gradle/dependencies.json +++ b/gradle/dependencies.json @@ -1,30 +1,16 @@ [ - { - "id": "mmdlib", - "coordinate": "curse.maven:mmdlib-261744:9414723", - "repository": "https://www.cursemaven.com", - "sha256": "B35CD5A4454B03D43744EE263BD92A4B718660557C360F961B65A6303A80F88C", - "runtime": true - }, { "id": "orespawn", - "coordinate": "curse.maven:mmd-orespawn-245586:9527784", + "coordinate": "curse.maven:mmd-orespawn-245586:9611746", "repository": "https://www.cursemaven.com", - "sha256": "DA6B243317E3365371C397CC9A895D108312F3437FB1797FBB5E8241525EA75F", + "sha256": "E65DCCA0623A20111C40779CB0FE2078E98C618FAA68F79C9A28E1E53C63957E", "runtime": true }, { - "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", + "id": "basemetals", + "coordinate": "zone.moddev.mc.basemetals:BaseMetals:3.0.3.113021", + "repository": "https://maven.moddev.zone/releases", + "sha256": "88B9DB908249B9CD7E415F5577311ED03A22CF6BF17800688F67FC65F6BE2ECD", "runtime": false } ] diff --git a/gradle/eclipse.gradle b/gradle/eclipse.gradle new file mode 100644 index 0000000..10cda04 --- /dev/null +++ b/gradle/eclipse.gradle @@ -0,0 +1,29 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +eclipse { + synchronizationTasks 'prepareEclipse' +} + +tasks.named('genEclipseRuns') { + doLast { + def boundaries = [ + '--cache': ' --metadata', '--metadata': ' --to-srg', + '--to-srg': ' --to-obf', '--to-obf': ' --' + ] + ['runClient.launch', 'runServer.launch'].each { name -> + File launch = file(name) + String xml = launch.getText('UTF-8') + boundaries.each { option, next -> + def match = Pattern.compile(Pattern.quote(option + ' ') + '(.+?)' + Pattern.quote(next)) + .matcher(xml) + if (!match.find()) throw new GradleException("Missing ${option} in ${name}") + String argument = match.group(1) + if (!argument.startsWith('"')) { + xml = match.replaceFirst(Matcher.quoteReplacement(option + ' "' + argument + '"' + next)) + } + } + launch.setText(xml, 'UTF-8') + } + } +} diff --git a/gradle/generate-resources.py b/gradle/generate-resources.py new file mode 100644 index 0000000..21c44c7 --- /dev/null +++ b/gradle/generate-resources.py @@ -0,0 +1,218 @@ +"""Translate the legacy art and build the data pack for Forge 25.""" + +from pathlib import Path +import json +import shutil +import sys + +PROJECT = Path(__file__).resolve().parent.parent +ASSETS = PROJECT / "src/main/resources/assets/basegems" +OUTPUT = Path(sys.argv[1]).resolve() +if (PROJECT / "build").resolve() not in OUTPUT.parents or OUTPUT.name != "basegems-resources": + raise SystemExit(f"Refusing to clear an unexpected output directory: {OUTPUT}") +if OUTPUT.exists(): + shutil.rmtree(OUTPUT) + +GEMS = sorted(path.stem.removesuffix("_ore") for path in (ASSETS / "blockstates").glob("*_ore.json")) +assert len(GEMS) == 33 +FORMS = ("block", "ore", "plate", "bars", "door", "trapdoor", "button", "slab", + "lever", "pressure_plate", "stairs", "wall") +TEMPLATES = json.loads((PROJECT / "gradle/model-templates.json").read_text(encoding="utf-8")) + + +def write(path, data): + target = OUTPUT / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n") + + +def asset(path, data): + write(f"assets/basegems/{path}", data) + + +def generated_model(texture): + return {"parent": "minecraft:item/generated", "textures": {"layer0": texture}} + + +def convert_models(): + for path in (ASSETS / "models").rglob("*.json"): + if any(path.stem.endswith("_" + armour) for armour in + ("helmet", "chestplate", "leggings", "boots", "horse_armor")): + continue + data = json.loads(path.read_text(encoding="utf-8")) + if data.get("parent") in ("block/half_slab", "minecraft:block/half_slab"): + data["parent"] = "minecraft:block/slab" + elif data.get("parent") in ("block/upper_slab", "minecraft:block/upper_slab"): + data["parent"] = "minecraft:block/slab_top" + asset(path.relative_to(ASSETS).as_posix(), data) + for path, template in TEMPLATES.items(): + if "{gem}" not in path: + asset(path, template) + continue + for gem in GEMS: + data = json.loads(json.dumps(template).replace("{gem}", gem)) + for key, texture in data.get("textures", {}).items(): + if not texture.startswith("basegems:block/"): + continue + texture_path = texture.removeprefix("basegems:block/") + if not (ASSETS / "textures/blocks" / (texture_path + ".png")).is_file(): + texture_path = gem + "_block" + data["textures"][key] = "basegems:blocks/" + texture_path + asset(path.replace("{gem}", gem), data) + for gem in GEMS: + asset(f"blockstates/{gem}.json", {"variants": {"": {"model": "basegems:block/molten_gem"}}}) + asset(f"models/item/{gem}.json", generated_model(f"basegems:items/{gem}_ingot")) + asset(f"models/item/{gem}_bucket.json", generated_model("minecraft:item/lava_bucket")) + for form in FORMS: + path = f"{gem}_{form}" + if form == "door": + model = generated_model(f"basegems:blocks/{gem}_door_bottom") + elif form == "slab": + model = {"parent": "basegems:block/half_slab_" + gem} + else: + suffix = {"bars": "_bars_post", "button": "_button_inventory", "trapdoor": "_trapdoor_bottom", + "wall": "_wall_inventory", "pressure_plate": "_pressure_plate_up"}.get(form, "_" + form) + model = {"parent": "basegems:block/" + gem + suffix} + asset(f"models/item/{path}.json", model) + asset("models/block/molten_gem.json", {"textures": {"particle": "basegems:blocks/molten_metal_still"}}) + + +def convert_languages(): + for path in (ASSETS / "lang").glob("*.lang"): + values = {} + for line in path.read_text(encoding="utf-8-sig").splitlines(): + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + if key.startswith(("item.basegems.", "tile.basegems.")) and key.endswith(".name"): + key = key.removesuffix(".name") + if key.startswith("tile."): + key = "block." + key.removeprefix("tile.") + values[key] = value + values["itemGroup.basegems"] = "Base Gems" + values.setdefault("config.basegems.restart", "Restart Minecraft to apply changes.") + for gem in GEMS: + name = values.get(f"item.basegems.{gem}_ingot", gem) + values[f"item.basegems.{gem}_bucket"] = values.get(f"fluid.basegems.{gem}", name) + " Bucket" + asset(f"lang/{path.stem.lower()}.json", values) + + +def item(gem, form): + return {"item": f"basegems:{gem}_{form}"} + + +def tag(kind, gem=None): + return {"tag": f"forge:{kind}" + (f"/{gem}" if gem else "")} + + +def vanilla(name): + return {"item": f"minecraft:{name}"} + + +def game_data(kind, path, data): + write(f"data/basegems/{kind}/{path}.json", data) + + +def advancement(gem, name, trigger): + recipe = f"basegems:{gem}_{name}" + game_data("advancements/recipes", f"{gem}_{name}", { + "parent": "minecraft:recipes/root", + "rewards": {"recipes": [recipe]}, + "criteria": { + "has_ingredient": {"trigger": "minecraft:inventory_changed", + "conditions": {"items": [item(gem, trigger)]}}, + "has_recipe": {"trigger": "minecraft:recipe_unlocked", "conditions": {"recipe": recipe}}, + }, + "requirements": [["has_ingredient", "has_recipe"]], + }) + + +def crafting(gem): + gem_tag, nugget, block, rod, gear = (tag(kind, gem) for kind in + ("gems", "nuggets", "storage_blocks", "rods", "gears")) + stick = tag("rods/wooden") + + def recipe(name, form, count, trigger, body): + body["result"] = dict(item(gem, form), count=count) + game_data("recipes", f"{gem}_{name}", {"type": "basegems:content_crafting", "recipe": body}) + advancement(gem, name, trigger) + + def shaped(form, count, trigger, pattern, key, name=None): + recipe(name or form, form, count, trigger, + {"type": "minecraft:crafting_shaped", "pattern": pattern.split("|"), "key": key}) + + def shapeless(name, form, count, trigger, ingredient): + recipe(name, form, count, trigger, {"type": "minecraft:crafting_shapeless", "ingredients": [ingredient]}) + + shaped("block", 1, "ingot", "xxx|xxx|xxx", {"x": gem_tag}) + shapeless("unpack_block", "ingot", 9, "block", block) + shaped("ingot", 1, "nugget", "xxx|xxx|xxx", {"x": nugget}) + shapeless("unpack_gem", "nugget", 9, "ingot", gem_tag) + shaped("powder", 1, "smallpowder", "xxx|xxx|xxx", {"x": item(gem, "smallpowder")}) + shapeless("unpack_powder", "smallpowder", 9, "powder", item(gem, "powder")) + for form, count, trigger, pattern, ingredients in ( + ("plate", 3, "ingot", "xxx", {"x": gem_tag}), + ("slab", 6, "block", "xxx", {"x": block}), + ("lever", 1, "ingot", "s|x", {"s": stick, "x": gem_tag}), + ("stairs", 4, "block", "x |xx |xxx", {"x": block}), + ("wall", 6, "block", "xxx|xxx", {"x": block}), + ("bars", 16, "ingot", "xxx|xxx", {"x": gem_tag}), + ("pressure_plate", 1, "ingot", "xx", {"x": gem_tag}), + ("door", 3, "ingot", "xx|xx|xx", {"x": gem_tag}), + ("trapdoor", 1, "ingot", "xx|xx", {"x": gem_tag}), + ("button", 1, "nugget", "x|x", {"x": nugget}), + ("rod", 4, "ingot", "x|x", {"x": gem_tag}), + ("gear", 1, "rod", " x |xrx| x ", {"x": gem_tag, "r": rod}), + ("crackhammer", 1, "block", "x|s|s", {"x": block, "s": stick}), + ("axe", 1, "ingot", "xx|xs| s", {"x": gem_tag, "s": stick}), + ("hoe", 1, "ingot", "xx| s| s", {"x": gem_tag, "s": stick}), + ("pickaxe", 1, "ingot", "xxx| s | s ", {"x": gem_tag, "s": stick}), + ("shovel", 1, "ingot", "x|s|s", {"x": gem_tag, "s": stick}), + ("sword", 1, "ingot", "x|x|s", {"x": gem_tag, "s": stick}), + ("shears", 1, "ingot", " x|x ", {"x": gem_tag}), + ("arrow", 4, "nugget", "x|r|f", {"x": nugget, "r": rod, "f": vanilla("feather")}), + ("bow", 1, "rod", " rs|r s| rs", {"r": rod, "s": vanilla("string")}), + ("bolt", 4, "rod", "r|r|f", {"r": rod, "f": vanilla("feather")}), + ("crossbow", 1, "rod", "srr| gr|r s", {"r": rod, "g": gear, "s": vanilla("string")}), + ("fishing_rod", 1, "rod", " r| rs|r s", {"r": rod, "s": vanilla("string")}), + ("shield", 1, "ingot", "xpx|xxx| x ", {"x": gem_tag, "p": {"tag": "minecraft:planks"}}), + ): + shaped(form, count, trigger, pattern, ingredients) + shaped("bars", 4, "rod", "xxx", {"x": rod}, name="bars_from_rods") + game_data("recipes", f"{gem}_repair_shield", {"type": "basegems:plate_repair", + "target": f"basegems:{gem}_shield", "plate": tag("plates", gem)}) + advancement(gem, "repair_shield", "plate") + for form in ("ore", "powder"): + game_data("recipes", f"{gem}_{form}_smelting", {"type": "basegems:smelting", + "ingredient": item(gem, form), "result": item(gem, "ingot"), "experience": 0.45}) + advancement(gem, f"{form}_smelting", form) + for form, result, count in (("ore", "powder", 2), ("ingot", "powder", 1), ("block", "powder", 9), + ("nugget", "smallpowder", 1)): + game_data("recipes", f"{gem}_{form}_crushing", {"type": "basegems:crushing", + "ingredient": item(gem, form), "result": dict(item(gem, result), count=count)}) + + +def generate_tags(): + for category, form in {"gems": "ingot", "ingots": "ingot", "nuggets": "nugget", "dusts": "powder", + "tiny_dusts": "smallpowder", "rods": "rod", "gears": "gear", "ores": "ore", + "storage_blocks": "block", "plates": "plate"}.items(): + for gem in GEMS: + entry = {"replace": False, "values": [f"basegems:{gem}_{form}"]} + write(f"data/forge/tags/items/{category}/{gem}.json", entry) + if form in ("ore", "block", "plate"): + write(f"data/forge/tags/blocks/{category}/{gem}.json", entry) + write(f"data/forge/tags/items/{category}.json", + {"replace": False, "values": [f"#forge:{category}/{gem}" for gem in GEMS]}) + if form in ("ore", "block", "plate"): + write(f"data/forge/tags/blocks/{category}.json", + {"replace": False, "values": [f"#forge:{category}/{gem}" for gem in GEMS]}) + write("data/forge/tags/items/rods/wooden.json", {"replace": False, "values": ["minecraft:stick"]}) + write("data/minecraft/tags/items/arrows.json", + {"replace": False, "values": [f"basegems:{gem}_arrow" for gem in GEMS]}) + + +convert_models() +convert_languages() +generate_tags() +for gem in GEMS: + crafting(gem) diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle deleted file mode 100644 index 7dc0d1c..0000000 --- a/gradle/ide/eclipse.gradle +++ /dev/null @@ -1,206 +0,0 @@ -def escapeXml = { String text -> - text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') -} - -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.. - String memento = '' - ' ' - }.join('\n') - text = lines.join('\n') - if (!text.contains('ATTR_EXCLUDE_TEST_CODE')) { - text = text.replace('', - ' \n') - } - launch.setText(text, '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', 'runServer.launch'].each { name -> - 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 (runtime.any { !text.contains(it.path.replace('\\', '/')) } - || optional.any { text.contains(it.path.replace('\\', '/')) }) { - throw new GradleException("Runtime/compile-only separation failed in ${name}") - } - ['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 deleted file mode 100644 index a770e98..0000000 --- a/gradle/ide/log4j2.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/gradle/model-templates.json b/gradle/model-templates.json new file mode 100644 index 0000000..52a8aa6 --- /dev/null +++ b/gradle/model-templates.json @@ -0,0 +1,2019 @@ +{ + "blockstates/{gem}_bars.json": { + "multipart": [ + { + "apply": { + "model": "basegems:block/{gem}_bars_post" + } + }, + { + "when": { + "north": "true" + }, + "apply": { + "model": "basegems:block/{gem}_bars_side" + } + }, + { + "when": { + "north": "false" + }, + "apply": { + "model": "basegems:block/{gem}_bars_cap" + } + }, + { + "when": { + "south": "true" + }, + "apply": { + "model": "basegems:block/{gem}_bars_side_alt" + } + }, + { + "when": { + "south": "false" + }, + "apply": { + "model": "basegems:block/{gem}_bars_cap_alt", + "y": 90 + } + }, + { + "when": { + "west": "true" + }, + "apply": { + "model": "basegems:block/{gem}_bars_side_alt", + "y": 90 + } + }, + { + "when": { + "west": "false" + }, + "apply": { + "model": "basegems:block/{gem}_bars_cap", + "y": 270 + } + }, + { + "when": { + "east": "true" + }, + "apply": { + "model": "basegems:block/{gem}_bars_side", + "y": 90 + } + }, + { + "when": { + "east": "false" + }, + "apply": { + "model": "basegems:block/{gem}_bars_cap_alt" + } + } + ] + }, + "blockstates/{gem}_block.json": { + "variants": { + "": { + "model": "basegems:block/{gem}_block" + } + } + }, + "blockstates/{gem}_button.json": { + "variants": { + "face=floor,facing=north,powered=false": { + "model": "basegems:block/{gem}_button" + }, + "face=wall,facing=north,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 90, + "uvlock": true + }, + "face=ceiling,facing=north,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 180, + "y": 180 + }, + "face=floor,facing=south,powered=false": { + "model": "basegems:block/{gem}_button", + "y": 180 + }, + "face=wall,facing=south,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 90, + "y": 180, + "uvlock": true + }, + "face=ceiling,facing=south,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 180 + }, + "face=floor,facing=west,powered=false": { + "model": "basegems:block/{gem}_button", + "y": 270 + }, + "face=wall,facing=west,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 90, + "y": 270, + "uvlock": true + }, + "face=ceiling,facing=west,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 180, + "y": 90 + }, + "face=floor,facing=east,powered=false": { + "model": "basegems:block/{gem}_button", + "y": 90 + }, + "face=wall,facing=east,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 90, + "y": 90, + "uvlock": true + }, + "face=ceiling,facing=east,powered=false": { + "model": "basegems:block/{gem}_button", + "x": 180, + "y": 270 + }, + "face=floor,facing=north,powered=true": { + "model": "basegems:block/{gem}_button_pressed" + }, + "face=wall,facing=north,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 90, + "uvlock": true + }, + "face=ceiling,facing=north,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 180, + "y": 180 + }, + "face=floor,facing=south,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "y": 180 + }, + "face=wall,facing=south,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 90, + "y": 180, + "uvlock": true + }, + "face=ceiling,facing=south,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 180 + }, + "face=floor,facing=west,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "y": 270 + }, + "face=wall,facing=west,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 90, + "y": 270, + "uvlock": true + }, + "face=ceiling,facing=west,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 180, + "y": 90 + }, + "face=floor,facing=east,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "y": 90 + }, + "face=wall,facing=east,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 90, + "y": 90, + "uvlock": true + }, + "face=ceiling,facing=east,powered=true": { + "model": "basegems:block/{gem}_button_pressed", + "x": 180, + "y": 270 + } + } + }, + "blockstates/{gem}_door.json": { + "variants": { + "facing=north,half=upper,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_top", + "y": 270 + }, + "facing=south,half=upper,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_top", + "y": 90 + }, + "facing=west,half=upper,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_top", + "y": 180 + }, + "facing=east,half=upper,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_top" + }, + "facing=north,half=lower,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_bottom", + "y": 270 + }, + "facing=south,half=lower,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_bottom", + "y": 90 + }, + "facing=west,half=lower,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_bottom", + "y": 180 + }, + "facing=east,half=lower,hinge=left,open=false": { + "model": "basegems:block/{gem}_door_bottom" + }, + "facing=north,half=upper,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_top_rh", + "y": 270 + }, + "facing=south,half=upper,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_top_rh", + "y": 90 + }, + "facing=west,half=upper,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_top_rh", + "y": 180 + }, + "facing=east,half=upper,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_top_rh" + }, + "facing=north,half=lower,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_bottom_rh", + "y": 270 + }, + "facing=south,half=lower,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_bottom_rh", + "y": 90 + }, + "facing=west,half=lower,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_bottom_rh", + "y": 180 + }, + "facing=east,half=lower,hinge=right,open=false": { + "model": "basegems:block/{gem}_door_bottom_rh" + }, + "facing=north,half=upper,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_top_rh" + }, + "facing=south,half=upper,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_top_rh", + "y": 180 + }, + "facing=west,half=upper,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_top_rh", + "y": 270 + }, + "facing=east,half=upper,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_top_rh", + "y": 90 + }, + "facing=north,half=lower,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_bottom_rh" + }, + "facing=south,half=lower,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_bottom_rh", + "y": 180 + }, + "facing=west,half=lower,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_bottom_rh", + "y": 270 + }, + "facing=east,half=lower,hinge=left,open=true": { + "model": "basegems:block/{gem}_door_bottom_rh", + "y": 90 + }, + "facing=north,half=upper,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_top", + "y": 180 + }, + "facing=south,half=upper,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_top" + }, + "facing=west,half=upper,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_top", + "y": 90 + }, + "facing=east,half=upper,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_top", + "y": 270 + }, + "facing=north,half=lower,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_bottom", + "y": 180 + }, + "facing=south,half=lower,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_bottom" + }, + "facing=west,half=lower,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_bottom", + "y": 90 + }, + "facing=east,half=lower,hinge=right,open=true": { + "model": "basegems:block/{gem}_door_bottom", + "y": 270 + } + } + }, + "blockstates/{gem}_lever.json": { + "variants": { + "face=floor,facing=north,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=wall,facing=north,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=ceiling,facing=north,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=floor,facing=south,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=wall,facing=south,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=ceiling,facing=south,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=floor,facing=west,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=wall,facing=west,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=ceiling,facing=west,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=floor,facing=east,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=wall,facing=east,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=ceiling,facing=east,powered=false": { + "model": "basegems:block/{gem}_lever_off" + }, + "face=floor,facing=north,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=wall,facing=north,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=ceiling,facing=north,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=floor,facing=south,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=wall,facing=south,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=ceiling,facing=south,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=floor,facing=west,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=wall,facing=west,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=ceiling,facing=west,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=floor,facing=east,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=wall,facing=east,powered=true": { + "model": "basegems:block/{gem}_lever" + }, + "face=ceiling,facing=east,powered=true": { + "model": "basegems:block/{gem}_lever" + } + } + }, + "blockstates/{gem}_ore.json": { + "variants": { + "": { + "model": "basegems:block/{gem}_ore" + } + } + }, + "blockstates/{gem}_plate.json": { + "variants": { + "facing=down": { + "model": "basegems:block/{gem}_plate", + "x": 90 + }, + "facing=up": { + "model": "basegems:block/{gem}_plate", + "x": 270 + }, + "facing=north": { + "model": "basegems:block/{gem}_plate" + }, + "facing=south": { + "model": "basegems:block/{gem}_plate", + "y": 180 + }, + "facing=west": { + "model": "basegems:block/{gem}_plate", + "y": 270 + }, + "facing=east": { + "model": "basegems:block/{gem}_plate", + "y": 90 + } + } + }, + "blockstates/{gem}_pressure_plate.json": { + "variants": { + "powered=false": { + "model": "basegems:block/{gem}_pressure_plate_up" + }, + "powered=true": { + "model": "basegems:block/{gem}_pressure_plate_down" + } + } + }, + "blockstates/{gem}_slab.json": { + "variants": { + "type=top": { + "model": "basegems:block/upper_slab_{gem}" + }, + "type=bottom": { + "model": "basegems:block/half_slab_{gem}" + }, + "type=double": { + "model": "basegems:block/{gem}_block" + } + } + }, + "blockstates/{gem}_stairs.json": { + "variants": { + "facing=north,half=top,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "x": 180, + "y": 270, + "uvlock": true + }, + "facing=south,half=top,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "x": 180, + "y": 90, + "uvlock": true + }, + "facing=west,half=top,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "x": 180, + "y": 180, + "uvlock": true + }, + "facing=east,half=top,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "x": 180, + "uvlock": true + }, + "facing=north,half=bottom,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "y": 270, + "uvlock": true + }, + "facing=south,half=bottom,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "y": 90, + "uvlock": true + }, + "facing=west,half=bottom,shape=straight": { + "model": "basegems:block/{gem}_stairs", + "y": 180, + "uvlock": true + }, + "facing=east,half=bottom,shape=straight": { + "model": "basegems:block/{gem}_stairs" + }, + "facing=north,half=top,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "y": 270, + "uvlock": true + }, + "facing=south,half=top,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "y": 90, + "uvlock": true + }, + "facing=west,half=top,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "y": 180, + "uvlock": true + }, + "facing=east,half=top,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "uvlock": true + }, + "facing=north,half=bottom,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "y": 180, + "uvlock": true + }, + "facing=south,half=bottom,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs" + }, + "facing=west,half=bottom,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "y": 90, + "uvlock": true + }, + "facing=east,half=bottom,shape=inner_left": { + "model": "basegems:block/{gem}_inner_stairs", + "y": 270, + "uvlock": true + }, + "facing=north,half=top,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "uvlock": true + }, + "facing=south,half=top,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "y": 180, + "uvlock": true + }, + "facing=west,half=top,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "y": 270, + "uvlock": true + }, + "facing=east,half=top,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "x": 180, + "y": 90, + "uvlock": true + }, + "facing=north,half=bottom,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "y": 270, + "uvlock": true + }, + "facing=south,half=bottom,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "y": 90, + "uvlock": true + }, + "facing=west,half=bottom,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs", + "y": 180, + "uvlock": true + }, + "facing=east,half=bottom,shape=inner_right": { + "model": "basegems:block/{gem}_inner_stairs" + }, + "facing=north,half=top,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "y": 270, + "uvlock": true + }, + "facing=south,half=top,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "y": 90, + "uvlock": true + }, + "facing=west,half=top,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "y": 180, + "uvlock": true + }, + "facing=east,half=top,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "uvlock": true + }, + "facing=north,half=bottom,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "y": 180, + "uvlock": true + }, + "facing=south,half=bottom,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs" + }, + "facing=west,half=bottom,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "y": 90, + "uvlock": true + }, + "facing=east,half=bottom,shape=outer_left": { + "model": "basegems:block/{gem}_outer_stairs", + "y": 270, + "uvlock": true + }, + "facing=north,half=top,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "uvlock": true + }, + "facing=south,half=top,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "y": 180, + "uvlock": true + }, + "facing=west,half=top,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "y": 270, + "uvlock": true + }, + "facing=east,half=top,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "x": 180, + "y": 90, + "uvlock": true + }, + "facing=north,half=bottom,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "y": 270, + "uvlock": true + }, + "facing=south,half=bottom,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "y": 90, + "uvlock": true + }, + "facing=west,half=bottom,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs", + "y": 180, + "uvlock": true + }, + "facing=east,half=bottom,shape=outer_right": { + "model": "basegems:block/{gem}_outer_stairs" + } + } + }, + "blockstates/{gem}_trapdoor.json": { + "variants": { + "facing=north,half=top,open=false": { + "model": "basegems:block/{gem}_trapdoor_top" + }, + "facing=south,half=top,open=false": { + "model": "basegems:block/{gem}_trapdoor_top", + "y": 180 + }, + "facing=west,half=top,open=false": { + "model": "basegems:block/{gem}_trapdoor_top", + "y": 270 + }, + "facing=east,half=top,open=false": { + "model": "basegems:block/{gem}_trapdoor_top", + "y": 90 + }, + "facing=north,half=bottom,open=false": { + "model": "basegems:block/{gem}_trapdoor_bottom" + }, + "facing=south,half=bottom,open=false": { + "model": "basegems:block/{gem}_trapdoor_bottom", + "y": 180 + }, + "facing=west,half=bottom,open=false": { + "model": "basegems:block/{gem}_trapdoor_bottom", + "y": 270 + }, + "facing=east,half=bottom,open=false": { + "model": "basegems:block/{gem}_trapdoor_bottom", + "y": 90 + }, + "facing=north,half=top,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "x": 180, + "y": 180 + }, + "facing=south,half=top,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "x": 180 + }, + "facing=west,half=top,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "x": 180, + "y": 90 + }, + "facing=east,half=top,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "x": 180, + "y": 270 + }, + "facing=north,half=bottom,open=true": { + "model": "basegems:block/{gem}_trapdoor_open" + }, + "facing=south,half=bottom,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "y": 180 + }, + "facing=west,half=bottom,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "y": 270 + }, + "facing=east,half=bottom,open=true": { + "model": "basegems:block/{gem}_trapdoor_open", + "y": 90 + } + } + }, + "blockstates/{gem}_wall.json": { + "multipart": [ + { + "when": { + "up": "true" + }, + "apply": { + "model": "basegems:block/{gem}_wall_post" + } + }, + { + "when": { + "north": "true" + }, + "apply": { + "model": "basegems:block/{gem}_wall_side", + "uvlock": true + } + }, + { + "when": { + "east": "true" + }, + "apply": { + "model": "basegems:block/{gem}_wall_side", + "y": 90, + "uvlock": true + } + }, + { + "when": { + "south": "true" + }, + "apply": { + "model": "basegems:block/{gem}_wall_side", + "y": 180, + "uvlock": true + } + }, + { + "when": { + "west": "true" + }, + "apply": { + "model": "basegems:block/{gem}_wall_side", + "y": 270, + "uvlock": true + } + } + ] + }, + "blockstates/double_{gem}_slab.json": { + "variants": { + "type=top": { + "model": "basegems:block/upper_slab_{gem}" + }, + "type=bottom": { + "model": "basegems:block/half_slab_{gem}" + }, + "type=double": { + "model": "basegems:block/{gem}_block" + } + } + }, + "models/block/{gem}_bars_cap.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_bars", + "bars": "basegems:block/{gem}_bars", + "edge": "basegems:block/{gem}_bars" + }, + "elements": [ + { + "from": [ + 8, + 0, + 8 + ], + "to": [ + 8, + 16, + 9 + ], + "faces": { + "west": { + "uv": [ + 8, + 0, + 7, + 16 + ], + "texture": "#bars" + }, + "east": { + "uv": [ + 7, + 0, + 8, + 16 + ], + "texture": "#bars" + } + } + }, + { + "from": [ + 7, + 0, + 9 + ], + "to": [ + 9, + 16, + 9 + ], + "faces": { + "north": { + "uv": [ + 9, + 0, + 7, + 16 + ], + "texture": "#bars" + }, + "south": { + "uv": [ + 7, + 0, + 9, + 16 + ], + "texture": "#bars" + } + } + } + ] + }, + "models/block/{gem}_bars_cap_alt.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_bars", + "bars": "basegems:block/{gem}_bars", + "edge": "basegems:block/{gem}_bars" + }, + "elements": [ + { + "from": [ + 8, + 0, + 7 + ], + "to": [ + 8, + 16, + 8 + ], + "faces": { + "west": { + "uv": [ + 8, + 0, + 9, + 16 + ], + "texture": "#bars" + }, + "east": { + "uv": [ + 9, + 0, + 8, + 16 + ], + "texture": "#bars" + } + } + }, + { + "from": [ + 7, + 0, + 7 + ], + "to": [ + 9, + 16, + 7 + ], + "faces": { + "north": { + "uv": [ + 7, + 0, + 9, + 16 + ], + "texture": "#bars" + }, + "south": { + "uv": [ + 9, + 0, + 7, + 16 + ], + "texture": "#bars" + } + } + } + ] + }, + "models/block/{gem}_bars_post.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_bars", + "bars": "basegems:block/{gem}_bars" + }, + "elements": [ + { + "from": [ + 8, + 0, + 7 + ], + "to": [ + 8, + 16, + 9 + ], + "faces": { + "west": { + "uv": [ + 7, + 0, + 9, + 16 + ], + "texture": "#bars" + }, + "east": { + "uv": [ + 9, + 0, + 7, + 16 + ], + "texture": "#bars" + } + } + }, + { + "from": [ + 7, + 0, + 8 + ], + "to": [ + 9, + 16, + 8 + ], + "faces": { + "north": { + "uv": [ + 7, + 0, + 9, + 16 + ], + "texture": "#bars" + }, + "south": { + "uv": [ + 9, + 0, + 7, + 16 + ], + "texture": "#bars" + } + } + } + ] + }, + "models/block/{gem}_bars_side.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_bars", + "bars": "basegems:block/{gem}_bars", + "edge": "basegems:block/{gem}_bars" + }, + "elements": [ + { + "from": [ + 8, + 0, + 0 + ], + "to": [ + 8, + 16, + 8 + ], + "faces": { + "west": { + "uv": [ + 16, + 0, + 8, + 16 + ], + "texture": "#bars" + }, + "east": { + "uv": [ + 8, + 0, + 16, + 16 + ], + "texture": "#bars" + } + } + }, + { + "from": [ + 7, + 0, + 0 + ], + "to": [ + 9, + 16, + 7 + ], + "faces": { + "north": { + "uv": [ + 7, + 0, + 9, + 16 + ], + "texture": "#edge", + "cullface": "north" + } + } + }, + { + "from": [ + 7, + 0.001, + 0 + ], + "to": [ + 9, + 0.001, + 7 + ], + "faces": { + "down": { + "uv": [ + 9, + 0, + 7, + 7 + ], + "texture": "#edge" + }, + "up": { + "uv": [ + 7, + 0, + 9, + 7 + ], + "texture": "#edge" + } + } + }, + { + "from": [ + 7, + 15.999, + 0 + ], + "to": [ + 9, + 15.999, + 7 + ], + "faces": { + "down": { + "uv": [ + 9, + 0, + 7, + 7 + ], + "texture": "#edge" + }, + "up": { + "uv": [ + 7, + 0, + 9, + 7 + ], + "texture": "#edge" + } + } + } + ] + }, + "models/block/{gem}_bars_side_alt.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_bars", + "bars": "basegems:block/{gem}_bars", + "edge": "basegems:block/{gem}_bars" + }, + "elements": [ + { + "from": [ + 8, + 0, + 8 + ], + "to": [ + 8, + 16, + 16 + ], + "faces": { + "west": { + "uv": [ + 8, + 0, + 0, + 16 + ], + "texture": "#bars" + }, + "east": { + "uv": [ + 0, + 0, + 8, + 16 + ], + "texture": "#bars" + } + } + }, + { + "from": [ + 7, + 0, + 9 + ], + "to": [ + 9, + 16, + 16 + ], + "faces": { + "south": { + "uv": [ + 7, + 0, + 9, + 16 + ], + "texture": "#edge", + "cullface": "south" + }, + "down": { + "uv": [ + 9, + 9, + 7, + 16 + ], + "texture": "#edge" + }, + "up": { + "uv": [ + 7, + 9, + 9, + 16 + ], + "texture": "#edge" + } + } + }, + { + "from": [ + 7, + 0.001, + 9 + ], + "to": [ + 9, + 0.001, + 16 + ], + "faces": { + "down": { + "uv": [ + 9, + 9, + 7, + 16 + ], + "texture": "#edge" + }, + "up": { + "uv": [ + 7, + 9, + 9, + 16 + ], + "texture": "#edge" + } + } + }, + { + "from": [ + 7, + 15.999, + 9 + ], + "to": [ + 9, + 15.999, + 16 + ], + "faces": { + "down": { + "uv": [ + 9, + 9, + 7, + 16 + ], + "texture": "#edge" + }, + "up": { + "uv": [ + 7, + 9, + 9, + 16 + ], + "texture": "#edge" + } + } + } + ] + }, + "models/block/{gem}_block.json": { + "parent": "block/cube_all", + "textures": { + "all": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_button.json": { + "parent": "block/button", + "textures": { + "texture": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_button_inventory.json": { + "parent": "block/button_inventory", + "textures": { + "texture": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_button_pressed.json": { + "parent": "block/button_pressed", + "textures": { + "texture": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_door_bottom.json": { + "parent": "block/door_bottom", + "textures": { + "bottom": "basegems:block/{gem}_door_lower", + "top": "basegems:block/{gem}_door_upper" + } + }, + "models/block/{gem}_door_bottom_rh.json": { + "parent": "block/door_bottom_rh", + "textures": { + "bottom": "basegems:block/{gem}_door_lower", + "top": "basegems:block/{gem}_door_upper" + } + }, + "models/block/{gem}_door_top.json": { + "parent": "block/door_top", + "textures": { + "bottom": "basegems:block/{gem}_door_lower", + "top": "basegems:block/{gem}_door_upper" + } + }, + "models/block/{gem}_door_top_rh.json": { + "parent": "block/door_top_rh", + "textures": { + "bottom": "basegems:block/{gem}_door_lower", + "top": "basegems:block/{gem}_door_upper" + } + }, + "models/block/{gem}_inner_stairs.json": { + "parent": "block/inner_stairs", + "textures": { + "bottom": "basegems:block/{gem}_block", + "top": "basegems:block/{gem}_block", + "side": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_lever.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_block", + "base": "basegems:block/{gem}_block", + "lever": "minecraft:block/lever" + }, + "elements": [ + { + "from": [ + 5, + 0, + 4 + ], + "to": [ + 11, + 3, + 12 + ], + "faces": { + "down": { + "uv": [ + 5, + 4, + 11, + 12 + ], + "texture": "#base" + }, + "up": { + "uv": [ + 5, + 4, + 11, + 12 + ], + "texture": "#base" + }, + "north": { + "uv": [ + 5, + 0, + 11, + 3 + ], + "texture": "#base" + }, + "south": { + "uv": [ + 5, + 0, + 11, + 3 + ], + "texture": "#base" + }, + "west": { + "uv": [ + 4, + 0, + 12, + 3 + ], + "texture": "#base" + }, + "east": { + "uv": [ + 4, + 0, + 12, + 3 + ], + "texture": "#base" + } + } + }, + { + "from": [ + 7, + 1, + 7 + ], + "to": [ + 9, + 11, + 9 + ], + "rotation": { + "origin": [ + 8, + 1, + 8 + ], + "axis": "x", + "angle": -45 + }, + "faces": { + "down": { + "uv": [ + 7, + 6, + 9, + 8 + ], + "texture": "#lever" + }, + "up": { + "uv": [ + 7, + 6, + 9, + 8 + ], + "texture": "#lever" + }, + "north": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + }, + "south": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + }, + "west": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + }, + "east": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + } + } + } + ] + }, + "models/block/{gem}_lever_off.json": { + "ambientocclusion": false, + "textures": { + "particle": "basegems:block/{gem}_block", + "base": "basegems:block/{gem}_block", + "lever": "minecraft:block/lever" + }, + "elements": [ + { + "from": [ + 5, + 0, + 4 + ], + "to": [ + 11, + 3, + 12 + ], + "faces": { + "down": { + "uv": [ + 5, + 4, + 11, + 12 + ], + "texture": "#base" + }, + "up": { + "uv": [ + 5, + 4, + 11, + 12 + ], + "texture": "#base" + }, + "north": { + "uv": [ + 5, + 0, + 11, + 3 + ], + "texture": "#base" + }, + "south": { + "uv": [ + 5, + 0, + 11, + 3 + ], + "texture": "#base" + }, + "west": { + "uv": [ + 4, + 0, + 12, + 3 + ], + "texture": "#base" + }, + "east": { + "uv": [ + 4, + 0, + 12, + 3 + ], + "texture": "#base" + } + } + }, + { + "from": [ + 7, + 1, + 7 + ], + "to": [ + 9, + 11, + 9 + ], + "rotation": { + "origin": [ + 8, + 1, + 8 + ], + "axis": "x", + "angle": 45 + }, + "faces": { + "down": { + "uv": [ + 7, + 6, + 9, + 8 + ], + "texture": "#lever" + }, + "up": { + "uv": [ + 7, + 6, + 9, + 8 + ], + "texture": "#lever" + }, + "north": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + }, + "south": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + }, + "west": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + }, + "east": { + "uv": [ + 7, + 6, + 9, + 16 + ], + "texture": "#lever" + } + } + } + ] + }, + "models/block/{gem}_ore.json": { + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/stone", + "base": "minecraft:block/stone", + "overlay": "basegems:blocks/ore_overlays/{gem}_ore" + }, + "elements": [ + { + "from": [ + 0.0, + 0.0, + 0.0 + ], + "to": [ + 16.0, + 16.0, + 16.0 + ], + "faces": { + "down": { + "texture": "#base", + "cullface": "down" + }, + "up": { + "texture": "#base", + "cullface": "up" + }, + "north": { + "texture": "#base", + "cullface": "north" + }, + "south": { + "texture": "#base", + "cullface": "south" + }, + "west": { + "texture": "#base", + "cullface": "west" + }, + "east": { + "texture": "#base", + "cullface": "east" + } + } + }, + { + "from": [ + -0.001, + -0.001, + -0.001 + ], + "to": [ + 16.001, + 16.001, + 16.001 + ], + "faces": { + "down": { + "texture": "#overlay", + "cullface": "down" + }, + "up": { + "texture": "#overlay", + "cullface": "up" + }, + "north": { + "texture": "#overlay", + "cullface": "north" + }, + "south": { + "texture": "#overlay", + "cullface": "south" + }, + "west": { + "texture": "#overlay", + "cullface": "west" + }, + "east": { + "texture": "#overlay", + "cullface": "east" + } + } + } + ] + }, + "models/block/{gem}_outer_stairs.json": { + "parent": "block/outer_stairs", + "textures": { + "bottom": "basegems:block/{gem}_block", + "top": "basegems:block/{gem}_block", + "side": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_plate.json": { + "parent": "basegems:block/plate", + "textures": { + "top": "basegems:block/{gem}_plate", + "side": "basegems:block/{gem}_plate", + "bottom": "basegems:block/{gem}_plate" + } + }, + "models/block/{gem}_pressure_plate_down.json": { + "parent": "block/pressure_plate_down", + "textures": { + "texture": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_pressure_plate_up.json": { + "parent": "block/pressure_plate_up", + "textures": { + "texture": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_stairs.json": { + "parent": "block/stairs", + "textures": { + "bottom": "basegems:block/{gem}_block", + "top": "basegems:block/{gem}_block", + "side": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_trapdoor_bottom.json": { + "parent": "minecraft:block/template_orientable_trapdoor_bottom", + "textures": { + "texture": "basegems:block/{gem}_trapdoor" + } + }, + "models/block/{gem}_trapdoor_open.json": { + "parent": "minecraft:block/template_orientable_trapdoor_open", + "textures": { + "texture": "basegems:block/{gem}_trapdoor" + } + }, + "models/block/{gem}_trapdoor_top.json": { + "parent": "minecraft:block/template_orientable_trapdoor_top", + "textures": { + "texture": "basegems:block/{gem}_trapdoor" + } + }, + "models/block/{gem}_wall_inventory.json": { + "parent": "block/wall_inventory", + "textures": { + "wall": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_wall_post.json": { + "parent": "minecraft:block/template_wall_post", + "textures": { + "wall": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_wall_side.json": { + "parent": "minecraft:block/template_wall_side", + "textures": { + "wall": "basegems:block/{gem}_block" + } + }, + "models/block/{gem}_wall_side_tall.json": { + "parent": "minecraft:block/template_wall_side_tall", + "textures": { + "wall": "basegems:block/{gem}_block" + } + }, + "models/block/plate.json": { + "textures": { + "particle": "#top" + }, + "elements": [ + { + "from": [ + 0, + 0, + 15 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 16, + 1 + ], + "texture": "#side", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 16, + 1 + ], + "texture": "#side", + "cullface": "up" + }, + "west": { + "uv": [ + 15, + 0, + 16, + 16 + ], + "texture": "#side", + "cullface": "west" + }, + "east": { + "uv": [ + 15, + 0, + 16, + 16 + ], + "texture": "#side", + "cullface": "east" + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#bottom", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#top" + } + } + } + ], + "display": { + "gui": { + "rotation": [ + 0, + 0, + 0 + ], + "translation": [ + 0, + 0, + 0 + ], + "scale": [ + 0.625, + 0.625, + 0.625 + ] + }, + "ground": { + "rotation": [ + 0, + 0, + 0 + ], + "translation": [ + 0, + 3, + 0 + ], + "scale": [ + 0.25, + 0.25, + 0.25 + ] + }, + "fixed": { + "rotation": [ + 0, + 0, + 0 + ], + "translation": [ + 0, + 0, + 0 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ] + }, + "thirdperson_righthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "firstperson_righthand": { + "rotation": [ + 0, + 45, + 0 + ], + "translation": [ + 0, + 0, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "firstperson_lefthand": { + "rotation": [ + 0, + 225, + 0 + ], + "translation": [ + 0, + 0, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + } + } + } +} diff --git a/gradle/normalize-javadoc-trees.gradle b/gradle/normalize-javadoc-trees.gradle new file mode 100644 index 0000000..463adfb --- /dev/null +++ b/gradle/normalize-javadoc-trees.gradle @@ -0,0 +1,28 @@ +// Java 8's doclet leaves enum siblings in platform-dependent order. +// Keep the full hierarchy while giving leaf-only sibling lists a stable order. +tasks.withType(Javadoc).configureEach { + inputs.property('javadocTreeOrder', 'lexical leaf siblings and nullable alignment 2') + doLast { + fileTree(destinationDir) { include '**/*-tree.html' }.files.each { page -> + String original = page.getText('UTF-8') + String ordered = original.replaceAll( + /(
    \r?\n)((?:
  • [^\r\n]*<\/li>\r?\n){2,})(<\/ul>)/) { + all, opening, siblings, closing -> + opening + siblings.readLines().sort().join('\n') + '\n' + closing + } + if (ordered != original) page.setText(ordered, 'UTF-8') + } + // The Java 8 doclet counts CRLF in @Nullable when aligning wrapped parameters. + fileTree(destinationDir) { include '**/*.html' }.files.each { page -> + String original = page.getText('UTF-8') + String aligned = original.replaceAll( + /(
    @Nullable\r?\n)(public[^\r\n]*?\()([^\r\n]*\r?\n)( +)([^ \r\n][^\r\n]*\)<\/pre>)/) {
    +                all, annotation, prefix, first, indentation, last ->
    +                String visible = prefix.replaceAll(/<[^>]+>/, '')
    +                        .replace(' ', ' ').replace('<', '<').replace('>', '>')
    +                annotation + prefix + first + (' ' * visible.length()) + last
    +            }
    +            if (aligned != original) page.setText(aligned, 'UTF-8')
    +        }
    +    }
    +}
    diff --git a/gradle/ore-models.gradle b/gradle/ore-models.gradle
    deleted file mode 100644
    index 303e20c..0000000
    --- a/gradle/ore-models.gradle
    +++ /dev/null
    @@ -1,75 +0,0 @@
    -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/basegems')
    -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'
    -}
    -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/basegems/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: 'basegems:block/ore_overlay',
    -                    textures: [particle: 'minecraft:blocks/stone', base: 'minecraft:blocks/stone',
    -                               overlay: "basegems:blocks/ore_overlays/${name}"]
    -                ])
    -            } else {
    -                writeModel(name, [
    -                    parent: 'block/cube_all',
    -                    textures: [all: "basegems: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/recipe-advancements.gradle b/gradle/recipe-advancements.gradle
    deleted file mode 100644
    index 541aa5c..0000000
    --- a/gradle/recipe-advancements.gradle
    +++ /dev/null
    @@ -1,67 +0,0 @@
    -import groovy.json.JsonOutput
    -
    -def gemOres = fileTree('src/main/resources/assets/basegems/blockstates') { include '*_ore.json' }
    -def generated = layout.buildDirectory.dir('generated/recipe-advancements')
    -
    -// Match the stable recipe names in GemRecipes and use a main ingredient to introduce each one.
    -def recipesByIngredient = [
    -    ingot: ['block', 'unpack_gem', 'plate', 'lever', 'bars', 'pressure_plate', 'door',
    -            'trapdoor', 'rod', 'axe', 'hoe', 'pickaxe', 'shovel', 'sword', 'shears', 'shield'],
    -    block: ['unpack_block', 'slab', 'stairs', 'wall', 'crackhammer'],
    -    nugget: ['ingot', 'button', 'arrow'],
    -    rod: ['bars_from_rods', 'gear', 'bow', 'bolt', 'crossbow', 'fishing_rod'],
    -    plate: ['repair_shield'],
    -    smallpowder: ['powder'],
    -    powder: ['unpack_powder']
    -]
    -
    -def generateRecipeAdvancements = tasks.register('generateRecipeAdvancements') {
    -    group = 'build'
    -    description = 'Generates recipe book unlocks for Base Gems.'
    -    inputs.files(gemOres).withPathSensitivity(PathSensitivity.NAME_ONLY)
    -    inputs.property('recipesByIngredient', recipesByIngredient)
    -    outputs.dir(generated)
    -
    -    doLast {
    -        def directory = generated.get().dir('assets/basegems/advancements/recipes').asFile
    -        directory.mkdirs()
    -        Set current = [] as Set
    -        gemOres.files.collect { it.name - '_ore.json' }.sort().each { gem ->
    -            recipesByIngredient.each { ingredient, recipes ->
    -                String item = "basegems:${gem}_${ingredient}"
    -                recipes.each { name ->
    -                    String recipe = "basegems:${gem}_${name}"
    -                    String filename = "${gem}_${name}.json"
    -                    current.add(filename)
    -                    def advancement = [
    -                        parent: 'minecraft:recipes/root',
    -                        conditions: [
    -                            [type: 'minecraft:item_exists', item: item],
    -                            [type: 'basegems:book_recipe', recipe: recipe]
    -                        ],
    -                        rewards: [recipes: [recipe]],
    -                        criteria: [
    -                            has_ingredient: [trigger: 'minecraft:inventory_changed', conditions: [items: [[item: item]]]],
    -                            has_recipe: [trigger: 'minecraft:recipe_unlocked', conditions: [recipe: recipe]]
    -                        ],
    -                        requirements: [['has_ingredient', 'has_recipe']]
    -                    ]
    -                    new File(directory, filename).setText(JsonOutput.prettyPrint(JsonOutput.toJson(advancement)) + '\n', 'UTF-8')
    -                }
    -            }
    -        }
    -        directory.listFiles().findAll { it.isFile() && it.name.endsWith('.json') && !current.contains(it.name) }.each {
    -            if (!it.delete()) throw new GradleException("Could not remove stale recipe advancement ${it.name}")
    -        }
    -    }
    -}
    -
    -tasks.named('processResources', ProcessResources) {
    -    dependsOn generateRecipeAdvancements
    -    from generated
    -}
    -
    -tasks.named('sourcesJar', Jar) {
    -    dependsOn generateRecipeAdvancements
    -    from generated
    -}
    diff --git a/gradle/release-113.gradle b/gradle/release-113.gradle
    new file mode 100644
    index 0000000..10d47ea
    --- /dev/null
    +++ b/gradle/release-113.gradle
    @@ -0,0 +1,197 @@
    +import org.apache.tools.ant.filters.FixCrLfFilter
    +import java.security.MessageDigest
    +import java.util.jar.Manifest
    +import java.util.zip.ZipFile
    +import groovy.xml.XmlSlurper
    +
    +// Publish the same audited files that were built and tested by merged-source CI.
    +def archiveName = 'BaseGems'
    +def modId = 'basegems'
    +def expectedGroup = 'zone.moddev.mc.' + modId
    +def playerJar = tasks.named('renameJar')
    +def releaseNames = ['', '-sources', '-javadoc'].collect { "${archiveName}-${project.version}${it}.jar".toString() }
    +def preparedDirectory = providers.gradleProperty('preparedReleaseDir')
    +def textPatterns = ['cfg', 'css', 'html', 'info', 'java', 'js', 'json', 'lang', 'mcmeta', 'md', 'properties', 'txt', 'xml', 'toml', 'list']
    +        .collect { "**/*.${it}".toString() } + ['**/package-list', '**/element-list']
    +
    +tasks.withType(AbstractArchiveTask).configureEach {
    +    inputs.property('releaseArchiveTextFormat', 'UTF-8 LF 2')
    +    preserveFileTimestamps = false
    +    reproducibleFileOrder = true
    +    includeEmptyDirs = false
    +    exclude '**/desktop.ini', '**/Thumbs.db', '**/.DS_Store'
    +    filesMatching(textPatterns) {
    +        filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('lf'))
    +    }
    +}
    +tasks.withType(Jar).configureEach { filteringCharset = 'UTF-8' }
    +tasks.withType(Javadoc).configureEach {
    +    inputs.property('releaseJavadocFormat', 'UTF-8 sorted sources 1')
    +    setSource(files(project.providers.provider {
    +        project.sourceSets.main.allJava.files.sort { project.relativePath(it).replace('\\', '/') }
    +    }))
    +    options.addStringOption('docencoding', 'UTF-8')
    +    options.addStringOption('charset', 'UTF-8')
    +    options.addBooleanOption('notimestamp', true)
    +}
    +tasks.named('jar', Jar) {
    +    manifest.attributes('Implementation-Title': archiveName, 'Implementation-Version': project.version,
    +            'Maven-Artifact': "${expectedGroup}:${archiveName}:${project.version}".toString(),
    +            'Build-Target': 'minecraft-1.13.2-forge-25.0.223-java-8')
    +}
    +tasks.named('assemble') { dependsOn playerJar, tasks.named('sourcesJar'), tasks.named('javadocJar') }
    +
    +def sha256 = { File file -> MessageDigest.getInstance('SHA-256').digest(file.bytes).encodeHex().toString() }
    +def audit = { File directory ->
    +    if (project.group.toString() != expectedGroup || !(project.version.toString() ==~ /\d+\.\d+\.\d+\.113021/)) {
    +        throw new GradleException('Wrong Maven or Minecraft release identity')
    +    }
    +    if (directory.listFiles().findAll { it.name.endsWith('.jar') }.collect { it.name }.sort() != releaseNames.sort(false)) {
    +        throw new GradleException('The release directory must contain exactly the three current jars')
    +    }
    +    releaseNames.each { name ->
    +        File file = new File(directory, name)
    +        if (!file.isFile() || file.length() == 0) throw new GradleException("Missing ${name}")
    +        new ZipFile(file).withCloseable { zip ->
    +            zip.entries().each { entry ->
    +                String path = entry.name.toLowerCase(Locale.ROOT)
    +                if (path.contains('agent-notes') || path.contains('agents.md') || path.contains('.codex')
    +                        || path.startsWith('tools/') || path.startsWith('reference/') || path.contains('/testmod/')
    +                        || path.startsWith('com/mcmoddev/lib/') || path.startsWith('zone/moddev/mc/mmdlib/')
    +                        || path.startsWith('zone/moddev/mc/basemetals/') || path.startsWith('com/mcmoddev/orespawn/')
    +                        || path.startsWith('net/minecraft/') || path.startsWith('org/junit/')) {
    +                    throw new GradleException("Non-production entry in ${name}: ${entry.name}")
    +                }
    +                if (entry.name.endsWith('.class')) {
    +                    byte[] bytes = zip.getInputStream(entry).bytes
    +                    int major = ((bytes[6] & 255) << 8) | (bytes[7] & 255)
    +                    if (major != 52) throw new GradleException("Non-Java-8 class: ${entry.name}")
    +                }
    +            }
    +            if (name == releaseNames[0]) {
    +                def manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))).mainAttributes
    +                if (manifest.getValue('Maven-Artifact') != "${expectedGroup}:${archiveName}:${project.version}") {
    +                    throw new GradleException('Wrong release manifest')
    +                }
    +                String metadata = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8')
    +                if (metadata.contains('${') || !metadata.contains('modId="' + modId + '"')
    +                        || !metadata.contains('version="' + project.version + '"')
    +                        || !metadata.contains('versionRange="[1.13.2]"')
    +                        || !metadata.contains('versionRange="[4.1.7.113021,5.0.0)"')) {
    +                    throw new GradleException('Wrong or unresolved release mod metadata')
    +                }
    +            }
    +        }
    +    }
    +}
    +
    +tasks.register('verify113ReleaseArtifacts') {
    +    group = 'verification'
    +    dependsOn tasks.named('assemble'), tasks.named('check')
    +    doLast { audit(layout.buildDirectory.dir('libs').get().asFile) }
    +}
    +tasks.register('write113ReleaseChecksums') {
    +    group = 'verification'
    +    dependsOn tasks.named('verify113ReleaseArtifacts')
    +    doLast {
    +        File output = layout.buildDirectory.file('release/SHA256SUMS').get().asFile
    +        output.parentFile.mkdirs()
    +        output.setText(releaseNames.sort(false).collect { name ->
    +            sha256(layout.buildDirectory.file('libs/' + name).get().asFile) + '  ' + name
    +        }.join('\n') + '\n', 'UTF-8')
    +    }
    +}
    +tasks.register('verify113PreparedReleaseArtifacts') {
    +    group = 'verification'
    +    doLast {
    +        if (!preparedDirectory.isPresent()) throw new GradleException('preparedReleaseDir is required')
    +        File directory = file(preparedDirectory.get())
    +        if (directory.listFiles().collect { it.name }.sort() != (releaseNames + ['SHA256SUMS', 'CHANGELOG.txt']).sort(false)) {
    +            throw new GradleException('Unexpected prepared bundle contents')
    +        }
    +        audit(directory)
    +        Map checksums = [:]
    +        new File(directory, 'SHA256SUMS').readLines('UTF-8').each { line ->
    +            def match = line =~ /^([0-9a-f]{64})  (.+)$/
    +            if (!match.matches()) throw new GradleException('Invalid prepared checksum entry')
    +            if (checksums.containsKey(match[0][2])) throw new GradleException('Duplicate prepared checksum entry')
    +            checksums[match[0][2]] = match[0][1]
    +        }
    +        if (checksums.keySet().sort() != releaseNames.sort(false)) throw new GradleException('Wrong prepared jar inventory')
    +        checksums.each { name, hash ->
    +            if (sha256(new File(directory, name)) != hash) throw new GradleException("Checksum mismatch: ${name}")
    +        }
    +        if (!new File(directory, 'CHANGELOG.txt').readLines('UTF-8')[0].contains(project.version.toString())) {
    +            throw new GradleException('Prepared changelog belongs to another release')
    +        }
    +    }
    +}
    +
    +publishing {
    +    publications {
    +        mavenJava(MavenPublication) {
    +            groupId = expectedGroup
    +            artifactId = archiveName
    +            version = project.version.toString()
    +            setArtifacts([])
    +            if (preparedDirectory.isPresent()) {
    +                artifact new File(preparedDirectory.get(), releaseNames[0])
    +                artifact(new File(preparedDirectory.get(), releaseNames[1])) { classifier = 'sources' }
    +                artifact(new File(preparedDirectory.get(), releaseNames[2])) { classifier = 'javadoc' }
    +            } else {
    +                artifact playerJar
    +                artifact tasks.named('sourcesJar')
    +                artifact tasks.named('javadocJar')
    +            }
    +            pom {
    +                name = 'Base Gems'
    +                description = 'Base Gems for Minecraft 1.13.2, using Forge 25 and OreSpawn 4.'
    +                url = 'https://github.com/MinecraftModDevelopmentMods/BaseGems'
    +                licenses {
    +                    license {
    +                        name = 'GNU Lesser General Public License, Version 2.1'
    +                        url = 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt'
    +                    }
    +                }
    +            }
    +            pom.withXml {
    +                def dependency = asNode().appendNode('dependencies').appendNode('dependency')
    +                dependency.appendNode('groupId', 'zone.moddev.mc.basemetals')
    +                dependency.appendNode('artifactId', 'BaseMetals')
    +                dependency.appendNode('version', '3.0.3.113021')
    +                dependency.appendNode('optional', 'true')
    +            }
    +        }
    +    }
    +    repositories {
    +        maven {
    +            name = 'release'
    +            url = uri(providers.environmentVariable('MAVEN_UPLOAD_URL').getOrElse('https://invalid.invalid/missing-upload-url'))
    +            credentials {
    +                username = providers.environmentVariable('MAVEN_UPLOAD_USERNAME').getOrElse('')
    +                password = providers.environmentVariable('MAVEN_UPLOAD_PASSWORD').getOrElse('')
    +            }
    +        }
    +    }
    +}
    +tasks.register('verify113MavenCoordinates') {
    +    dependsOn 'generatePomFileForMavenJavaPublication'
    +    doLast {
    +        def pom = new XmlSlurper(false, false).parse(layout.buildDirectory.file('publications/mavenJava/pom-default.xml').get().asFile)
    +        if ([pom.groupId.text(), pom.artifactId.text(), pom.version.text()] != [expectedGroup, archiveName, project.version.toString()]) {
    +            throw new GradleException('Wrong Maven publication coordinates')
    +        }
    +    }
    +}
    +tasks.withType(PublishToMavenRepository).configureEach {
    +    dependsOn 'verify113MavenCoordinates'
    +    dependsOn preparedDirectory.isPresent() ? 'verify113PreparedReleaseArtifacts' : 'verify113ReleaseArtifacts'
    +    doFirst {
    +        def url = providers.environmentVariable('MAVEN_UPLOAD_URL').getOrElse('')
    +        if (!url.startsWith('https://') || url.startsWith('https://invalid.invalid')
    +                || !providers.environmentVariable('MAVEN_UPLOAD_USERNAME').getOrElse('').trim()
    +                || !providers.environmentVariable('MAVEN_UPLOAD_PASSWORD').getOrElse('').trim()) {
    +            throw new GradleException('Configure the HTTPS Maven upload URL, username and password')
    +        }
    +    }
    +}
    diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle
    index be899ad..e853298 100644
    --- a/gradle/release/artifacts.gradle
    +++ b/gradle/release/artifacts.gradle
    @@ -19,7 +19,7 @@ tasks.register('verifyJava8Toolchain') {
         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 (compiler.languageVersion.asInt() != 8) throw new GradleException('Minecraft 1.13 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')) {
    @@ -38,25 +38,11 @@ tasks.register('verifyReleaseConfiguration') {
         group = 'verification'
         dependsOn tasks.named('verifyJava8Toolchain')
         doLast {
    -        if (project.group != 'zone.moddev.mc.basegems' || project.version != '2.5.0.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 Gems target, mappings or Maven identity changed.')
    -        }
    -        String source = file('src/main/java/zone/moddev/mc/basegems/BaseGems.java').getText('UTF-8')
    -        if (!source.contains('VERSION = "' + project.version + '"') || !source.contains('MODID = "basegems"')) {
    -            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-rc3'
    -                || project.mmdlib_version_range != '[1.0.0-rc2,2.0.0)'
    -                || !source.contains('required-after:mmdlib@' + project.mmdlib_version_range)) {
    -            throw new GradleException('The MMDLib runtime requirement changed.')
    +        if (project.group != 'zone.moddev.mc.basegems' || project.version != '2.5.0.113021'
    +                || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223'
    +                || project.mappings_channel != 'snapshot' || project.mappings_version != '20180921-1.13'
    +                || project.orespawn_version != '4.1.7.113021') {
    +            throw new GradleException('Base Gems target, mappings or dependency pin changed.')
             }
         }
     }
    @@ -117,9 +103,10 @@ def auditJars = { File directory ->
                                 || 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 != 'basegems' || info.version != project.version || info.mcversion != '1.12.2') {
    -                        throw new GradleException('Incorrect processed mcmod.info.')
    +                    def info = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8')
    +                    if (!info.contains('modId="basegems"') || !info.contains('version="' + project.version + '"')
    +                            || !info.contains('versionRange="[1.13.2]"')) {
    +                        throw new GradleException('Incorrect processed mods.toml.')
                         }
                         if (zip.getEntry('zone/moddev/mc/basegems/BaseGems.class') == null) {
                             throw new GradleException('Missing mod entry point.')
    diff --git a/gradle/resources.gradle b/gradle/resources.gradle
    new file mode 100644
    index 0000000..e058e93
    --- /dev/null
    +++ b/gradle/resources.gradle
    @@ -0,0 +1,23 @@
    +def generated = layout.buildDirectory.dir('generated/basegems-resources')
    +def generator = tasks.register('generateGemResources', Exec) {
    +    def python = providers.gradleProperty('pythonExecutable')
    +            .orElse(org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'python' : 'python3')
    +    inputs.file 'gradle/generate-resources.py'
    +    inputs.file 'gradle/model-templates.json'
    +    inputs.dir 'src/main/resources/assets/basegems'
    +    outputs.dir generated
    +    commandLine python.get(), 'gradle/generate-resources.py', generated.get().asFile.absolutePath
    +}
    +
    +// The original models and locale files are the inputs for the flattened pack.
    +sourceSets.main.resources {
    +    exclude 'assets/basegems/blockstates/**', 'assets/basegems/models/**', 'assets/basegems/advancements/**'
    +}
    +tasks.named('processResources', ProcessResources) {
    +    dependsOn generator
    +    from generated
    +}
    +tasks.named('sourcesJar', Jar) {
    +    dependsOn generator
    +    from generated
    +}
    diff --git a/gradle/stage-legacy-dependencies.py b/gradle/stage-legacy-dependencies.py
    index 4803075..507b7aa 100644
    --- a/gradle/stage-legacy-dependencies.py
    +++ b/gradle/stage-legacy-dependencies.py
    @@ -28,7 +28,7 @@ def main():
                 if cached:
                     data = cached[0].read_bytes()
                 else:
    -                url = spec["repository"].rstrip("/") + "/" + relative.as_posix()
    +                url = spec.get("download_url") or spec["repository"].rstrip("/") + "/" + relative.as_posix()
                     request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
                     for attempt in range(3):
                         try:
    diff --git a/src/main/java/zone/moddev/mc/basegems/BaseGems.java b/src/main/java/zone/moddev/mc/basegems/BaseGems.java
    index 39b4216..92535a1 100644
    --- a/src/main/java/zone/moddev/mc/basegems/BaseGems.java
    +++ b/src/main/java/zone/moddev/mc/basegems/BaseGems.java
    @@ -1,75 +1,42 @@
     package zone.moddev.mc.basegems;
     
    +import net.minecraftforge.fml.ModLoadingContext;
    +import net.minecraftforge.fml.DeferredWorkQueue;
    +import net.minecraftforge.fml.DistExecutor;
    +import net.minecraftforge.api.distmarker.Dist;
    +import net.minecraftforge.fml.common.Mod;
    +import net.minecraftforge.fml.config.ModConfig;
    +import net.minecraftforge.common.MinecraftForge;
    +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
    +import net.minecraftforge.fml.event.lifecycle.InterModEnqueueEvent;
    +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
     import org.apache.logging.log4j.LogManager;
     import org.apache.logging.log4j.Logger;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +import zone.moddev.mc.basegems.client.ClientSetup;
    +import zone.moddev.mc.basegems.entity.ModEntities;
    +import zone.moddev.mc.basegems.recipe.CrushingRecipe;
    +import zone.moddev.mc.basegems.trade.GemTrades;
    +import zone.moddev.mc.basegems.worldgen.GemWorldgen;
    +
    +@Mod(BaseGems.MOD_ID)
    +public final class BaseGems {
    +    public static final String MOD_ID = "basegems";
    +    public static final Logger LOGGER = LogManager.getLogger(MOD_ID);
    +
    +    public BaseGems() {
    +        ModEntities.initialize();
    +        ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, GemConfig.SPEC);
    +        FMLJavaModLoadingContext.get().getModEventBus().addListener(GemConfig::onConfigLoading);
    +        MinecraftForge.EVENT_BUS.register(new GemEvents());
    +        DistExecutor.runWhenOn(Dist.CLIENT, () -> ClientSetup::register);
    +        FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
    +        FMLJavaModLoadingContext.get().getModEventBus().addListener(this::enqueue);
    +    }
     
    -import zone.moddev.mc.basegems.proxy.CommonProxy;
    -import zone.moddev.mc.basegems.util.Config;
    -
    -import net.minecraftforge.fml.common.Mod;
    -import net.minecraftforge.fml.common.Mod.EventHandler;
    -import net.minecraftforge.fml.common.Mod.Instance;
    -import net.minecraftforge.fml.common.SidedProxy;
    -import net.minecraftforge.fml.common.event.FMLInitializationEvent;
    -import net.minecraftforge.fml.common.event.FMLConstructionEvent;
    -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
    -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
    -
    -/**
    - * This is the entry point for this Mod.
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -@Mod(
    -		modid = BaseGems.MODID,
    -		name = BaseGems.NAME,
    -		version = BaseGems.VERSION,
    -		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);before:buildingbricks",
    -		acceptedMinecraftVersions = "[1.12.2]",
    -		guiFactory = "zone.moddev.mc.basegems.client.BaseGemsGuiFactory")
    -public class BaseGems {
    -
    -	@Instance
    -	public static BaseGems instance;
    -
    -	/** ID of this Mod */
    -	public static final String MODID = "basegems";
    -
    -	/** Display name of this Mod */
    -	public static final String NAME = "Base Gems";
    -
    -	/**
    -	 * Version number, in Major.Minor.Build format. The minor number is
    -	 * increased whenever a change is made that has the potential to break
    -	 * compatibility with other mods that depend on this one.
    -	 */
    -	public static final String VERSION = "2.5.0.112021";
    -
    -	private static final String PROXY_BASE = "zone.moddev.mc." + MODID + ".proxy.";
    -
    -	@SidedProxy(clientSide = PROXY_BASE + "ClientProxy", serverSide = PROXY_BASE + "ServerProxy")
    -	public static CommonProxy proxy;
    -
    -	public static final Logger logger = LogManager.getFormatterLogger(BaseGems.MODID);
    -
    -	@EventHandler
    -	public static void preInit(FMLPreInitializationEvent event) {
    -		proxy.preInit(event);
    -	}
    -
    -	@EventHandler
    -	public static void init(FMLInitializationEvent event) {
    -		proxy.init(event);
    -	}
    -
    -	@EventHandler
    -	public static void postInit(FMLPostInitializationEvent event) {
    -		proxy.postInit(event);
    -	}
    -
    -    @EventHandler
    -    public static void construct(FMLConstructionEvent event) {
    -        Config.init();
    +    private void setup(FMLCommonSetupEvent event) {
    +        DeferredWorkQueue.runLater(CrushingRecipe::register);
    +        GemTrades.register();
         }
    +    private void enqueue(InterModEnqueueEvent event) { GemWorldgen.enqueue(); }
     }
    diff --git a/src/main/java/zone/moddev/mc/basegems/GemEvents.java b/src/main/java/zone/moddev/mc/basegems/GemEvents.java
    new file mode 100644
    index 0000000..dff745d
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/GemEvents.java
    @@ -0,0 +1,63 @@
    +package zone.moddev.mc.basegems;
    +
    +import java.util.Collections;
    +
    +import net.minecraft.advancements.Advancement;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.entity.item.EntityItem;
    +import net.minecraft.entity.player.EntityPlayer;
    +import net.minecraft.entity.player.EntityPlayerMP;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.crafting.IRecipe;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.util.math.BlockPos;
    +import net.minecraft.world.World;
    +import net.minecraftforge.event.world.BlockEvent;
    +import net.minecraftforge.eventbus.api.EventPriority;
    +import net.minecraftforge.eventbus.api.SubscribeEvent;
    +import net.minecraftforge.fml.common.gameevent.PlayerEvent;
    +import zone.moddev.mc.basegems.content.CrackhammerItem;
    +import zone.moddev.mc.basegems.recipe.CrushingRecipe;
    +
    +final class GemEvents {
    +    @SubscribeEvent
    +    public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
    +        if (!(event.getPlayer() instanceof EntityPlayerMP)) return;
    +        EntityPlayerMP player = (EntityPlayerMP) event.getPlayer();
    +        for (IRecipe recipe : player.getServer().getRecipeManager().getRecipes()) {
    +            ResourceLocation id = recipe.getId();
    +            if (!BaseGems.MOD_ID.equals(id.getNamespace()) || recipe.isDynamic()
    +                    || player.getRecipeBook().isUnlocked(recipe)) continue;
    +            Advancement advancement = player.getServer().getAdvancementManager().getAdvancement(
    +                    new ResourceLocation(BaseGems.MOD_ID, "recipes/" + id.getPath()));
    +            if (advancement != null && player.getAdvancements().getProgress(advancement).isDone()) {
    +                player.unlockRecipes(Collections.singletonList(recipe));
    +            }
    +        }
    +    }
    +
    +    @SubscribeEvent(priority = EventPriority.LOWEST)
    +    public void onBreak(BlockEvent.BreakEvent event) {
    +        EntityPlayer player = event.getPlayer();
    +        World world = player.world;
    +        ItemStack held = player.getHeldItemMainhand();
    +        IBlockState state = event.getState();
    +        BlockPos pos = event.getPos();
    +        if (world.isRemote || player.isCreative() || !(held.getItem() instanceof CrackhammerItem)
    +                || !state.canHarvestBlock(world, pos, player)) return;
    +
    +        IRecipe recipe = CrushingRecipe.find(world, new ItemStack(state.getBlock()));
    +        if (recipe == null) return;
    +        event.setCanceled(true);
    +        state.getBlock().onBlockHarvested(world, pos, state, player);
    +        if (!world.removeBlock(pos)) return;
    +        state.getBlock().onPlayerDestroy(world, pos, state);
    +        EntityItem drop = new EntityItem(world, pos.getX() + 0.5, pos.getY() + 0.5,
    +                pos.getZ() + 0.5, recipe.getRecipeOutput().copy());
    +        drop.setDefaultPickupDelay();
    +        world.spawnEntity(drop);
    +        player.addStat(net.minecraft.stats.StatList.BLOCK_MINED.get(state.getBlock()));
    +        player.addExhaustion(0.005F);
    +        held.damageItem(1, player);
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/block/OverlayOreBlock.java b/src/main/java/zone/moddev/mc/basegems/block/OverlayOreBlock.java
    deleted file mode 100644
    index ff2ea45..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/block/OverlayOreBlock.java
    +++ /dev/null
    @@ -1,37 +0,0 @@
    -package zone.moddev.mc.basegems.block;
    -
    -import com.mcmoddev.lib.block.BlockMMDOre;
    -import com.mcmoddev.lib.data.Names;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import net.minecraft.block.state.IBlockState;
    -import net.minecraft.util.BlockRenderLayer;
    -import net.minecraft.util.math.BlockPos;
    -import net.minecraft.world.IBlockAccess;
    -import net.minecraftforge.fml.relauncher.Side;
    -import net.minecraftforge.fml.relauncher.SideOnly;
    -import zone.moddev.mc.basegems.data.GemRarity;
    -
    -/** A gem ore with mining XP and alpha testing for overlay models. */
    -public final class OverlayOreBlock extends BlockMMDOre {
    -    private final GemRarity rarity;
    -
    -    public OverlayOreBlock(MMDMaterial material) {
    -        super(material);
    -        this.rarity = GemRarity.of(material.getName());
    -    }
    -
    -    @Override
    -    public int getExpDrop(IBlockState state, IBlockAccess world, BlockPos pos, int fortune) {
    -        if (!getMMDMaterial().hasItem(Names.GEM)) {
    -            return 0;
    -        }
    -        // Forge calls this for a successful harvest without Silk Touch.
    -        return rarity.minXp + RANDOM.nextInt(rarity.maxXp - rarity.minXp + 1);
    -    }
    -
    -    @Override
    -    @SideOnly(Side.CLIENT)
    -    public BlockRenderLayer getRenderLayer() {
    -        return BlockRenderLayer.CUTOUT_MIPPED;
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/BaseGemsConfigScreen.java b/src/main/java/zone/moddev/mc/basegems/client/BaseGemsConfigScreen.java
    deleted file mode 100644
    index 7cba626..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/client/BaseGemsConfigScreen.java
    +++ /dev/null
    @@ -1,30 +0,0 @@
    -package zone.moddev.mc.basegems.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.basegems.BaseGems;
    -import zone.moddev.mc.basegems.util.Config;
    -
    -/** One parent-aware screen for Forge's mod list and OreSpawn's child-mod list. */
    -@SideOnly(Side.CLIENT)
    -public final class BaseGemsConfigScreen extends GuiConfig {
    -    public BaseGemsConfigScreen(final GuiScreen parent) {
    -        super(parent, elements(),
    -              BaseGems.MODID, false, true, I18n.format("config.basegems.title"),
    -              I18n.format("config.basegems.description"));
    -    }
    -
    -    private static List elements() {
    -        final List elements = new ArrayList<>(
    -                new ConfigElement(Config.getConfiguration().getCategory(Config.GENERAL_CAT)).getChildElements());
    -        elements.addAll(new ConfigElement(Config.getConfiguration().getCategory(Config.MATERIALS_CAT)).getChildElements());
    -        return elements;
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/BaseGemsGuiFactory.java b/src/main/java/zone/moddev/mc/basegems/client/BaseGemsGuiFactory.java
    deleted file mode 100644
    index 2524a63..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/client/BaseGemsGuiFactory.java
    +++ /dev/null
    @@ -1,18 +0,0 @@
    -package zone.moddev.mc.basegems.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 BaseGemsGuiFactory implements IModGuiFactory {
    -    @Override public void initialize(final Minecraft minecraft) { }
    -    @Override public boolean hasConfigGui() { return true; }
    -    @Override public GuiScreen createConfigGui(final GuiScreen parent) { return new BaseGemsConfigScreen(parent); }
    -    @Override public Set runtimeGuiCategories() { return Collections.emptySet(); }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/ClientMoltenGemRenderer.java b/src/main/java/zone/moddev/mc/basegems/client/ClientMoltenGemRenderer.java
    new file mode 100644
    index 0000000..9dbaefc
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/client/ClientMoltenGemRenderer.java
    @@ -0,0 +1,54 @@
    +package zone.moddev.mc.basegems.client;
    +
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.content.ModContent;
    +
    +import net.minecraft.client.Minecraft;
    +import net.minecraft.client.renderer.texture.TextureAtlasSprite;
    +import net.minecraft.fluid.IFluidState;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraftforge.api.distmarker.Dist;
    +import net.minecraftforge.api.distmarker.OnlyIn;
    +import net.minecraftforge.client.event.TextureStitchEvent;
    +import net.minecraftforge.common.MinecraftForge;
    +
    +/** Adds the fluid textures and colours that Forge 25 doesn't render on its own. */
    +@OnlyIn(Dist.CLIENT)
    +public final class ClientMoltenGemRenderer {
    +    private static final ResourceLocation STILL = new ResourceLocation(BaseGems.MOD_ID, "blocks/molten_metal_still");
    +    private static final ResourceLocation FLOW = new ResourceLocation(BaseGems.MOD_ID, "blocks/molten_metal_flow");
    +    private static volatile TextureAtlasSprite[] sprites;
    +
    +    private ClientMoltenGemRenderer() {}
    +
    +    public static void register() {
    +        MinecraftForge.EVENT_BUS.addListener(ClientMoltenGemRenderer::onTexturePre);
    +        MinecraftForge.EVENT_BUS.addListener(ClientMoltenGemRenderer::onTexturePost);
    +    }
    +
    +    private static void onTexturePre(TextureStitchEvent.Pre event) {
    +        event.getMap().registerSprite(Minecraft.getInstance().getResourceManager(), STILL);
    +        event.getMap().registerSprite(Minecraft.getInstance().getResourceManager(), FLOW);
    +    }
    +
    +    private static void onTexturePost(TextureStitchEvent.Post event) {
    +        sprites = new TextureAtlasSprite[] { event.getMap().getSprite(STILL), event.getMap().getSprite(FLOW) };
    +    }
    +
    +    public static boolean useOpaqueFluidPath(IFluidState state, boolean vanillaValue) {
    +        return isOurs(state) || vanillaValue;
    +    }
    +
    +    public static TextureAtlasSprite[] overrideSprites(IFluidState state, TextureAtlasSprite[] vanillaSprites) {
    +        TextureAtlasSprite[] current = sprites;
    +        return isOurs(state) && current != null ? current : vanillaSprites;
    +    }
    +
    +    public static int overrideColor(IFluidState state, int vanillaColor) {
    +        return isOurs(state) ? ModContent.fluidColour(state.getFluid()) : vanillaColor;
    +    }
    +
    +    private static boolean isOurs(IFluidState state) {
    +        return state != null && ModContent.isBaseGemsFluid(state.getFluid());
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/ClientSetup.java b/src/main/java/zone/moddev/mc/basegems/client/ClientSetup.java
    new file mode 100644
    index 0000000..c0b01c4
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/client/ClientSetup.java
    @@ -0,0 +1,35 @@
    +package zone.moddev.mc.basegems.client;
    +
    +import zone.moddev.mc.basegems.content.FluidContent;
    +import zone.moddev.mc.basegems.content.ModContent;
    +import zone.moddev.mc.basegems.entity.MaterialProjectile;
    +
    +import net.minecraftforge.client.event.ColorHandlerEvent;
    +import net.minecraftforge.common.MinecraftForge;
    +import net.minecraftforge.fml.client.registry.RenderingRegistry;
    +import net.minecraftforge.fml.ExtensionPoint;
    +import net.minecraftforge.fml.ModLoadingContext;
    +import zone.moddev.mc.orespawn.api.client.WorldSettingsExtensionRegistry;
    +
    +public final class ClientSetup {
    +    private ClientSetup() {}
    +
    +    public static void register() {
    +        ModLoadingContext.get().registerExtensionPoint(ExtensionPoint.CONFIGGUIFACTORY,
    +                () -> (minecraft, parent) -> new GemConfigScreen(parent));
    +        WorldSettingsExtensionRegistry.registerConfigScreen("basegems", GemConfigScreen::new);
    +        ClientMoltenGemRenderer.register();
    +        MinecraftForge.EVENT_BUS.addListener(ClientSetup::registerBucketColours);
    +        MinecraftForge.EVENT_BUS.addListener(ModsListConfigButton::onDraw);
    +        RenderingRegistry.registerEntityRenderingHandler(MaterialProjectile.class,
    +                manager -> new MaterialProjectileRenderer(manager));
    +    }
    +
    +    private static void registerBucketColours(ColorHandlerEvent.Item event) {
    +        for (final FluidContent content : ModContent.fluids().values()) {
    +            event.getItemColors().register((stack, tintIndex) -> tintIndex == 1
    +                    ? ModContent.fluidColour(content.source().get()) : 0xFFFFFF,
    +                    content.bucket().get());
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/GemConfigScreen.java b/src/main/java/zone/moddev/mc/basegems/client/GemConfigScreen.java
    new file mode 100644
    index 0000000..2b38938
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/client/GemConfigScreen.java
    @@ -0,0 +1,79 @@
    +package zone.moddev.mc.basegems.client;
    +
    +import java.util.LinkedHashMap;
    +import java.util.Map;
    +
    +import net.minecraft.client.gui.GuiButton;
    +import net.minecraft.client.gui.GuiScreen;
    +import net.minecraft.client.resources.I18n;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +/** Shared screen for the Forge Mods list and OreSpawn. */
    +final class GemConfigScreen extends GuiScreen {
    +    private final GuiScreen parent;
    +    private final Map enabled = new LinkedHashMap<>();
    +    private String mode;
    +    private int page;
    +
    +    GemConfigScreen(GuiScreen parent) {
    +        this.parent = parent;
    +        mode = GemConfig.configuredMode();
    +        for (Gem gem : Gem.ALL) enabled.put(gem.name(), GemConfig.configuredGem(gem.name()));
    +    }
    +
    +    @Override protected void initGui() {
    +        int left = width / 2 - 120;
    +        int rows = Math.max(1, (height - 104) / 23);
    +        int perPage = rows * 2;
    +        int lastPage = (Gem.ALL.size() - 1) / perPage;
    +        page = Math.min(page, lastPage);
    +        addButton(new Button(0, left, 29, 240, I18n.format("config.basegems.content_mode")
    +                + ": " + I18n.format(mode), () -> {
    +            mode = "low_fantasy".equals(mode) ? "high_fantasy" : "low_fantasy";
    +            mc.displayGuiScreen(this);
    +        }));
    +        for (int index = 0; index < perPage && page * perPage + index < Gem.ALL.size(); index++) {
    +            Gem gem = Gem.ALL.get(page * perPage + index);
    +            addButton(new Button(index + 10, left + (index % 2) * 122, 54 + (index / 2) * 23, 118,
    +                    I18n.format("config.basegems." + gem.configKey()) + ": " +
    +                            I18n.format(enabled.get(gem.name()) ? "options.on" : "options.off"), () -> {
    +                enabled.put(gem.name(), !enabled.get(gem.name()));
    +                mc.displayGuiScreen(this);
    +            }));
    +        }
    +        if (page > 0) addButton(new Button(1, left, height - 49, 118, "<", () -> {
    +            page--;
    +            mc.displayGuiScreen(this);
    +        }));
    +        if (page < lastPage) addButton(new Button(2, left + 122, height - 49, 118, ">", () -> {
    +            page++;
    +            mc.displayGuiScreen(this);
    +        }));
    +        addButton(new Button(3, left, height - 26, 118, I18n.format("gui.done"), () -> {
    +            GemConfig.save(mode, enabled);
    +            close();
    +        }));
    +        addButton(new Button(4, left + 122, height - 26, 118, I18n.format("gui.cancel"), this::close));
    +    }
    +
    +    @Override public void close() { mc.displayGuiScreen(parent); }
    +
    +    @Override public void render(int mouseX, int mouseY, float ticks) {
    +        drawDefaultBackground();
    +        drawCenteredString(fontRenderer, I18n.format("config.basegems.title"), width / 2, 8, 0xffffff);
    +        drawCenteredString(fontRenderer, I18n.format("config.basegems.restart"), width / 2, 19, 0xc0c0c0);
    +        super.render(mouseX, mouseY, ticks);
    +    }
    +
    +    private static final class Button extends GuiButton {
    +        private final Runnable click;
    +
    +        Button(int id, int x, int y, int width, String text, Runnable click) {
    +            super(id, x, y, width, 20, text);
    +            this.click = click;
    +        }
    +
    +        @Override public void onClick(double mouseX, double mouseY) { click.run(); }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/MaterialProjectileRenderer.java b/src/main/java/zone/moddev/mc/basegems/client/MaterialProjectileRenderer.java
    new file mode 100644
    index 0000000..0e55a34
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/client/MaterialProjectileRenderer.java
    @@ -0,0 +1,21 @@
    +package zone.moddev.mc.basegems.client;
    +
    +import zone.moddev.mc.basegems.entity.MaterialProjectile;
    +
    +import net.minecraft.client.renderer.entity.RenderArrow;
    +import net.minecraft.client.renderer.entity.RenderManager;
    +import net.minecraft.util.ResourceLocation;
    +
    +public final class MaterialProjectileRenderer extends RenderArrow {
    +    private static final ResourceLocation ARROW_TEXTURE =
    +            new ResourceLocation("textures/entity/projectiles/arrow.png");
    +
    +    public MaterialProjectileRenderer(RenderManager manager) {
    +        super(manager);
    +    }
    +
    +    @Override
    +    protected ResourceLocation getEntityTexture(MaterialProjectile projectile) {
    +        return ARROW_TEXTURE;
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/client/ModsListConfigButton.java b/src/main/java/zone/moddev/mc/basegems/client/ModsListConfigButton.java
    new file mode 100644
    index 0000000..93beb70
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/client/ModsListConfigButton.java
    @@ -0,0 +1,51 @@
    +package zone.moddev.mc.basegems.client;
    +
    +import java.lang.reflect.Field;
    +
    +import net.minecraft.client.gui.GuiButton;
    +import net.minecraftforge.client.event.GuiScreenEvent;
    +import net.minecraftforge.fml.client.gui.GuiModList;
    +import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
    +import zone.moddev.mc.basegems.BaseGems;
    +
    +/** Enables our Config button on Forge 25's Mods screen. */
    +final class ModsListConfigButton {
    +    private static final int CONFIG_BUTTON_ID = 20;
    +    private static final Field SELECTED_MOD = selectedModField();
    +
    +    private ModsListConfigButton() {}
    +
    +    static void onDraw(GuiScreenEvent.DrawScreenEvent.Pre event) {
    +        if (!(event.getGui() instanceof GuiModList)) return;
    +
    +        GuiModList screen = (GuiModList) event.getGui();
    +        ModInfo selected = selectedMod(screen);
    +        if (selected != null && !BaseGems.MOD_ID.equals(selected.getModId())) return;
    +
    +        // Forge 25's hasConfigUI() always returns false, even with a registered factory.
    +        screen.getChildren().stream()
    +                .filter(GuiButton.class::isInstance)
    +                .map(GuiButton.class::cast)
    +                .filter(button -> button.id == CONFIG_BUTTON_ID)
    +                .forEach(button -> button.enabled = selected != null);
    +    }
    +
    +    private static Field selectedModField() {
    +        // Forge keeps the selection private; the field isn't Minecraft-mapped.
    +        try {
    +            Field field = GuiModList.class.getDeclaredField("selectedMod");
    +            field.setAccessible(true);
    +            return field;
    +        } catch (NoSuchFieldException failure) {
    +            throw new IllegalStateException("Cannot read the selected mod on Forge's Mods screen", failure);
    +        }
    +    }
    +
    +    private static ModInfo selectedMod(GuiModList screen) {
    +        try {
    +            return (ModInfo) SELECTED_MOD.get(screen);
    +        } catch (IllegalAccessException failure) {
    +            throw new IllegalStateException("Cannot read the selected mod on Forge's Mods screen", failure);
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/config/GemConfig.java b/src/main/java/zone/moddev/mc/basegems/config/GemConfig.java
    new file mode 100644
    index 0000000..69feca1
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/config/GemConfig.java
    @@ -0,0 +1,74 @@
    +package zone.moddev.mc.basegems.config;
    +
    +import java.util.LinkedHashMap;
    +import java.util.Map;
    +
    +import net.minecraftforge.common.ForgeConfigSpec;
    +import net.minecraftforge.fml.config.ModConfig;
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +/** Registry names always exist. These options control new gameplay and recipes. */
    +public final class GemConfig {
    +    public static final ForgeConfigSpec SPEC;
    +    private static final ForgeConfigSpec.ConfigValue MODE;
    +    private static final Map ENABLED = new LinkedHashMap<>();
    +    private static volatile ModConfig loaded;
    +    private static volatile String activeMode = "high_fantasy";
    +    private static volatile Map activeGems = java.util.Collections.emptyMap();
    +    static {
    +        ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
    +        MODE = builder.comment("Choose high_fantasy or low_fantasy and restart Minecraft.")
    +                .worldRestart().define("general.contentMode", "high_fantasy",
    +                        value -> "high_fantasy".equals(value) || "low_fantasy".equals(value));
    +        for (Gem gem : Gem.ALL) {
    +            ENABLED.put(gem.name(), builder.comment("Enable new deposits, recipes and trades for " + gem.name() + ".")
    +                    .worldRestart().define("gems." + gem.configKey(), true));
    +        }
    +        SPEC = builder.build();
    +    }
    +
    +    private GemConfig() {}
    +
    +    public static void onConfigLoading(ModConfig.Loading event) {
    +        if (event.getConfig().getSpec() != SPEC) return;
    +        activeMode = MODE.get();
    +        Map options = new LinkedHashMap<>();
    +        ENABLED.forEach((gem, value) -> options.put(gem, value.get()));
    +        activeGems = java.util.Collections.unmodifiableMap(options);
    +        loaded = event.getConfig();
    +    }
    +
    +    public static boolean enabled(String gem) {
    +        return activeGems.getOrDefault(gem, true);
    +    }
    +
    +    public static String configuredMode() { return loaded == null ? activeMode : MODE.get(); }
    +    public static boolean configuredGem(String gem) { return loaded == null || ENABLED.get(gem).get(); }
    +
    +    public static void save(String mode, Map gems) {
    +        ModConfig config = loaded;
    +        if (config == null) return;
    +        config.getConfigData().set(MODE.getPath(), mode);
    +        gems.forEach((gem, value) -> config.getConfigData().set(ENABLED.get(gem).getPath(), value));
    +        config.save();
    +    }
    +
    +    public static boolean allows(String path) {
    +        int separator = path.indexOf('_');
    +        if (separator < 0) return enabled(path);
    +        if (!enabled(path.substring(0, separator))) return false;
    +        if (!"low_fantasy".equals(activeMode)) return true;
    +        switch (path.substring(separator + 1)) {
    +            case "ore":
    +            case "ingot":
    +            case "nugget":
    +            case "powder":
    +            case "smallpowder":
    +            case "plate":
    +            case "block":
    +                return true;
    +            default:
    +                return false;
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/CompatibilityDoubleSlabBlock.java b/src/main/java/zone/moddev/mc/basegems/content/CompatibilityDoubleSlabBlock.java
    new file mode 100644
    index 0000000..ea34898
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/CompatibilityDoubleSlabBlock.java
    @@ -0,0 +1,30 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import net.minecraft.block.BlockSlab;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.state.properties.SlabType;
    +import net.minecraft.util.IItemProvider;
    +import net.minecraft.util.math.BlockPos;
    +import net.minecraft.world.World;
    +import net.minecraftforge.common.ToolType;
    +
    +/** Keeps old double slabs intact when a 1.10 or 1.12 world is upgraded. */
    +public final class CompatibilityDoubleSlabBlock extends BlockSlab {
    +    private final int harvestLevel;
    +    private final BlockSlab single;
    +
    +    public CompatibilityDoubleSlabBlock(Properties properties, int harvestLevel, BlockSlab single) {
    +        super(properties);
    +        this.harvestLevel = harvestLevel;
    +        this.single = single;
    +        setDefaultState(getDefaultState().with(TYPE, SlabType.DOUBLE).with(WATERLOGGED, Boolean.FALSE));
    +    }
    +
    +    @Override public ToolType getHarvestTool(IBlockState state) { return ToolType.PICKAXE; }
    +    @Override public int getHarvestLevel(IBlockState state) { return harvestLevel; }
    +    @Override public IItemProvider getItemDropped(IBlockState state, World world, BlockPos pos, int fortune) {
    +        return single;
    +    }
    +    @Override protected ItemStack getSilkTouchDrop(IBlockState state) { return new ItemStack(single, 2); }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/CrackhammerItem.java b/src/main/java/zone/moddev/mc/basegems/content/CrackhammerItem.java
    new file mode 100644
    index 0000000..e32e7c1
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/CrackhammerItem.java
    @@ -0,0 +1,105 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.Collections;
    +import java.util.List;
    +
    +import javax.annotation.Nullable;
    +
    +import zone.moddev.mc.basegems.data.Gem;
    +import zone.moddev.mc.basegems.recipe.CrushingRecipe;
    +
    +import net.minecraft.block.Block;
    +import net.minecraft.block.material.Material;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.entity.EntityLivingBase;
    +import net.minecraft.entity.item.EntityItem;
    +import net.minecraft.entity.player.EntityPlayer;
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemBlock;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.ItemTool;
    +import net.minecraft.item.ItemUseContext;
    +import net.minecraft.item.crafting.IRecipe;
    +import net.minecraft.client.util.ITooltipFlag;
    +import net.minecraft.util.EnumActionResult;
    +import net.minecraft.util.EnumFacing;
    +import net.minecraft.util.SoundCategory;
    +import net.minecraft.util.math.AxisAlignedBB;
    +import net.minecraft.util.math.BlockPos;
    +import net.minecraft.util.text.ITextComponent;
    +import net.minecraft.world.World;
    +import net.minecraftforge.common.ToolType;
    +
    +public final class CrackhammerItem extends ItemTool implements MaterialBacked {
    +    private final Gem material;
    +
    +    public CrackhammerItem(Gem material, Item.Properties properties) {
    +        super(material.crackhammerAttackDamage() - material.baseAttackDamage(), -3.5F,
    +                new MaterialTier(material), Collections.emptySet(),
    +                properties.defaultMaxDamage(material.crackhammerDurability())
    +                        .addToolType(ToolType.PICKAXE, material.toolLevel()));
    +        this.material = material;
    +    }
    +
    +    @Override public Gem gem() { return material; }
    +
    +    @Override public float getDestroySpeed(ItemStack stack, IBlockState state) {
    +        return canHarvestBlock(state)
    +                ? material.crackhammerDestroySpeed() : 1.0F;
    +    }
    +
    +    @Override public boolean canHarvestBlock(IBlockState state) {
    +        if (state.getHarvestTool() == ToolType.PICKAXE) return material.toolLevel() >= state.getHarvestLevel();
    +        Material blockMaterial = state.getMaterial();
    +        return blockMaterial == Material.ROCK || blockMaterial == Material.IRON
    +                || blockMaterial == Material.ANVIL;
    +    }
    +
    +
    +    @Override
    +    public EnumActionResult onItemUse(ItemUseContext context) {
    +        World world = context.getWorld();
    +        EntityPlayer player = context.getPlayer();
    +        if (context.getFace() != EnumFacing.UP || world.isRemote || player == null) return EnumActionResult.PASS;
    +
    +        AxisAlignedBB area = new AxisAlignedBB(context.getPos().up());
    +        List entities = world.getEntitiesWithinAABB(EntityItem.class, area);
    +        for (EntityItem entity : entities) {
    +            ItemStack input = entity.getItem();
    +            IRecipe recipe = CrushingRecipe.find(world, input);
    +            if (recipe == null || !canCrushDroppedBlock(input)) continue;
    +            int requested = player.isSneaking() ? input.getCount() : 1;
    +            ItemStack hammer = context.getItem();
    +            int durability = hammer.isDamageable()
    +                    ? Math.max(0, hammer.getMaxDamage() - hammer.getDamage()) : requested;
    +            int operations = Math.min(requested, durability);
    +            if (operations <= 0) break;
    +            ItemStack output = recipe.getRecipeOutput().copy();
    +            input.shrink(operations);
    +            if (input.isEmpty()) entity.remove(); else entity.setItem(input);
    +            spawnOutputs(world, entity, output, operations);
    +            hammer.damageItem(operations, player);
    +            world.playSound(null, context.getPos(), net.minecraft.init.SoundEvents.BLOCK_GRAVEL_BREAK,
    +                    SoundCategory.BLOCKS, 0.5F, 0.5F + world.rand.nextFloat() * 0.3F);
    +            return EnumActionResult.SUCCESS;
    +        }
    +        return EnumActionResult.PASS;
    +    }
    +
    +    private boolean canCrushDroppedBlock(ItemStack input) {
    +        if (!(input.getItem() instanceof ItemBlock)) return true;
    +        return canHarvestBlock(((ItemBlock) input.getItem()).getBlock().getDefaultState());
    +    }
    +
    +    private static void spawnOutputs(World world, EntityItem source, ItemStack result, int operations) {
    +        int remaining = result.getCount() * operations;
    +        while (remaining > 0) {
    +            ItemStack output = result.copy();
    +            output.setCount(Math.min(output.getMaxStackSize(), remaining));
    +            remaining -= output.getCount();
    +            EntityItem crushed = new EntityItem(world, source.posX, source.posY, source.posZ, output);
    +            crushed.setDefaultPickupDelay();
    +            world.spawnEntity(crushed);
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/FluidContent.java b/src/main/java/zone/moddev/mc/basegems/content/FluidContent.java
    new file mode 100644
    index 0000000..446ee29
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/FluidContent.java
    @@ -0,0 +1,25 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import net.minecraft.block.BlockFlowingFluid;
    +import net.minecraft.fluid.FlowingFluid;
    +import net.minecraft.item.Item;
    +
    +public final class FluidContent {
    +    private final RegistryHandle source;
    +    private final RegistryHandle flowing;
    +    private final RegistryHandle block;
    +    private final RegistryHandle bucket;
    +
    +    public FluidContent(RegistryHandle source, RegistryHandle flowing,
    +            RegistryHandle block, RegistryHandle bucket) {
    +        this.source = source;
    +        this.flowing = flowing;
    +        this.block = block;
    +        this.bucket = bucket;
    +    }
    +
    +    public RegistryHandle source() { return source; }
    +    public RegistryHandle flowing() { return flowing; }
    +    public RegistryHandle block() { return block; }
    +    public RegistryHandle bucket() { return bucket; }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/GemAmmoItem.java b/src/main/java/zone/moddev/mc/basegems/content/GemAmmoItem.java
    new file mode 100644
    index 0000000..96dd25a
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/GemAmmoItem.java
    @@ -0,0 +1,42 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.function.Supplier;
    +
    +import zone.moddev.mc.basegems.entity.MaterialProjectile;
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +import net.minecraft.entity.EntityLivingBase;
    +import net.minecraft.entity.EntityType;
    +import net.minecraft.entity.projectile.EntityArrow;
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemArrow;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.world.World;
    +
    +public final class GemAmmoItem extends ItemArrow implements MaterialBacked {
    +    public enum Kind { ARROW, BOLT }
    +    private final Gem material;
    +    private final Kind kind;
    +    private final Supplier> entityType;
    +
    +    public GemAmmoItem(Gem material, Kind kind,
    +            Supplier> entityType, Item.Properties properties) {
    +        super(properties);
    +        this.material = material;
    +        this.kind = kind;
    +        this.entityType = entityType;
    +    }
    +
    +    public Kind kind() { return kind; }
    +    @Override public Gem gem() { return material; }
    +
    +    @Override
    +    public EntityArrow createArrow(World world, ItemStack stack, EntityLivingBase shooter) {
    +        MaterialProjectile projectile = new MaterialProjectile(entityType.get(), world, shooter, stack);
    +
    +        // A vanilla bow adds one damage. Material bows replace that with their own damage.
    +        projectile.setDamage(1.0D + material.baseAttackDamage());
    +
    +        return projectile;
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/GemBlocks.java b/src/main/java/zone/moddev/mc/basegems/content/GemBlocks.java
    new file mode 100644
    index 0000000..813399f
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/GemBlocks.java
    @@ -0,0 +1,103 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.LinkedHashMap;
    +import java.util.Map;
    +
    +import net.minecraft.block.*;
    +import net.minecraft.block.material.Material;
    +import net.minecraftforge.common.ToolType;
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +final class GemBlocks {
    +    private GemBlocks() {}
    +
    +    static Map create(Gem gem) {
    +        String name = gem.name();
    +        int harvest = gem.requiredHarvestLevel();
    +        Map blocks = new LinkedHashMap<>();
    +        Block storage = new HarvestBlock(properties(gem), harvest);
    +        blocks.put(name + "_block", storage);
    +        blocks.put(name + "_ore", new GemOreBlock(gem, Block.Properties.create(Material.ROCK)
    +                .sound(SoundType.STONE).hardnessAndResistance(3, 5).variableOpacity()));
    +        blocks.put(name + "_plate", new PlateBlock(properties(gem).variableOpacity(), harvest));
    +        blocks.put(name + "_bars", new HarvestPane(properties(gem).variableOpacity(), harvest));
    +        blocks.put(name + "_door", new HarvestDoor(properties(gem).variableOpacity(), harvest));
    +        blocks.put(name + "_trapdoor", new HarvestTrapDoor(properties(gem).variableOpacity(), harvest));
    +        blocks.put(name + "_button", new HarvestButton(properties(gem).doesNotBlockMovement(), harvest));
    +        blocks.put(name + "_lever", new HarvestLever(properties(gem).doesNotBlockMovement(), harvest));
    +        blocks.put(name + "_pressure_plate", new HarvestPressurePlate(properties(gem), harvest));
    +        BlockSlab slab = new HarvestSlab(properties(gem), harvest);
    +        blocks.put(name + "_slab", slab);
    +        blocks.put("double_" + name + "_slab", new CompatibilityDoubleSlabBlock(properties(gem), harvest, slab));
    +        blocks.put(name + "_stairs", new HarvestStairs(storage.getDefaultState(), properties(gem), harvest));
    +        blocks.put(name + "_wall", new HarvestWall(properties(gem), harvest));
    +        return blocks;
    +    }
    +
    +    private static Block.Properties properties(Gem gem) {
    +        return Block.Properties.create(Material.ROCK).sound(SoundType.STONE)
    +                .hardnessAndResistance(gem.hardness(), gem.hardness() * 2);
    +    }
    +
    +    private static class HarvestBlock extends Block {
    +        private final int level;
    +        HarvestBlock(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +
    +    private static class HarvestPane extends BlockPane {
    +        private final int level;
    +        HarvestPane(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestDoor extends BlockDoor {
    +        private final int level;
    +        HarvestDoor(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestTrapDoor extends BlockTrapDoor {
    +        private final int level;
    +        HarvestTrapDoor(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestButton extends BlockButtonStone {
    +        private final int level;
    +        HarvestButton(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestSlab extends BlockSlab {
    +        private final int level;
    +        HarvestSlab(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestLever extends BlockLever {
    +        private final int level;
    +        HarvestLever(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestPressurePlate extends BlockPressurePlate {
    +        private final int level;
    +        HarvestPressurePlate(Properties properties, int level) { super(Sensitivity.MOBS, properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestStairs extends BlockStairs {
    +        private final int level;
    +        HarvestStairs(net.minecraft.block.state.IBlockState base, Properties properties, int level) { super(base, properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +    private static class HarvestWall extends BlockWall {
    +        private final int level;
    +        HarvestWall(Properties properties, int level) { super(properties); this.level = level; }
    +        @Override public ToolType getHarvestTool(net.minecraft.block.state.IBlockState state) { return ToolType.PICKAXE; }
    +        @Override public int getHarvestLevel(net.minecraft.block.state.IBlockState state) { return level; }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/GemBucketItem.java b/src/main/java/zone/moddev/mc/basegems/content/GemBucketItem.java
    new file mode 100644
    index 0000000..07ac59a
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/GemBucketItem.java
    @@ -0,0 +1,12 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import net.minecraft.fluid.Fluid;
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemBucket;
    +
    +/** A dedicated filled bucket; Forge 25 has no universal bucket replacement. */
    +public final class GemBucketItem extends ItemBucket {
    +    public GemBucketItem(Fluid fluid, Item.Properties properties) {
    +        super(fluid, properties);
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/GemItems.java b/src/main/java/zone/moddev/mc/basegems/content/GemItems.java
    new file mode 100644
    index 0000000..4dbf99a
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/GemItems.java
    @@ -0,0 +1,37 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.Map;
    +import java.util.function.Supplier;
    +
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemGroup;
    +import zone.moddev.mc.basegems.data.Gem;
    +import zone.moddev.mc.basegems.entity.ModEntities;
    +
    +final class GemItems {
    +    private GemItems() {}
    +
    +    static void add(Gem gem, ItemGroup group, Map> items) {
    +        String id = gem.name() + "_";
    +        for (String form : new String[]{"ingot", "nugget", "powder", "smallpowder", "rod", "gear"}) {
    +            items.put(id + form, () -> new Item(properties(group)));
    +        }
    +        items.put(id + "pickaxe", () -> new MaterialItems.Pickaxe(gem, properties(group)));
    +        items.put(id + "axe", () -> new MaterialItems.Axe(gem, properties(group)));
    +        items.put(id + "hoe", () -> new MaterialItems.Hoe(gem, properties(group)));
    +        items.put(id + "shovel", () -> new MaterialItems.Shovel(gem, properties(group)));
    +        items.put(id + "sword", () -> new MaterialItems.Sword(gem, properties(group)));
    +        items.put(id + "shears", () -> new MaterialItems.Shears(gem, properties(group)));
    +        items.put(id + "bow", () -> new MaterialItems.Bow(gem, properties(group)));
    +        items.put(id + "crossbow", () -> new MaterialItems.Crossbow(gem, properties(group)));
    +        items.put(id + "fishing_rod", () -> new MaterialItems.FishingRod(gem, properties(group)));
    +        items.put(id + "shield", () -> new MaterialItems.Shield(gem, properties(group)));
    +        items.put(id + "crackhammer", () -> new CrackhammerItem(gem, properties(group)));
    +        items.put(id + "arrow", () -> new GemAmmoItem(gem, GemAmmoItem.Kind.ARROW,
    +                ModEntities.CUSTOM_ARROW, properties(group)));
    +        items.put(id + "bolt", () -> new GemAmmoItem(gem, GemAmmoItem.Kind.BOLT,
    +                ModEntities.CUSTOM_BOLT, properties(group)));
    +    }
    +
    +    private static Item.Properties properties(ItemGroup group) { return new Item.Properties().group(group); }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/GemOreBlock.java b/src/main/java/zone/moddev/mc/basegems/content/GemOreBlock.java
    new file mode 100644
    index 0000000..7d7ec4d
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/GemOreBlock.java
    @@ -0,0 +1,37 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import net.minecraft.block.BlockOre;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.util.BlockRenderLayer;
    +import net.minecraft.util.IItemProvider;
    +import net.minecraft.util.math.BlockPos;
    +import net.minecraft.world.IWorldReader;
    +import net.minecraft.world.World;
    +import net.minecraftforge.common.ToolType;
    +import zone.moddev.mc.basegems.data.Gem;
    +import zone.moddev.mc.basegems.data.GemRarity;
    +
    +final class GemOreBlock extends BlockOre {
    +    private final Gem gem;
    +
    +    GemOreBlock(Gem gem, Properties properties) {
    +        super(properties);
    +        this.gem = gem;
    +    }
    +
    +    @Override public ToolType getHarvestTool(IBlockState state) { return ToolType.PICKAXE; }
    +    @Override public int getHarvestLevel(IBlockState state) { return gem.requiredHarvestLevel(); }
    +    @Override public BlockRenderLayer getRenderLayer() { return BlockRenderLayer.CUTOUT_MIPPED; }
    +
    +    @Override
    +    public IItemProvider getItemDropped(IBlockState state, World world, BlockPos pos, int fortune) {
    +        return ModContent.item(gem.name() + "_ingot");
    +    }
    +
    +    @Override
    +    public int getExpDrop(IBlockState state, IWorldReader world, BlockPos pos, int fortune) {
    +        GemRarity rarity = GemRarity.of(gem.name());
    +        return rarity.minXp + java.util.concurrent.ThreadLocalRandom.current()
    +                .nextInt(rarity.maxXp - rarity.minXp + 1);
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/MaterialBacked.java b/src/main/java/zone/moddev/mc/basegems/content/MaterialBacked.java
    new file mode 100644
    index 0000000..ede05a4
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/MaterialBacked.java
    @@ -0,0 +1,16 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.tags.ItemTags;
    +import net.minecraft.item.ItemStack;
    +
    +public interface MaterialBacked {
    +    Gem gem();
    +
    +    default boolean isMaterialRepairIngredient(ItemStack repair) {
    +        return new ItemTags.Wrapper(new ResourceLocation(
    +                gem().repairIngredientTag())).contains(repair.getItem());
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/MaterialItems.java b/src/main/java/zone/moddev/mc/basegems/content/MaterialItems.java
    new file mode 100644
    index 0000000..dc9146b
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/MaterialItems.java
    @@ -0,0 +1,202 @@
    +package zone.moddev.mc.basegems.content;
    +
    +
    +
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +import net.minecraft.entity.EntityLivingBase;
    +import net.minecraft.entity.player.EntityPlayer;
    +import net.minecraft.entity.projectile.EntityArrow;
    +import net.minecraft.enchantment.EnchantmentHelper;
    +import net.minecraft.init.Enchantments;
    +import net.minecraft.init.SoundEvents;
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemArrow;
    +import net.minecraft.item.ItemAxe;
    +import net.minecraft.item.ItemBow;
    +import net.minecraft.item.ItemFishingRod;
    +import net.minecraft.item.ItemHoe;
    +import net.minecraft.item.ItemPickaxe;
    +import net.minecraft.item.ItemShears;
    +import net.minecraft.item.ItemShield;
    +import net.minecraft.item.ItemSpade;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.ItemSword;
    +import net.minecraft.stats.StatList;
    +import net.minecraft.util.SoundCategory;
    +import net.minecraft.world.World;
    +import net.minecraftforge.event.ForgeEventFactory;
    +
    +public final class MaterialItems {
    +    private MaterialItems() {}
    +
    +    public static final class Pickaxe extends ItemPickaxe implements MaterialBacked {
    +        protected final Gem material;
    +        public Pickaxe(Gem material, Properties properties) {
    +            super(new MaterialTier(material), 1, -2.8F, properties);
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +    }
    +
    +    public static final class Axe extends ItemAxe implements MaterialBacked {
    +        protected final Gem material;
    +        public Axe(Gem material, Properties properties) {
    +            super(new MaterialTier(material), 4.0F + material.baseAttackDamage(),
    +                    material.axeAttackSpeed(), properties);
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +    }
    +
    +    public static final class Shovel extends ItemSpade implements MaterialBacked {
    +        protected final Gem material;
    +        public Shovel(Gem material, Properties properties) {
    +            super(new MaterialTier(material), 1.5F, -3.0F, properties);
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +    }
    +
    +    public static final class Hoe extends ItemHoe implements MaterialBacked {
    +        protected final Gem material;
    +        public Hoe(Gem material, Properties properties) {
    +            super(new MaterialTier(material), material.baseAttackDamage() - 3.0F, properties);
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +    }
    +
    +    public static class Sword extends ItemSword implements MaterialBacked {
    +        protected final Gem material;
    +        public Sword(Gem material, Properties properties) {
    +            super(new MaterialTier(material), 3, -2.4F, properties);
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +    }
    +
    +    public static final class Shears extends ItemShears implements MaterialBacked {
    +        protected final Gem material;
    +        public Shears(Gem material, Properties properties) {
    +            super(properties.defaultMaxDamage(material.toolDurability()));
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +        @Override public boolean getIsRepairable(ItemStack toRepair, ItemStack repair) {
    +            return new MaterialTier(material).getRepairMaterial().test(repair) || super.getIsRepairable(toRepair, repair);
    +        }
    +    }
    +
    +    public static class Shield extends ItemShield implements MaterialBacked {
    +        protected final Gem material;
    +        public Shield(Gem material, Properties properties) {
    +            super(properties.defaultMaxDamage(material.shieldDurability()));
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +        @Override public boolean getIsRepairable(ItemStack toRepair, ItemStack repair) {
    +            return new MaterialTier(material).getRepairMaterial().test(repair) || super.getIsRepairable(toRepair, repair);
    +        }
    +    }
    +
    +    public static class Bow extends ItemBow implements MaterialBacked {
    +        protected final Gem material;
    +
    +        public Bow(Gem material, Properties properties) {
    +            super(properties.defaultMaxDamage(material.toolDurability()));
    +            this.material = material;
    +        }
    +
    +        @Override public Gem gem() { return material; }
    +
    +        @Override
    +        public EntityArrow customizeArrow(EntityArrow arrow) {
    +            // Forge calls this before applying Power, so enchantment bonuses stay unchanged.
    +            arrow.setDamage(arrow.getDamage() + material.baseAttackDamage() - 1.0D);
    +
    +            return arrow;
    +        }
    +
    +        @Override protected boolean isArrow(ItemStack stack) {
    +            if (stack.getItem() instanceof GemAmmoItem) {
    +                return ((GemAmmoItem) stack.getItem()).kind() == GemAmmoItem.Kind.ARROW;
    +            }
    +            return stack.getItem() instanceof ItemArrow;
    +        }
    +        @Override public boolean getIsRepairable(ItemStack toRepair, ItemStack repair) {
    +            return new MaterialTier(material).getRepairMaterial().test(repair) || super.getIsRepairable(toRepair, repair);
    +        }
    +    }
    +
    +    public static final class Crossbow extends Bow {
    +        public Crossbow(Gem material, Properties properties) {
    +            super(material, properties);
    +        }
    +
    +        @Override protected boolean isArrow(ItemStack stack) {
    +            return stack.getItem() instanceof GemAmmoItem
    +                    && ((GemAmmoItem) stack.getItem()).kind() == GemAmmoItem.Kind.BOLT;
    +        }
    +        @Override
    +        public void onPlayerStoppedUsing(ItemStack bow, World world, EntityLivingBase user, int timeLeft) {
    +            if (!(user instanceof EntityPlayer)) return;
    +            EntityPlayer player = (EntityPlayer) user;
    +            boolean hasInfiniteAmmo = player.abilities.isCreativeMode
    +                    || EnchantmentHelper.getEnchantmentLevel(Enchantments.INFINITY, bow) > 0;
    +            ItemStack ammunition = findAmmo(player);
    +            int charge = getUseDuration(bow) - timeLeft;
    +            charge = ForgeEventFactory.onArrowLoose(bow, world, player, charge,
    +                    !ammunition.isEmpty() || hasInfiniteAmmo);
    +            if (charge < 0 || ammunition.isEmpty() && !hasInfiniteAmmo) return;
    +
    +            if (ammunition.isEmpty()) ammunition = new ItemStack(ModContent.item(material.name() + "_bolt"));
    +            float power = getArrowVelocity(charge);
    +            if (power < 0.1F) return;
    +
    +            GemAmmoItem bolt = (GemAmmoItem) ammunition.getItem();
    +            boolean infiniteShot = player.abilities.isCreativeMode
    +                    || bolt.isInfinite(ammunition, bow, player);
    +            if (!world.isRemote) {
    +                EntityArrow projectile = customizeArrow(bolt.createArrow(world, ammunition, player));
    +                projectile.shoot(player, player.rotationPitch, player.rotationYaw, 0.0F,
    +                        power * 3.0F, 1.0F);
    +                if (power == 1.0F) projectile.setIsCritical(true);
    +                int powerLevel = EnchantmentHelper.getEnchantmentLevel(Enchantments.POWER, bow);
    +                if (powerLevel > 0) {
    +                    projectile.setDamage(projectile.getDamage() + powerLevel * 0.5D + 0.5D);
    +                }
    +                int punchLevel = EnchantmentHelper.getEnchantmentLevel(Enchantments.PUNCH, bow);
    +                if (punchLevel > 0) projectile.setKnockbackStrength(punchLevel);
    +                if (EnchantmentHelper.getEnchantmentLevel(Enchantments.FLAME, bow) > 0) {
    +                    projectile.setFire(100);
    +                }
    +                bow.damageItem(1, player);
    +                if (infiniteShot) projectile.pickupStatus = EntityArrow.PickupStatus.CREATIVE_ONLY;
    +                world.spawnEntity(projectile);
    +            }
    +            world.playSound(null, player.posX, player.posY, player.posZ,
    +                    SoundEvents.ENTITY_ARROW_SHOOT, SoundCategory.PLAYERS, 1.0F,
    +                    1.0F / (random.nextFloat() * 0.4F + 1.2F) + power * 0.5F);
    +            if (!infiniteShot && !player.abilities.isCreativeMode) {
    +                ammunition.shrink(1);
    +                if (ammunition.isEmpty()) player.inventory.deleteStack(ammunition);
    +            }
    +            player.addStat(StatList.ITEM_USED.get(this));
    +        }
    +    }
    +
    +    public static final class FishingRod extends ItemFishingRod implements MaterialBacked {
    +        protected final Gem material;
    +        public FishingRod(Gem material, Properties properties) {
    +            super(properties.defaultMaxDamage(material.toolDurability()));
    +            this.material = material;
    +        }
    +        @Override public Gem gem() { return material; }
    +        @Override public boolean getIsRepairable(ItemStack toRepair, ItemStack repair) {
    +            return new MaterialTier(material).getRepairMaterial().test(repair) || super.getIsRepairable(toRepair, repair);
    +        }
    +    }
    +
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/MaterialTier.java b/src/main/java/zone/moddev/mc/basegems/content/MaterialTier.java
    new file mode 100644
    index 0000000..5a32467
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/MaterialTier.java
    @@ -0,0 +1,29 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.function.Supplier;
    +
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +import net.minecraft.item.IItemTier;
    +import net.minecraft.item.crafting.Ingredient;
    +import net.minecraft.tags.ItemTags;
    +import net.minecraft.util.ResourceLocation;
    +
    +public final class MaterialTier implements IItemTier {
    +    private final Gem material;
    +    private final Supplier repair;
    +
    +    public MaterialTier(Gem material) {
    +        this.material = material;
    +        final ItemTags.Wrapper tag = new ItemTags.Wrapper(new ResourceLocation(material.repairIngredientTag()));
    +        this.repair = () -> Ingredient.fromTag(tag);
    +    }
    +
    +    public Gem material() { return material; }
    +    @Override public int getMaxUses() { return material.toolDurability(); }
    +    @Override public float getEfficiency() { return material.toolEfficiency(); }
    +    @Override public float getAttackDamage() { return material.baseAttackDamage(); }
    +    @Override public int getHarvestLevel() { return material.toolLevel(); }
    +    @Override public int getEnchantability() { return material.enchantability(); }
    +    @Override public Ingredient getRepairMaterial() { return repair.get(); }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/ModContent.java b/src/main/java/zone/moddev/mc/basegems/content/ModContent.java
    new file mode 100644
    index 0000000..0026cdd
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/ModContent.java
    @@ -0,0 +1,117 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.Collections;
    +import java.util.IdentityHashMap;
    +import java.util.LinkedHashMap;
    +import java.util.Map;
    +import java.util.function.Supplier;
    +
    +import net.minecraft.block.Block;
    +import net.minecraft.block.BlockFlowingFluid;
    +import net.minecraft.block.SoundType;
    +import net.minecraft.block.material.Material;
    +import net.minecraft.fluid.FlowingFluid;
    +import net.minecraft.fluid.Fluid;
    +import net.minecraft.fluid.IFluidState;
    +import net.minecraft.init.Items;
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemBlock;
    +import net.minecraft.item.ItemGroup;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.util.registry.IRegistry;
    +import net.minecraftforge.event.RegistryEvent;
    +import net.minecraftforge.eventbus.api.SubscribeEvent;
    +import net.minecraftforge.fml.common.Mod;
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.data.Gem;
    +
    +@Mod.EventBusSubscriber(modid = BaseGems.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
    +public final class ModContent {
    +    private static final Map BLOCKS = new LinkedHashMap<>();
    +    private static final Map ITEMS = new LinkedHashMap<>();
    +    private static final Map> ITEM_FACTORIES = new LinkedHashMap<>();
    +    private static final Map FLUIDS = new LinkedHashMap<>();
    +    private static final Map FLUID_COLOURS = new IdentityHashMap<>();
    +    private static final ItemGroup GEMS = new ItemGroup("basegems") {
    +        @Override public ItemStack createIcon() { return new ItemStack(item("ruby_ingot")); }
    +    };
    +
    +    private ModContent() {}
    +
    +    public static ResourceLocation id(String path) { return new ResourceLocation(BaseGems.MOD_ID, path); }
    +    public static Block block(String path) { return BLOCKS.get(path); }
    +    public static Item item(String path) { return ITEMS.get(path); }
    +    public static FluidContent fluid(String path) { return FLUIDS.get(path); }
    +    public static Map fluids() { return Collections.unmodifiableMap(FLUIDS); }
    +    public static boolean isBaseGemsFluid(Fluid fluid) { return FLUID_COLOURS.containsKey(fluid); }
    +    public static int fluidColour(Fluid fluid) { return FLUID_COLOURS.getOrDefault(fluid, 0xffffff); }
    +
    +    // Vanilla's fluid registries are shared, so use Forge's sequential block event.
    +    private static void initializeFluids() {
    +        for (Gem gem : Gem.ALL) {
    +            String name = gem.name();
    +            FluidContent fluid = new FluidContent(new RegistryHandle<>(name),
    +                    new RegistryHandle<>("flowing_" + name), new RegistryHandle<>(name),
    +                    new RegistryHandle<>(name + "_bucket"));
    +            FLUIDS.put(name, fluid);
    +            FlowingFluid source = new MoltenFluid.Source(name);
    +            FlowingFluid flowing = new MoltenFluid.Flowing(name);
    +            fluid.source().bind(source);
    +            fluid.flowing().bind(flowing);
    +            IRegistry.field_212619_h.put(id(name), source);
    +            IRegistry.field_212619_h.put(id("flowing_" + name), flowing);
    +            for (Fluid type : new Fluid[]{source, flowing}) {
    +                FLUID_COLOURS.put(type, gem.colour() & 0xffffff);
    +                for (IFluidState state : type.getStateContainer().getValidStates()) {
    +                    Fluid.STATE_REGISTRY.add(state);
    +                }
    +            }
    +        }
    +    }
    +
    +    @SubscribeEvent
    +    public static void registerBlocks(RegistryEvent.Register event) {
    +        initializeFluids();
    +        for (Gem gem : Gem.ALL) {
    +            String name = gem.name();
    +            GemBlocks.create(gem).forEach((path, value) -> {
    +                BLOCKS.put(path, value);
    +                event.getRegistry().register(value.setRegistryName(id(path)));
    +                if (!path.startsWith("double_")) {
    +                    ITEM_FACTORIES.put(path, () -> new ItemBlock(value, new Item.Properties().group(GEMS)));
    +                }
    +            });
    +            FluidContent fluid = fluid(name);
    +            BlockFlowingFluid molten = new MoltenGemBlock(fluid.source().get());
    +            fluid.block().bind(molten);
    +            BLOCKS.put(name, molten);
    +            event.getRegistry().register(molten.setRegistryName(id(name)));
    +            // The pre-flattening versions registered an item for the fluid block.
    +            ITEM_FACTORIES.put(name, () -> new ItemBlock(molten, new Item.Properties()));
    +            ITEM_FACTORIES.put(name + "_bucket", () -> {
    +                Item bucket = new GemBucketItem(fluid.source().get(),
    +                        new Item.Properties().containerItem(Items.BUCKET).maxStackSize(1).group(GEMS));
    +                fluid.bucket().bind(bucket);
    +                return bucket;
    +            });
    +        }
    +    }
    +
    +    @SubscribeEvent
    +    public static void registerItems(RegistryEvent.Register event) {
    +        for (Gem gem : Gem.ALL) GemItems.add(gem, GEMS, ITEM_FACTORIES);
    +        ITEM_FACTORIES.forEach((path, factory) -> {
    +            Item item = factory.get();
    +            ITEMS.put(path, item);
    +            event.getRegistry().register(item.setRegistryName(id(path)));
    +        });
    +    }
    +
    +    private static final class MoltenGemBlock extends BlockFlowingFluid {
    +        private MoltenGemBlock(FlowingFluid fluid) {
    +            super(fluid, Block.Properties.create(Material.LAVA).doesNotBlockMovement()
    +                    .hardnessAndResistance(100).variableOpacity());
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/MoltenFluid.java b/src/main/java/zone/moddev/mc/basegems/content/MoltenFluid.java
    new file mode 100644
    index 0000000..a5a18bc
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/MoltenFluid.java
    @@ -0,0 +1,67 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import net.minecraft.block.BlockFlowingFluid;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.fluid.FlowingFluid;
    +import net.minecraft.fluid.Fluid;
    +import net.minecraft.fluid.IFluidState;
    +import net.minecraft.item.Item;
    +import net.minecraft.state.StateContainer;
    +import net.minecraft.util.BlockRenderLayer;
    +import net.minecraft.util.EnumFacing;
    +import net.minecraft.util.math.BlockPos;
    +import net.minecraft.world.IWorld;
    +import net.minecraft.world.IWorldReaderBase;
    +import net.minecraftforge.api.distmarker.Dist;
    +import net.minecraftforge.api.distmarker.OnlyIn;
    +
    +/** Common flow behaviour for the molten gems. */
    +public abstract class MoltenFluid extends FlowingFluid {
    +    private final String name;
    +
    +    protected MoltenFluid(String name) {
    +        this.name = name;
    +    }
    +
    +    private FluidContent content() { return ModContent.fluid(name); }
    +    @Override public Fluid getFlowingFluid() { return content().flowing().get(); }
    +    @Override public Fluid getStillFluid() { return content().source().get(); }
    +    @Override public Item getFilledBucket() { return content().bucket().get(); }
    +
    +    @Override @OnlyIn(Dist.CLIENT)
    +    public BlockRenderLayer getRenderLayer() { return BlockRenderLayer.TRANSLUCENT; }
    +    @Override protected boolean canSourcesMultiply() { return false; }
    +    @Override protected void beforeReplacingBlock(IWorld world, BlockPos pos, IBlockState state) {
    +        state.dropBlockAsItem(world.getWorld(), pos, 0);
    +    }
    +    @Override public int getSlopeFindDistance(IWorldReaderBase world) { return 2; }
    +    @Override public int getLevelDecreasePerBlock(IWorldReaderBase world) { return 2; }
    +    @Override public int getTickRate(IWorldReaderBase world) { return 30; }
    +    @Override protected float getExplosionResistance() { return 100.0F; }
    +    @Override public boolean isEquivalentTo(Fluid fluid) {
    +        return fluid == content().source().get() || fluid == content().flowing().get();
    +    }
    +    @Override protected boolean canOtherFlowInto(IFluidState state, Fluid fluid, EnumFacing direction) {
    +        return direction == EnumFacing.DOWN && !fluid.isEquivalentTo(this);
    +    }
    +    @Override public IBlockState getBlockState(IFluidState state) {
    +        return content().block().get().getDefaultState()
    +                .with(BlockFlowingFluid.LEVEL, Integer.valueOf(getLevelFromState(state)));
    +    }
    +
    +    public static final class Flowing extends MoltenFluid {
    +        public Flowing(String name) { super(name); }
    +        @Override protected void fillStateContainer(StateContainer.Builder builder) {
    +            super.fillStateContainer(builder);
    +            builder.add(LEVEL_1_TO_8);
    +        }
    +        @Override public int getLevel(IFluidState state) { return state.get(LEVEL_1_TO_8); }
    +        @Override public boolean isSource(IFluidState state) { return false; }
    +    }
    +
    +    public static final class Source extends MoltenFluid {
    +        public Source(String name) { super(name); }
    +        @Override public int getLevel(IFluidState state) { return 8; }
    +        @Override public boolean isSource(IFluidState state) { return true; }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/PlateBlock.java b/src/main/java/zone/moddev/mc/basegems/content/PlateBlock.java
    new file mode 100644
    index 0000000..122e310
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/PlateBlock.java
    @@ -0,0 +1,51 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import net.minecraft.block.Block;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.item.BlockItemUseContext;
    +import net.minecraft.state.DirectionProperty;
    +import net.minecraft.state.StateContainer;
    +import net.minecraft.state.properties.BlockStateProperties;
    +import net.minecraft.util.EnumFacing;
    +import net.minecraft.util.Mirror;
    +import net.minecraft.util.Rotation;
    +import net.minecraft.util.math.BlockPos;
    +import net.minecraft.util.math.shapes.VoxelShape;
    +import net.minecraft.world.IBlockReader;
    +import net.minecraftforge.common.ToolType;
    +
    +/** A one-pixel-thick plate attached flush to the selected block face. */
    +public final class PlateBlock extends Block {
    +    public static final DirectionProperty FACING = BlockStateProperties.FACING;
    +    private static final VoxelShape[] SHAPES = {
    +            makeCuboidShape(0, 15, 0, 16, 16, 16), makeCuboidShape(0, 0, 0, 16, 1, 16),
    +            makeCuboidShape(0, 0, 15, 16, 16, 16), makeCuboidShape(0, 0, 0, 16, 16, 1),
    +            makeCuboidShape(15, 0, 0, 16, 16, 16), makeCuboidShape(0, 0, 0, 1, 16, 16)};
    +    private final int harvestLevel;
    +
    +    public PlateBlock(Properties properties, int harvestLevel) {
    +        super(properties);
    +        this.harvestLevel = harvestLevel;
    +        setDefaultState(stateContainer.getBaseState().with(FACING, EnumFacing.NORTH));
    +    }
    +
    +    @Override
    +    public IBlockState getStateForPlacement(BlockItemUseContext context) {
    +        return getDefaultState().with(FACING, context.getFace());
    +    }
    +
    +    @Override public VoxelShape getShape(IBlockState state, IBlockReader world, BlockPos pos) {
    +        return SHAPES[state.get(FACING).getIndex()];
    +    }
    +    @Override public IBlockState rotate(IBlockState state, Rotation rotation) {
    +        return state.with(FACING, rotation.rotate(state.get(FACING)));
    +    }
    +    @Override public IBlockState mirror(IBlockState state, Mirror mirror) {
    +        return rotate(state, mirror.toRotation(state.get(FACING)));
    +    }
    +    @Override protected void fillStateContainer(StateContainer.Builder builder) {
    +        builder.add(FACING);
    +    }
    +    @Override public ToolType getHarvestTool(IBlockState state) { return ToolType.PICKAXE; }
    +    @Override public int getHarvestLevel(IBlockState state) { return harvestLevel; }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/content/RegistryHandle.java b/src/main/java/zone/moddev/mc/basegems/content/RegistryHandle.java
    new file mode 100644
    index 0000000..8e2951d
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/content/RegistryHandle.java
    @@ -0,0 +1,33 @@
    +package zone.moddev.mc.basegems.content;
    +
    +import java.util.Objects;
    +import java.util.function.Supplier;
    +
    +/** Holds an entry until its Forge registration event supplies the instance. */
    +public final class RegistryHandle implements Supplier {
    +    private final String id;
    +    private T value;
    +
    +    public RegistryHandle(String id) {
    +        this.id = Objects.requireNonNull(id, "id");
    +    }
    +
    +    public String id() {
    +        return id;
    +    }
    +
    +    public void bind(T registeredValue) {
    +        if (value != null) {
    +            throw new IllegalStateException("Registry handle already bound: " + id);
    +        }
    +        value = Objects.requireNonNull(registeredValue, "registeredValue");
    +    }
    +
    +    @Override
    +    public T get() {
    +        if (value == null) {
    +            throw new IllegalStateException("Registry handle has not been bound: " + id);
    +        }
    +        return value;
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/data/Gem.java b/src/main/java/zone/moddev/mc/basegems/data/Gem.java
    new file mode 100644
    index 0000000..37cbcab
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/data/Gem.java
    @@ -0,0 +1,78 @@
    +package zone.moddev.mc.basegems.data;
    +
    +import java.util.Arrays;
    +import java.util.Collections;
    +import java.util.List;
    +
    +/** The gem names and physical properties used in the legacy releases. */
    +public final class Gem {
    +    public static final List ALL = Collections.unmodifiableList(Arrays.asList(
    +            new Gem("agate", "EnableAgate", 6.5F, 8F, 4.5F, 0xFF179FD1),
    +            new Gem("alexandrite", "EnableAlexandrite", 8.5F, 8F, 4.5F, 0xFF747385),
    +            new Gem("amber", "EnableAmber", 2F, 8F, 4.5F, 0xFFF9C923),
    +            new Gem("amethyst", "EnableAmethyst", 7F, 8F, 4.5F, 0xFFA242E2),
    +            new Gem("ametrine", "EnableAmetrine", 7F, 8F, 4.5F, 0xFFC8A394),
    +            new Gem("aquamarine", "EnableAquamarine", 7.5F, 8F, 4.5F, 0xFF78BACA),
    +            new Gem("beryl", "EnableBeryl", 7.5F, 8F, 4.5F, 0xFF4EB86A),
    +            new Gem("blackdiamond", "EnableBlackDiamond", 10F, 15F, 4.5F, 0xFF000000),
    +            new Gem("bluetopaz", "EnableBlueTopaz", 8F, 8F, 4.5F, 0xFF01A4E9),
    +            new Gem("carnelian", "EnableCarnelian", 6.5F, 8F, 4.5F, 0xFFF03905),
    +            new Gem("citrine", "EnableCitrine", 7F, 8F, 4.5F, 0xFFFAD563),
    +            new Gem("garnet", "EnableGarnet", 6.5F, 8F, 4.5F, 0xFF9D0938),
    +            new Gem("goldenberyl", "EnableGoldenBeryl", 7.5F, 8F, 4.5F, 0xFFFDEE4C),
    +            new Gem("heliodor", "EnableHeliodor", 7.5F, 8F, 4.5F, 0xFFF9FD5D),
    +            new Gem("indicolite", "EnableIndicolite", 7F, 8F, 4.5F, 0xFF7EFBF5),
    +            new Gem("iolite", "EnableIolite", 7F, 8F, 4.5F, 0xFF605DBD),
    +            new Gem("jade", "EnableJade", 6.5F, 8F, 4.5F, 0xFF008127),
    +            new Gem("jasper", "EnableJasper", 7F, 8F, 4.5F, 0xFF9E0403),
    +            new Gem("lepidolite", "EnableLepidolite", 3F, 8F, 4.5F, 0xFFB980A0),
    +            new Gem("malachite", "EnableMalachite", 4F, 8F, 4.5F, 0xFF25674C),
    +            new Gem("moldavite", "EnableMoldavite", 5F, 8F, 4.5F, 0xFF68873B),
    +            new Gem("moonstone", "EnableMoonstone", 6F, 8F, 4.5F, 0xFF9FAED7),
    +            new Gem("morganite", "EnableMorganite", 7.5F, 8F, 4.5F, 0xFFF4D8CA),
    +            new Gem("onyx", "EnableOnyx", 7F, 8F, 4.5F, 0xFF000000),
    +            new Gem("opal", "EnableOpal", 6F, 8F, 4.5F, 0xFFC3CCE9),
    +            new Gem("peridot", "EnablePeridot", 6.5F, 8F, 4.5F, 0xFFB9E64F),
    +            new Gem("ruby", "EnableRuby", 9F, 8F, 4.5F, 0xFF7E0308),
    +            new Gem("sapphire", "EnableSapphire", 9F, 8F, 4.5F, 0xFF1036D5),
    +            new Gem("spinel", "EnableSpinel", 7.5F, 8F, 4.5F, 0xFFFE2B80),
    +            new Gem("tanzanite", "EnableTanzanite", 6F, 8F, 4.5F, 0xFF4A4AE0),
    +            new Gem("topaz", "EnableTopaz", 8F, 8F, 4.5F, 0xFFF9E838),
    +            new Gem("turquoise", "EnableTurquoise", 5F, 8F, 4.5F, 0xFF3FB2C4),
    +            new Gem("violetsapphire", "EnableVioletSapphire", 9F, 8F, 4.5F, 0xFF8247B7)
    +    ));
    +
    +    private final String name;
    +    private final String configKey;
    +    private final float hardness;
    +    private final float strength;
    +    private final float magic;
    +    private final int colour;
    +
    +    private Gem(String name, String configKey, float hardness, float strength, float magic, int colour) {
    +        this.name = name;
    +        this.configKey = configKey;
    +        this.hardness = hardness;
    +        this.strength = strength;
    +        this.magic = magic;
    +        this.colour = colour;
    +    }
    +
    +    public String name() { return name; }
    +    public String configKey() { return configKey; }
    +    public int colour() { return colour; }
    +    public float hardness() { return hardness; }
    +    public int toolLevel() { return (int) (hardness / 3); }
    +    public int requiredHarvestLevel() { return Math.max(-1, Math.min(3, (int) (0.9F * hardness / 3))); }
    +    public int toolDurability() { return (int) (32 * strength); }
    +    public float toolEfficiency() { return hardness; }
    +    public float baseAttackDamage() { return Math.round(2.5F * hardness) / 10.0F; }
    +    public float equipmentTradeValue() { return hardness + strength + magic + toolLevel(); }
    +    public int enchantability() { return (int) (2.5F * magic); }
    +    public float axeAttackSpeed() { return -3.1F; }
    +    public int shieldDurability() { return toolDurability() * 2; }
    +    public int crackhammerDurability() { return toolDurability(); }
    +    public float crackhammerAttackDamage() { return baseAttackDamage() + 4; }
    +    public float crackhammerDestroySpeed() { return toolEfficiency(); }
    +    public String repairIngredientTag() { return "forge:gems/" + name; }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/entity/MaterialProjectile.java b/src/main/java/zone/moddev/mc/basegems/entity/MaterialProjectile.java
    new file mode 100644
    index 0000000..46fd629
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/entity/MaterialProjectile.java
    @@ -0,0 +1,56 @@
    +package zone.moddev.mc.basegems.entity;
    +
    +import zone.moddev.mc.basegems.content.ModContent;
    +
    +import net.minecraft.entity.EntityLivingBase;
    +import net.minecraft.entity.EntityType;
    +import net.minecraft.entity.projectile.EntityArrow;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.nbt.NBTTagCompound;
    +import net.minecraft.network.PacketBuffer;
    +import net.minecraft.world.World;
    +import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
    +
    +public final class MaterialProjectile extends EntityArrow implements IEntityAdditionalSpawnData {
    +    private ItemStack ammunition = ItemStack.EMPTY;
    +
    +    public MaterialProjectile(EntityType type, World world) {
    +        super(type, world);
    +    }
    +
    +    public MaterialProjectile(EntityType type, World world, EntityLivingBase shooter, ItemStack ammunition) {
    +        super(type, shooter, world);
    +        this.ammunition = single(ammunition);
    +    }
    +
    +    private static ItemStack single(ItemStack stack) {
    +        ItemStack copy = stack.copy();
    +        copy.setCount(1);
    +        return copy;
    +    }
    +
    +    @Override
    +    protected ItemStack getArrowStack() {
    +        return ammunition.isEmpty() ? new ItemStack(ModContent.item("ruby_arrow")) : ammunition.copy();
    +    }
    +
    +    public ItemStack getAmmunition() {
    +        return getArrowStack();
    +    }
    +
    +    @Override
    +    public void writeAdditional(NBTTagCompound tag) {
    +        super.writeAdditional(tag);
    +        if (!ammunition.isEmpty()) tag.setTag("Ammunition", ammunition.write(new NBTTagCompound()));
    +    }
    +
    +    @Override
    +    public void readAdditional(NBTTagCompound tag) {
    +        super.readAdditional(tag);
    +        ammunition = tag.contains("Ammunition", 10)
    +                ? ItemStack.read(tag.getCompound("Ammunition")) : ItemStack.EMPTY;
    +    }
    +
    +    @Override public void writeSpawnData(PacketBuffer buffer) { buffer.writeItemStack(ammunition); }
    +    @Override public void readSpawnData(PacketBuffer buffer) { ammunition = buffer.readItemStack(); }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/entity/ModEntities.java b/src/main/java/zone/moddev/mc/basegems/entity/ModEntities.java
    new file mode 100644
    index 0000000..129b097
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/entity/ModEntities.java
    @@ -0,0 +1,40 @@
    +package zone.moddev.mc.basegems.entity;
    +
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.content.RegistryHandle;
    +
    +import net.minecraft.entity.EntityType;
    +import net.minecraftforge.event.RegistryEvent;
    +import net.minecraftforge.eventbus.api.SubscribeEvent;
    +import net.minecraftforge.fml.common.Mod;
    +
    +@Mod.EventBusSubscriber(modid = BaseGems.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
    +public final class ModEntities {
    +    public static final RegistryHandle> CUSTOM_ARROW =
    +            new RegistryHandle>("custom_arrow");
    +    public static final RegistryHandle> CUSTOM_BOLT =
    +            new RegistryHandle>("custom_bolt");
    +
    +    private ModEntities() {}
    +
    +    public static void initialize() {
    +        // Register the event subscriber before item factories ask for these entities.
    +    }
    +
    +    @SubscribeEvent
    +    public static void registerEntities(RegistryEvent.Register> event) {
    +        CUSTOM_ARROW.bind(register(event, "custom_arrow", CUSTOM_ARROW));
    +        CUSTOM_BOLT.bind(register(event, "custom_bolt", CUSTOM_BOLT));
    +    }
    +
    +    private static EntityType register(RegistryEvent.Register> event,
    +            String name, final RegistryHandle> self) {
    +        EntityType type = EntityType.Builder
    +                .create(MaterialProjectile.class, world -> new MaterialProjectile(self.get(), world))
    +                .tracker(64, 20, true)
    +                .build(BaseGems.MOD_ID + ":" + name);
    +        type.setRegistryName(BaseGems.MOD_ID, name);
    +        event.getRegistry().register(type);
    +        return type;
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/Blocks.java b/src/main/java/zone/moddev/mc/basegems/init/Blocks.java
    deleted file mode 100644
    index fb7c69c..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/Blocks.java
    +++ /dev/null
    @@ -1,42 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.data.Names;
    -import com.mcmoddev.lib.util.Oredicts;
    -import net.minecraft.block.Block;
    -import zone.moddev.mc.basegems.block.OverlayOreBlock;
    -
    -/**
    - * This class initializes all blocks in Base Gems.
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -public class Blocks extends com.mcmoddev.lib.init.Blocks {
    -
    -	private static final Names[] FORMS = {
    -			Names.BLOCK, Names.PLATE, Names.BARS, Names.DOOR, Names.TRAPDOOR, Names.BUTTON,
    -			Names.SLAB, Names.DOUBLE_SLAB, Names.LEVER, Names.PRESSURE_PLATE, Names.STAIRS, Names.WALL
    -	};
    -
    -	protected Blocks() {
    -		throw new IllegalAccessError("Not a instantiable class");
    -	}
    -
    -	/**
    -	 *
    -	 */
    -	public static void init() {
    -		for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -			for (Names form : FORMS) {
    -				create(form, material);
    -			}
    -			if (material.hasOre() && !material.hasBlock(Names.ORE) && isNameEnabled(Names.ORE)) {
    -				Block ore = addBlock(new OverlayOreBlock(material), Names.ORE, material, ItemGroups.blocksTab);
    -				material.addNewBlock(Names.ORE, ore);
    -				Oredicts.registerOre("ore" + material.getCapitalizedName(), ore);
    -			}
    -		}
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/BookRecipeCondition.java b/src/main/java/zone/moddev/mc/basegems/init/BookRecipeCondition.java
    deleted file mode 100644
    index 400dea9..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/BookRecipeCondition.java
    +++ /dev/null
    @@ -1,22 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import com.google.gson.JsonObject;
    -import java.util.function.BooleanSupplier;
    -import net.minecraft.item.crafting.CraftingManager;
    -import net.minecraft.item.crafting.IRecipe;
    -import net.minecraft.util.JsonUtils;
    -import net.minecraft.util.ResourceLocation;
    -import net.minecraftforge.common.crafting.IConditionFactory;
    -import net.minecraftforge.common.crafting.JsonContext;
    -
    -/** Loads a recipe advancement only when the active config allows the recipe in the book. */
    -public final class BookRecipeCondition implements IConditionFactory {
    -    @Override
    -    public BooleanSupplier parse(JsonContext context, JsonObject json) {
    -        ResourceLocation id = new ResourceLocation(JsonUtils.getString(json, "recipe"));
    -        return () -> {
    -            IRecipe recipe = CraftingManager.getRecipe(id);
    -            return recipe != null && !recipe.isDynamic() && !recipe.getRecipeOutput().isEmpty();
    -        };
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/Fluids.java b/src/main/java/zone/moddev/mc/basegems/init/Fluids.java
    deleted file mode 100644
    index 7669ae0..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/Fluids.java
    +++ /dev/null
    @@ -1,28 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -
    -/**
    - * This class initializes all fluids in Base Gems.
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -public class Fluids extends com.mcmoddev.lib.init.Fluids {
    -
    -
    -	private Fluids() {
    -		throw new IllegalAccessError("Not a instantiable class");
    -	}
    -
    -	/**
    -	 *
    -	 */
    -	public static void init() {
    -		for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -			addFluid(material, 2000, 10000, 330, 10);
    -			addFluidBlock(material);
    -		}
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/GemRecipes.java b/src/main/java/zone/moddev/mc/basegems/init/GemRecipes.java
    deleted file mode 100644
    index 24a6ca9..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/GemRecipes.java
    +++ /dev/null
    @@ -1,118 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import com.mcmoddev.lib.data.Names;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.recipe.ShieldRepairRecipe;
    -import com.mcmoddev.lib.recipe.ShieldUpgradeRecipe;
    -import com.mcmoddev.lib.util.Config.Options;
    -import com.mcmoddev.lib.util.Oredicts;
    -import net.minecraft.item.ItemStack;
    -import net.minecraft.item.crafting.IRecipe;
    -import net.minecraft.util.ResourceLocation;
    -import net.minecraftforge.oredict.OreDictionary;
    -import net.minecraftforge.oredict.ShapedOreRecipe;
    -import net.minecraftforge.oredict.ShapelessOreRecipe;
    -import net.minecraftforge.registries.IForgeRegistry;
    -import zone.moddev.mc.basegems.BaseGems;
    -
    -/** Forge 1.12 registrations for the gem crafting patterns used on 1.11.2. */
    -final class GemRecipes {
    -    private final IForgeRegistry registry;
    -    private final MMDMaterial material;
    -
    -    GemRecipes(IForgeRegistry registry, MMDMaterial material) {
    -        this.registry = registry;
    -        this.material = material;
    -    }
    -
    -    void register() {
    -        String gem = ore(Oredicts.INGOT);
    -        String nugget = ore(Oredicts.NUGGET);
    -        String block = ore(Oredicts.BLOCK);
    -        String rod = ore(Oredicts.ROD);
    -        String stick = Oredicts.STICK_WOOD;
    -
    -        shaped(Names.BLOCK, 1, "xxx|xxx|xxx", 'x', gem);
    -        shapeless("unpack_block", Names.INGOT, 9, block);
    -        shaped(Names.INGOT, 1, "xxx|xxx|xxx", 'x', nugget);
    -        shapeless("unpack_gem", Names.NUGGET, 9, gem);
    -        shaped(Names.POWDER, 1, "xxx|xxx|xxx", 'x', stack(Names.SMALLPOWDER, 1));
    -        shapeless("unpack_powder", Names.SMALLPOWDER, 9, stack(Names.POWDER, 1));
    -        shaped(Names.PLATE, Options.plateQuantity(), "xxx", 'x', gem);
    -
    -        shaped(Names.SLAB, 6, "xxx", 'x', block);
    -        shaped(Names.LEVER, 1, "s|x", 's', stick, 'x', gem);
    -        shaped(Names.STAIRS, 4, "x  |xx |xxx", 'x', block);
    -        shaped(Names.WALL, 6, "xxx|xxx", 'x', block);
    -        shaped(Names.BARS, 16, "xxx|xxx", 'x', gem);
    -        shaped("bars_from_rods", Names.BARS, 4, "xxx", 'x', rod);
    -        shaped(Names.PRESSURE_PLATE, 1, "xx", 'x', gem);
    -        shaped(Names.DOOR, 3, "xx|xx|xx", 'x', gem);
    -        shaped(Names.TRAPDOOR, 1, "xx|xx", 'x', gem);
    -        shaped(Names.BUTTON, 1, "x|x", 'x', nugget);
    -        shaped(Names.ROD, Options.rodQuantity(), "x|x", 'x', gem);
    -        shaped(Names.GEAR, Options.gearQuantity(), " x |xrx| x ", 'x', gem, 'r', rod);
    -
    -        if (!Options.disableAllHammerRecipes()) {
    -            shaped(Names.CRACKHAMMER, 1, "x|s|s", 'x', block, 's', stick);
    -        }
    -        shaped(Names.AXE, 1, "xx|xs| s", 'x', gem, 's', stick);
    -        shaped(Names.HOE, 1, "xx| s| s", 'x', gem, 's', stick);
    -        shaped(Names.PICKAXE, 1, "xxx| s | s ", 'x', gem, 's', stick);
    -        shaped(Names.SHOVEL, 1, "x|s|s", 'x', gem, 's', stick);
    -        shaped(Names.SWORD, 1, "x|x|s", 'x', gem, 's', stick);
    -        shaped(Names.SHEARS, 1, " x|x ", 'x', ore(Oredicts.GEM));
    -        shaped(Names.ARROW, 4, "x|r|f", 'x', nugget, 'r', rod, 'f', Oredicts.FEATHER);
    -        shaped(Names.BOW, 1, " rs|r s| rs", 'r', rod, 's', Oredicts.STRING);
    -        shaped(Names.BOLT, 4, "r|r|f", 'r', rod, 'f', Oredicts.FEATHER);
    -        shaped(Names.CROSSBOW, 1, "srr| gr|r s", 'r', rod, 'g', ore(Oredicts.GEAR), 's', Oredicts.STRING);
    -        shaped(Names.FISHING_ROD, 1, "  r| rs|r s", 'r', rod, 's', Oredicts.STRING);
    -        shaped(Names.SHIELD, 1, "xpx|xxx| x ", 'x', gem, 'p', Oredicts.PLANK_WOOD);
    -        if (material.hasItem(Names.SHIELD) && material.hasBlock(Names.PLATE)) {
    -            if (Options.enablePlateRepairs()) add("repair_shield", new ShieldRepairRecipe(material));
    -            if (Options.enableShieldUpgrades()) add("upgrade_shield", new ShieldUpgradeRecipe(material));
    -        }
    -    }
    -
    -    private String ore(String prefix) {
    -        return prefix + material.getCapitalizedName();
    -    }
    -
    -    private ItemStack stack(Names form, int count) {
    -        if (material.hasItem(form)) return material.getItemStack(form, count);
    -        return material.hasBlock(form) ? material.getBlockItemStack(form, count) : ItemStack.EMPTY;
    -    }
    -
    -    private void shaped(Names form, int count, String rows, Object... keys) {
    -        shaped(form.toString(), form, count, rows, keys);
    -    }
    -
    -    private void shaped(String name, Names form, int count, String rows, Object... keys) {
    -        ItemStack result = stack(form, count);
    -        if (result.isEmpty()) return;
    -        for (int index = 1; index < keys.length; index += 2) {
    -            if (!hasIngredient(keys[index])) return;
    -        }
    -
    -        Object[] pattern = new Object[keys.length + 1];
    -        pattern[0] = rows.split("\\|");
    -        System.arraycopy(keys, 0, pattern, 1, keys.length);
    -        add(name, new ShapedOreRecipe(null, result, pattern));
    -    }
    -
    -    private void shapeless(String name, Names form, int count, Object input) {
    -        ItemStack result = stack(form, count);
    -        if (!result.isEmpty() && hasIngredient(input)) {
    -            add(name, new ShapelessOreRecipe(null, result, input));
    -        }
    -    }
    -
    -    private static boolean hasIngredient(Object ingredient) {
    -        if (ingredient instanceof ItemStack) return !((ItemStack) ingredient).isEmpty();
    -        return !OreDictionary.getOres((String) ingredient, false).isEmpty();
    -    }
    -
    -    private void add(String name, IRecipe recipe) {
    -        registry.register(recipe.setRegistryName(new ResourceLocation(BaseGems.MODID, material.getName() + "_" + name)));
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/ItemGroups.java b/src/main/java/zone/moddev/mc/basegems/init/ItemGroups.java
    deleted file mode 100644
    index 785f39e..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/ItemGroups.java
    +++ /dev/null
    @@ -1,29 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import com.mcmoddev.lib.data.Names;
    -import com.mcmoddev.lib.init.MMDCreativeTab;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import net.minecraft.init.Items;
    -
    -public class ItemGroups extends com.mcmoddev.lib.init.ItemGroups {
    -	public static final MMDCreativeTab blocksTab = addTab("blocks", true);
    -	public static final MMDCreativeTab itemsTab = addTab("items", true);
    -	public static final MMDCreativeTab toolsTab = addTab("tools", true);
    -
    -	private ItemGroups() {
    -		throw new IllegalAccessError("Not an instantiable class");
    -	}
    -
    -	public static void setupIcons() {
    -		MMDMaterial diamond = Materials.blackdiamond;
    -		if (diamond != null && diamond.hasBlock(Names.BLOCK)) {
    -			blocksTab.setTabIconItem(diamond.getBlock(Names.BLOCK));
    -		} else {
    -			blocksTab.setTabIconItem(net.minecraft.init.Blocks.DIAMOND_BLOCK);
    -		}
    -		itemsTab.setTabIconItem(diamond != null && diamond.hasItem(Names.GEAR)
    -				? diamond.getItem(Names.GEAR) : Items.DIAMOND);
    -		toolsTab.setTabIconItem(diamond != null && diamond.hasItem(Names.SWORD)
    -				? diamond.getItem(Names.SWORD) : Items.DIAMOND_SWORD);
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/Items.java b/src/main/java/zone/moddev/mc/basegems/init/Items.java
    deleted file mode 100644
    index 1a86e45..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/Items.java
    +++ /dev/null
    @@ -1,78 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.data.Names;
    -import com.mcmoddev.lib.util.Oredicts;
    -import net.minecraft.item.Item;
    -import net.minecraft.item.ItemStack;
    -import net.minecraftforge.oredict.OreDictionary;
    -
    -/**
    - * This class initializes all items in Base Gems.
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -public class Items extends com.mcmoddev.lib.init.Items {
    -
    -	private static final Names[] MATERIAL_FORMS = {
    -			Names.BLEND, Names.INGOT, Names.NUGGET, Names.POWDER, Names.SMALLBLEND,
    -			Names.SMALLPOWDER, Names.ROD, Names.GEAR
    -	};
    -	private static final Names[] TOOL_FORMS = {
    -			Names.ARROW, Names.AXE, Names.BOLT, Names.BOW, Names.CRACKHAMMER, Names.CROSSBOW,
    -			Names.FISHING_ROD, Names.HOE, Names.PICKAXE, Names.SHEARS, Names.SHIELD, Names.SHOVEL,
    -			Names.SWORD
    -	};
    -
    -	protected Items() {
    -		throw new IllegalAccessError("Not a instantiable class");
    -	}
    -
    -	/**
    -	 *
    -	 */
    -	public static void init() {
    -		for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -			for (Names form : MATERIAL_FORMS) {
    -				create(form, material, ItemGroups.itemsTab);
    -			}
    -			for (Names form : TOOL_FORMS) {
    -				create(form, material, ItemGroups.toolsTab);
    -			}
    -			create(Names.DOOR, material, ItemGroups.blocksTab);
    -			create(Names.SLAB, material, ItemGroups.blocksTab);
    -			Item gem = material.getItem(Names.INGOT);
    -			if (gem != null) {
    -				// Shared ore logic looks up GEM. Both names refer to the historical ingot item.
    -				material.addNewItem(Names.GEM, gem);
    -				Oredicts.registerOre(Oredicts.GEM + material.getCapitalizedName(), gem);
    -			}
    -		}
    -	}
    -
    -	static void registerCompoundNameAliases(MMDMaterial material) {
    -		// Match Silent's Gems while keeping the Base Metals keys used by shared recipes.
    -		final String suffix;
    -		switch (material.getName()) {
    -			case "blackdiamond": suffix = "BlackDiamond"; break;
    -			case "bluetopaz": suffix = "BlueTopaz"; break;
    -			case "goldenberyl": suffix = "GoldenBeryl"; break;
    -			case "violetsapphire": suffix = "VioletSapphire"; break;
    -			default: return;
    -		}
    -
    -		String legacySuffix = material.getCapitalizedName();
    -		// Shared material items include the ore and storage block's inventory forms.
    -		for (ItemStack stack : material.getItems()) {
    -			for (int oreId : OreDictionary.getOreIDs(stack)) {
    -				String name = OreDictionary.getOreName(oreId);
    -				if (name.endsWith(legacySuffix)) {
    -					String prefix = name.substring(0, name.length() - legacySuffix.length());
    -					OreDictionary.registerOre(prefix + suffix, stack);
    -				}
    -			}
    -		}
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/Materials.java b/src/main/java/zone/moddev/mc/basegems/init/Materials.java
    deleted file mode 100644
    index 0afff5e..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/Materials.java
    +++ /dev/null
    @@ -1,244 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import zone.moddev.mc.basegems.util.Config.Options;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.material.MMDMaterialType.MaterialType;
    -
    -/**
    - * This class initializes all of the materials in Base Gems.
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -public class Materials extends com.mcmoddev.lib.init.Materials {
    -
    -	/** Agate */
    -	public static MMDMaterial agate;
    -	/** Alexandrite */
    -	public static MMDMaterial alexandrite;
    -	/** Amber */
    -	public static MMDMaterial amber;
    -	/** Amethyst */
    -	public static MMDMaterial amethyst;
    -	/** Ametrine */
    -	public static MMDMaterial ametrine;
    -	/** Aquamarine */
    -	public static MMDMaterial aquamarine;
    -	/** Beryl */
    -	public static MMDMaterial beryl;
    -	/** Black Diamond */
    -	public static MMDMaterial blackdiamond;
    -	/** Blue Topaz */
    -	public static MMDMaterial bluetopaz;
    -	/** Carnelian */
    -	public static MMDMaterial carnelian;
    -	/** Citrine */
    -	public static MMDMaterial citrine;
    -	/** Garnet */
    -	public static MMDMaterial garnet;
    -	/** Golden Beryl */
    -	public static MMDMaterial goldenberyl;
    -	/** Heliodor */
    -	public static MMDMaterial heliodor;
    -	/** Indicolite */
    -	public static MMDMaterial indicolite;
    -	/** Iolite */
    -	public static MMDMaterial iolite;
    -	/** Jade */
    -	public static MMDMaterial jade;
    -	/** Jasper */
    -	public static MMDMaterial jasper;
    -	/** Lepidolite */
    -	public static MMDMaterial lepidolite;
    -	/** Malachite */
    -	public static MMDMaterial malachite;
    -	/** Moldavite */
    -	public static MMDMaterial moldavite;
    -	/** Moonstone */
    -	public static MMDMaterial moonstone;
    -	/**	Morganite */
    -	public static MMDMaterial morganite;
    -	/** Onyx */
    -	public static MMDMaterial onyx;
    -	/** Opal */
    -	public static MMDMaterial opal;
    -	/** Peridot */
    -	public static MMDMaterial peridot;
    -	/** Ruby */
    -	public static MMDMaterial ruby;
    -	/**	Sapphire */
    -	public static MMDMaterial sapphire;
    -	/**	Spinel */
    -	public static MMDMaterial spinel;
    -	/**	Tanzanite */
    -	public static MMDMaterial tanzanite;
    -	/**	Topaz */
    -	public static MMDMaterial topaz;
    -	/** Turquoise */
    -	public static MMDMaterial turquoise;
    -	/** Violet Sapphire */
    -	public static MMDMaterial violetsapphire;
    -
    -	// vanilla imports
    -	/** Diamond */
    -	public static MMDMaterial vanilla_diamond;
    -	/** Emerald */
    -	public static MMDMaterial vanilla_emerald;
    -	/** Quartz */
    -	public static MMDMaterial vanilla_quartz;
    -
    -	private static boolean initDone = false;
    -
    -	protected Materials() {
    -		throw new IllegalAccessError("Not a instantiable class");
    -	}
    -
    -	/**
    -	 *
    -	 */
    -	public static void init() {
    -		if (initDone) {
    -			return;
    -		}
    -
    -		// Base Metals already owns the vanilla gem materials.
    -		vanilla_diamond = getMaterialByName("diamond");
    -		vanilla_emerald = getMaterialByName("emerald");
    -		vanilla_quartz = getMaterialByName("quartz");
    -
    -		// Mod Materials
    -		if (Options.enableAgate) {
    -			agate = createMaterial("agate", MaterialType.GEM, 6.5, 8, 4.5, 0xFF179FD1);
    -		}
    -
    -		if (Options.enableAlexandrite) {
    -			alexandrite = createMaterial("alexandrite", MaterialType.GEM, 8.5, 8, 4.5, 0xFF747385);
    -		}
    -
    -		if (Options.enableAmber) {
    -			amber = createMaterial("amber", MaterialType.GEM, 2, 8, 4.5, 0xFFF9C923);
    -		}
    -
    -		if (Options.enableAmethyst) {
    -			amethyst = createMaterial("amethyst", MaterialType.GEM, 7, 8, 4.5, 0xFFA242E2);
    -		}
    -
    -		if (Options.enableAmetrine) {
    -			ametrine = createMaterial("ametrine", MaterialType.GEM, 7, 8, 4.5, 0xFFC8A394);
    -		}
    -
    -		if (Options.enableAquamarine) {
    -			aquamarine = createMaterial("aquamarine", MaterialType.GEM, 7.5, 8, 4.5, 0xFF78BACA);
    -		}
    -
    -		if (Options.enableBeryl) {
    -			beryl = createMaterial("beryl", MaterialType.GEM, 7.5, 8, 4.5, 0xFF4EB86A);
    -		}
    -
    -		if (Options.enableBlackDiamond) {
    -			blackdiamond = createMaterial("blackdiamond", MaterialType.GEM, 10, 15, 4.5, 0xFF000000);
    -		}
    -
    -		if (Options.enableBlueTopaz) {
    -			bluetopaz = createMaterial("bluetopaz", MaterialType.GEM, 8, 8, 4.5, 0xFF01A4E9);
    -		}
    -
    -		if (Options.enableCarnelian) {
    -			carnelian = createMaterial("carnelian", MaterialType.GEM, 6.5, 8, 4.5, 0xFFF03905);
    -		}
    -
    -		if (Options.enableCitrine) {
    -			citrine = createMaterial("citrine", MaterialType.GEM, 7, 8, 4.5, 0xFFFAD563);
    -		}
    -
    -		if (Options.enableGarnet) {
    -			garnet = createMaterial("garnet", MaterialType.GEM, 6.5, 8, 4.5, 0xFF9D0938);
    -		}
    -
    -		if (Options.enableGoldenBeryl) {
    -			goldenberyl = createMaterial("goldenberyl", MaterialType.GEM, 7.5, 8, 4.5, 0xFFFDEE4C);
    -		}
    -
    -		if (Options.enableHeliodor) {
    -			heliodor = createMaterial("heliodor", MaterialType.GEM, 7.5, 8, 4.5, 0xFFF9FD5D);
    -		}
    -
    -		if (Options.enableIndicolite) {
    -			indicolite = createMaterial("indicolite", MaterialType.GEM, 7, 8, 4.5, 0xFF7EFBF5);
    -		}
    -
    -		if (Options.enableIolite) {
    -			iolite = createMaterial("iolite", MaterialType.GEM, 7, 8, 4.5, 0xFF605DBD);
    -		}
    -
    -		if (Options.enableJade) {
    -			jade = createMaterial("jade", MaterialType.GEM, 6.5, 8, 4.5, 0xFF008127);
    -		}
    -
    -		if (Options.enableJasper) {
    -			jasper = createMaterial("jasper", MaterialType.GEM, 7, 8, 4.5, 0xFF9E0403);
    -		}
    -
    -		if (Options.enableLepidolite) {
    -			lepidolite = createMaterial("lepidolite", MaterialType.GEM, 3, 8, 4.5, 0xFFB980A0);
    -		}
    -
    -		if (Options.enableMalachite) {
    -			malachite = createMaterial("malachite", MaterialType.GEM, 4, 8, 4.5, 0xFF25674C);
    -		}
    -
    -		if (Options.enableMoldavite) {
    -			moldavite = createMaterial("moldavite", MaterialType.GEM, 5, 8, 4.5, 0xFF68873B);
    -		}
    -
    -		if (Options.enableMoonstone) {
    -			moonstone = createMaterial("moonstone", MaterialType.GEM, 6, 8, 4.5, 0xFF9FAED7);
    -		}
    -
    -		if (Options.enableMorganite) {
    -			morganite = createMaterial("morganite", MaterialType.GEM, 7.5, 8, 4.5, 0xFFF4D8CA);
    -		}
    -
    -		if (Options.enableOnyx) {
    -			onyx = createMaterial("onyx", MaterialType.GEM, 7, 8, 4.5, 0xFF000000);
    -		}
    -
    -		if (Options.enableOpal) {
    -			opal = createMaterial("opal", MaterialType.GEM, 6, 8, 4.5, 0xFFC3CCE9);
    -		}
    -
    -		if (Options.enablePeridot) {
    -			peridot = createMaterial("peridot", MaterialType.GEM, 6.5, 8, 4.5, 0xFFB9E64F);
    -		}
    -
    -		if (Options.enableRuby) {
    -			ruby = createMaterial("ruby", MaterialType.GEM, 9, 8, 4.5, 0xFF7E0308);
    -		}
    -
    -		if (Options.enableSapphire) {
    -			sapphire = createMaterial("sapphire", MaterialType.GEM, 9, 8, 4.5, 0xFF1036D5);
    -		}
    -
    -		if (Options.enableSpinel) {
    -			spinel = createMaterial("spinel", MaterialType.GEM, 7.5, 8, 4.5, 0xFFFE2B80);
    -		}
    -
    -		if (Options.enableTanzanite) {
    -			tanzanite = createMaterial("tanzanite", MaterialType.GEM, 6, 8, 4.5, 0xFF4A4AE0);
    -		}
    -
    -		if (Options.enableTopaz) {
    -			topaz = createMaterial("topaz", MaterialType.GEM, 8, 8, 4.5, 0xFFF9E838);
    -		}
    -
    -		if (Options.enableTurquoise) {
    -			turquoise = createMaterial("turquoise", MaterialType.GEM, 5, 8, 4.5, 0xFF3FB2C4);
    -		}
    -
    -		if (Options.enableVioletSapphire) {
    -			violetsapphire = createMaterial("violetsapphire", MaterialType.GEM, 9, 8, 4.5, 0xFF8247B7);
    -		}
    -
    -		initDone = true;
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/PolicyRecipe.java b/src/main/java/zone/moddev/mc/basegems/init/PolicyRecipe.java
    deleted file mode 100644
    index dd31f1a..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/PolicyRecipe.java
    +++ /dev/null
    @@ -1,64 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import zone.moddev.mc.basegems.util.ContentPolicy;
    -import net.minecraft.inventory.InventoryCrafting;
    -import net.minecraft.item.ItemStack;
    -import net.minecraft.item.crafting.IRecipe;
    -import net.minecraft.util.NonNullList;
    -import net.minecraft.world.World;
    -import net.minecraftforge.registries.IForgeRegistryEntry;
    -
    -/** Disables restricted recipes without removing their saved registry names. */
    -final class PolicyRecipe extends IForgeRegistryEntry.Impl implements IRecipe {
    -	private final IRecipe delegate;
    -	private final boolean allowed;
    -
    -	PolicyRecipe(final IRecipe delegate) {
    -		this.delegate = delegate;
    -
    -		this.allowed = ContentPolicy.allows(delegate.getRecipeOutput());
    -		setRegistryName(delegate.getRegistryName());
    -	}
    -
    -	@Override
    -	public boolean matches(final InventoryCrafting inventory, final World world) {
    -		return allowed && delegate.matches(inventory, world) && ContentPolicy.allows(delegate.getCraftingResult(inventory));
    -	}
    -
    -	@Override
    -	public ItemStack getCraftingResult(final InventoryCrafting inventory) {
    -		if (!allowed) return ItemStack.EMPTY;
    -        final ItemStack result = delegate.getCraftingResult(inventory);
    -        return ContentPolicy.allows(result) ? result : ItemStack.EMPTY;
    -	}
    -
    -	@Override
    -	public boolean canFit(final int width, final int height) {
    -		return allowed && delegate.canFit(width, height);
    -	}
    -
    -	@Override
    -	public ItemStack getRecipeOutput() {
    -		return allowed ? delegate.getRecipeOutput() : ItemStack.EMPTY;
    -	}
    -
    -	@Override
    -	public NonNullList getRemainingItems(final InventoryCrafting inventory) {
    -		return delegate.getRemainingItems(inventory);
    -	}
    -
    -	@Override
    -	public NonNullList getIngredients() {
    -		return delegate.getIngredients();
    -	}
    -
    -	@Override
    -	public boolean isDynamic() {
    -		return !allowed || delegate.isDynamic();
    -	}
    -
    -	@Override
    -	public String getGroup() {
    -		return delegate.getGroup();
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/RegistryHandlers.java b/src/main/java/zone/moddev/mc/basegems/init/RegistryHandlers.java
    deleted file mode 100644
    index e84dd33..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/RegistryHandlers.java
    +++ /dev/null
    @@ -1,91 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import com.mcmoddev.lib.events.MMDLibRegisterBlocks;
    -import com.mcmoddev.lib.events.MMDLibRegisterFluids;
    -import com.mcmoddev.lib.events.MMDLibRegisterItems;
    -import com.mcmoddev.lib.events.MMDLibRegisterMaterials;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.util.Oredicts;
    -import java.util.ArrayList;
    -import java.util.List;
    -import net.minecraft.block.Block;
    -import net.minecraft.item.Item;
    -import net.minecraft.item.ItemStack;
    -import net.minecraft.item.crafting.IRecipe;
    -import net.minecraftforge.event.RegistryEvent;
    -import net.minecraftforge.fml.common.Mod;
    -import net.minecraftforge.fml.common.eventhandler.EventPriority;
    -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
    -import zone.moddev.mc.basegems.BaseGems;
    -import zone.moddev.mc.basegems.util.Config;
    -import zone.moddev.mc.basegems.util.ContentPolicy;
    -
    -@Mod.EventBusSubscriber(modid = BaseGems.MODID)
    -public final class RegistryHandlers {
    -    private RegistryHandlers() { }
    -
    -    @SubscribeEvent
    -    public static void createMaterials(MMDLibRegisterMaterials event) {
    -        Materials.init();
    -    }
    -
    -    @SubscribeEvent
    -    public static void createBlocks(MMDLibRegisterBlocks event) {
    -        Blocks.init();
    -    }
    -
    -    @SubscribeEvent
    -    public static void createItems(MMDLibRegisterItems event) {
    -        Items.init();
    -    }
    -
    -    @SubscribeEvent
    -    public static void createFluids(MMDLibRegisterFluids event) {
    -        Fluids.init();
    -    }
    -
    -    @SubscribeEvent
    -    public static void registerBlocks(RegistryEvent.Register event) {
    -        for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -            material.getBlocks().stream().filter(block -> BaseGems.MODID.equals(block.getRegistryName().getNamespace()))
    -                    .forEach(event.getRegistry()::register);
    -        }
    -    }
    -
    -    @SubscribeEvent
    -    public static void registerItems(RegistryEvent.Register event) {
    -        for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -            material.getItems().stream().map(ItemStack::getItem).distinct()
    -                    .filter(item -> item.getRegistryName() != null && BaseGems.MODID.equals(item.getRegistryName().getNamespace()))
    -                    .forEach(event.getRegistry()::register);
    -        }
    -
    -        Oredicts.registerItemOreDictionaryEntries();
    -        Oredicts.registerBlockOreDictionaryEntries();
    -        for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -            Items.registerCompoundNameAliases(material);
    -        }
    -    }
    -
    -    @SubscribeEvent(priority = EventPriority.LOWEST)
    -    public static void registerRecipes(RegistryEvent.Register event) {
    -        com.mcmoddev.lib.init.Recipes.register(event);
    -        for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -            new GemRecipes(event.getRegistry(), material).register();
    -        }
    -        if (!Config.lowFantasy()) return;
    -
    -        List restricted = new ArrayList<>();
    -        for (IRecipe recipe : event.getRegistry()) {
    -            boolean sharedDynamic = recipe.getRecipeOutput().isEmpty()
    -                    && recipe.getClass().getName().startsWith("com.mcmoddev.lib.recipe.");
    -            if (!ContentPolicy.allows(recipe.getRecipeOutput()) || sharedDynamic) {
    -                restricted.add(recipe);
    -            }
    -        }
    -        // Preserve recipe IDs so worlds and recipe books can switch modes safely.
    -        for (IRecipe recipe : restricted) {
    -            event.getRegistry().register(new PolicyRecipe(recipe));
    -        }
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/init/VillagerTrades.java b/src/main/java/zone/moddev/mc/basegems/init/VillagerTrades.java
    deleted file mode 100644
    index ec8d2dd..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/init/VillagerTrades.java
    +++ /dev/null
    @@ -1,76 +0,0 @@
    -package zone.moddev.mc.basegems.init;
    -
    -import java.util.ArrayList;
    -import java.util.Comparator;
    -import java.util.EnumMap;
    -import java.util.List;
    -import java.util.Map;
    -
    -import com.mcmoddev.lib.data.MaterialStats;
    -import com.mcmoddev.lib.data.Names;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import net.minecraft.init.Items;
    -import net.minecraft.item.Item;
    -import net.minecraft.item.ItemStack;
    -import net.minecraft.village.MerchantRecipe;
    -import zone.moddev.mc.basegems.BaseGems;
    -import zone.moddev.mc.basegems.data.GemRarity;
    -import com.mcmoddev.lib.util.VillagerTradeHelper;
    -
    -public final class VillagerTrades extends com.mcmoddev.lib.init.VillagerTrades {
    -    private static final int[] SMITH_CAREERS = {ARMOR_SMITH_ID, WEAPON_SMITH_ID, TOOL_SMITH_ID};
    -    private static final Names[] TOOLS = {Names.CRACKHAMMER, Names.AXE, Names.HOE, Names.SHOVEL, Names.PICKAXE};
    -    private static final int GEM_TRADE_USES = 6;
    -    private static boolean initDone;
    -
    -    private VillagerTrades() { }
    -
    -    public static void init() {
    -        if (initDone) {
    -            return;
    -        }
    -
    -        Map> gemsByRarity = new EnumMap<>(GemRarity.class);
    -        for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -            registerEquipment(material);
    -            if (material.hasItem(Names.GEM)) {
    -                GemRarity rarity = GemRarity.of(material.getName());
    -                gemsByRarity.computeIfAbsent(rarity, key -> new ArrayList<>()).add(material.getItem(Names.GEM));
    -            }
    -        }
    -
    -        // One gem and its two prices per tier, so the rest of the smith's offers stay usable.
    -        gemsByRarity.forEach((rarity, gems) -> {
    -            gems.sort(Comparator.comparing(gem -> gem.getRegistryName().getPath()));
    -            for (int career : SMITH_CAREERS) {
    -                VillagerTradeHelper.insertTrades(SMITH_RL, career, rarity.tradeLevel, (merchant, offers, random) -> {
    -                    Item gem = gems.get(random.nextInt(gems.size()));
    -                    ItemStack bundle = new ItemStack(gem, rarity.gemsPerTrade);
    -                    offers.add(new MerchantRecipe(bundle, ItemStack.EMPTY, new ItemStack(Items.EMERALD), 0, GEM_TRADE_USES));
    -                    offers.add(new MerchantRecipe(new ItemStack(Items.EMERALD, rarity.emeraldsToBuy),
    -                            ItemStack.EMPTY, bundle.copy(), 0, GEM_TRADE_USES));
    -                });
    -            }
    -        });
    -        initDone = true;
    -    }
    -
    -    private static void registerEquipment(MMDMaterial material) {
    -        float value = material.getStat(MaterialStats.HARDNESS) + material.getStat(MaterialStats.STRENGTH)
    -                + material.getStat(MaterialStats.MAGICAFFINITY) + material.getToolHarvestLevel();
    -        int price = emeraldPurchaseValue(value);
    -        int level = tradeLevel(value);
    -        for (Names form : TOOLS) {
    -            registerEquipment(material, form, TOOL_SMITH_ID, level, price);
    -        }
    -        registerEquipment(material, Names.SWORD, WEAPON_SMITH_ID, level,
    -                price + (int) (material.getBaseAttackDamage() / 2) - 1);
    -    }
    -
    -    private static void registerEquipment(MMDMaterial material, Names form, int career, int level, int price) {
    -        if (material.hasItem(form)) {
    -            VillagerTradeHelper.insertTrades(SMITH_RL, career, level,
    -                    makePurchasePalette(price, material.getItemStack(form)));
    -        }
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/integration/plugins/EnderIO.java b/src/main/java/zone/moddev/mc/basegems/integration/plugins/EnderIO.java
    deleted file mode 100644
    index c2fa6d8..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/integration/plugins/EnderIO.java
    +++ /dev/null
    @@ -1,28 +0,0 @@
    -package zone.moddev.mc.basegems.integration.plugins;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.integration.MMDPlugin;
    -import com.mcmoddev.lib.integration.IIntegration;
    -
    -/**
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -@MMDPlugin(addonId = BaseGems.MODID, pluginId = EnderIO.PLUGIN_MODID)
    -public class EnderIO extends com.mcmoddev.lib.integration.plugins.EnderIOBase implements IIntegration {
    -
    -	private static boolean initDone = false;
    -
    -	/**
    -	 *
    -	 */
    -	@Override
    -	public void init() {
    -		if (initDone || !com.mcmoddev.lib.util.ConfigBase.Options.isModEnabled(PLUGIN_MODID)) {
    -			return;
    -		}
    -
    -		initDone = true;
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/integration/plugins/IC2.java b/src/main/java/zone/moddev/mc/basegems/integration/plugins/IC2.java
    deleted file mode 100644
    index b1fe9e0..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/integration/plugins/IC2.java
    +++ /dev/null
    @@ -1,20 +0,0 @@
    -package zone.moddev.mc.basegems.integration.plugins;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.integration.MMDPlugin;
    -import com.mcmoddev.lib.integration.IIntegration;
    -
    -@MMDPlugin(addonId = BaseGems.MODID, pluginId = IC2.PLUGIN_MODID)
    -public class IC2 extends com.mcmoddev.lib.integration.plugins.IC2Base implements IIntegration {
    -
    -	private static boolean initDone = false;
    -
    -	@Override
    -	public void init() {
    -		if (initDone || !com.mcmoddev.lib.util.ConfigBase.Options.isModEnabled(PLUGIN_MODID)) {
    -			return;
    -		}
    -
    -		initDone = true;
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/integration/plugins/Mekanism.java b/src/main/java/zone/moddev/mc/basegems/integration/plugins/Mekanism.java
    deleted file mode 100644
    index 1d1181f..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/integration/plugins/Mekanism.java
    +++ /dev/null
    @@ -1,26 +0,0 @@
    -package zone.moddev.mc.basegems.integration.plugins;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import zone.moddev.mc.basegems.init.Materials;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.integration.MMDPlugin;
    -import com.mcmoddev.lib.integration.IIntegration;
    -
    -@MMDPlugin(addonId = BaseGems.MODID, pluginId = Mekanism.PLUGIN_MODID)
    -public class Mekanism extends com.mcmoddev.lib.integration.plugins.MekanismBase implements IIntegration {
    -
    -	private static boolean initDone = false;
    -
    -	@Override
    -	public void init() {
    -		if (initDone || !com.mcmoddev.lib.util.ConfigBase.Options.isModEnabled(PLUGIN_MODID)) {
    -			return;
    -		}
    -
    -		for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -			addOreMultiplicationRecipes(material);
    -		}
    -
    -		initDone = true;
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/integration/plugins/Thaumcraft.java b/src/main/java/zone/moddev/mc/basegems/integration/plugins/Thaumcraft.java
    deleted file mode 100644
    index ef78e4b..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/integration/plugins/Thaumcraft.java
    +++ /dev/null
    @@ -1,20 +0,0 @@
    -package zone.moddev.mc.basegems.integration.plugins;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.integration.MMDPlugin;
    -import com.mcmoddev.lib.integration.IIntegration;
    -
    -@MMDPlugin(addonId = BaseGems.MODID, pluginId = Thaumcraft.PLUGIN_MODID)
    -public class Thaumcraft extends com.mcmoddev.lib.integration.plugins.ThaumcraftBase implements IIntegration {
    -
    -	private static boolean initDone = false;
    -
    -	@Override
    -	public void init() {
    -		if (initDone || !com.mcmoddev.lib.util.ConfigBase.Options.isModEnabled(PLUGIN_MODID)) {
    -			return;
    -		}
    -
    -		initDone = true;
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/integration/plugins/TinkersConstruct.java b/src/main/java/zone/moddev/mc/basegems/integration/plugins/TinkersConstruct.java
    deleted file mode 100644
    index 7f7d4f8..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/integration/plugins/TinkersConstruct.java
    +++ /dev/null
    @@ -1,59 +0,0 @@
    -package zone.moddev.mc.basegems.integration.plugins;
    -
    -import com.mcmoddev.lib.data.Names;
    -import net.minecraftforge.fluids.Fluid;
    -import slimeknights.tconstruct.library.TinkerRegistry;
    -import zone.moddev.mc.basegems.BaseGems;
    -import zone.moddev.mc.basegems.init.Materials;
    -import com.mcmoddev.lib.material.MMDMaterial;
    -import com.mcmoddev.lib.integration.MMDPlugin;
    -import com.mcmoddev.lib.integration.IIntegration;
    -
    -/**
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -@MMDPlugin(addonId = BaseGems.MODID, pluginId = TinkersConstruct.PLUGIN_MODID)
    -public class TinkersConstruct extends com.mcmoddev.lib.integration.plugins.TinkersConstructBase implements IIntegration {
    -
    -	private static boolean initDone = false;
    -
    -	@Override
    -	public void init() {
    -		if (initDone || !com.mcmoddev.lib.util.ConfigBase.Options.isModEnabled(PLUGIN_MODID)) {
    -			return;
    -		}
    -
    -		for (MMDMaterial material : Materials.getMaterialsByMod(BaseGems.MODID)) {
    -			Fluid fluid = material.getFluid();
    -            if (fluid == null) continue;
    -
    -            melt(material, fluid, Names.ORE, 288);
    -            melt(material, fluid, Names.BLOCK, 1296);
    -            melt(material, fluid, Names.INGOT, 144);
    -            melt(material, fluid, Names.NUGGET, 16);
    -            melt(material, fluid, Names.POWDER, 144);
    -            melt(material, fluid, Names.SMALLPOWDER, 16);
    -            melt(material, fluid, Names.SLAB, 648);
    -            melt(material, fluid, Names.WALL, 1296);
    -            melt(material, fluid, Names.SHEARS, 288);
    -            melt(material, fluid, Names.PRESSURE_PLATE, 288);
    -            melt(material, fluid, Names.BARS, 54);
    -            melt(material, fluid, Names.ROD, 72);
    -            melt(material, fluid, Names.DOOR, 288);
    -            melt(material, fluid, Names.TRAPDOOR, 576);
    -            melt(material, fluid, Names.BUTTON, 32);
    -		}
    -
    -		initDone = true;
    -	}
    -
    -    private static void melt(MMDMaterial material, Fluid fluid, Names form, int amount) {
    -        if (material.hasItem(form)) {
    -            TinkerRegistry.registerMelting(material.getItem(form), fluid, amount);
    -        } else if (material.hasBlock(form)) {
    -            TinkerRegistry.registerMelting(material.getBlock(form), fluid, amount);
    -        }
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/integration/plugins/VeinMiner.java b/src/main/java/zone/moddev/mc/basegems/integration/plugins/VeinMiner.java
    deleted file mode 100644
    index 70b678a..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/integration/plugins/VeinMiner.java
    +++ /dev/null
    @@ -1,26 +0,0 @@
    -package zone.moddev.mc.basegems.integration.plugins;
    -
    -import zone.moddev.mc.basegems.BaseGems;
    -import com.mcmoddev.lib.integration.MMDPlugin;
    -import com.mcmoddev.lib.integration.IIntegration;
    -
    -/**
    - * VeinMiner Integration Plugin
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -@MMDPlugin(addonId = BaseGems.MODID, pluginId = VeinMiner.PLUGIN_MODID)
    -public class VeinMiner extends com.mcmoddev.lib.integration.plugins.VeinMinerBase implements IIntegration {
    -
    -	private static boolean initDone = false;
    -
    -	@Override
    -	public void init() {
    -		if (initDone || !com.mcmoddev.lib.util.ConfigBase.Options.isModEnabled(PLUGIN_MODID)) {
    -			return;
    -		}
    -
    -		initDone = true;
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/migration/LegacyDoorStates.java b/src/main/java/zone/moddev/mc/basegems/migration/LegacyDoorStates.java
    new file mode 100644
    index 0000000..c70a727
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/migration/LegacyDoorStates.java
    @@ -0,0 +1,119 @@
    +package zone.moddev.mc.basegems.migration;
    +
    +import java.util.HashMap;
    +import java.util.Map;
    +
    +import net.minecraft.block.Block;
    +import net.minecraft.block.BlockDoor;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.nbt.NBTTagCompound;
    +import net.minecraft.nbt.NBTTagList;
    +import net.minecraft.nbt.NBTUtil;
    +import net.minecraft.util.BitArray;
    +
    +/** An old door stores facing and open on its bottom half, and hinge and power on its top. */
    +final class LegacyDoorStates {
    +    private static final String MARKER = "BaseGemsLegacyDoors";
    +    private static final int SECTION_SIZE = 4096;
    +
    +    private LegacyDoorStates() {}
    +
    +    static void capture(NBTTagCompound level, Block[] doorIds) {
    +        NBTTagList list = level.getList("Sections", 10);
    +        RawSection[] sections = new RawSection[16];
    +        for (int i = 0; i < list.size(); i++) {
    +            NBTTagCompound section = list.getCompound(i);
    +            int y = section.getByte("Y");
    +            if (y >= 0 && y < sections.length && section.contains("Blocks", 7)) {
    +                sections[y] = new RawSection(section);
    +            }
    +        }
    +        NBTTagList corrections = new NBTTagList();
    +        for (int y = 0; y < sections.length; y++) {
    +            RawSection lower = sections[y];
    +            if (lower == null) continue;
    +            for (int i = 0; i < SECTION_SIZE; i++) {
    +                int id = lower.id(i);
    +                Block door = doorIds[id];
    +                if (door == null || (lower.meta(i) & 8) != 0) continue;
    +                int upperIndex = i + 256;
    +                int upperY = y + upperIndex / SECTION_SIZE;
    +                upperIndex %= SECTION_SIZE;
    +                if (upperY >= sections.length) continue;
    +                RawSection upper = sections[upperY];
    +                if (upper == null || upper.id(upperIndex) != id || (upper.meta(upperIndex) & 8) == 0) continue;
    +
    +                IBlockState bottom = LegacyWorldDataHook.legacyState(door, door.getRegistryName().getPath(), lower.meta(i));
    +                IBlockState top = LegacyWorldDataHook.legacyState(door, door.getRegistryName().getPath(), upper.meta(upperIndex));
    +                IBlockState correctedBottom = bottom.with(BlockDoor.HINGE, top.get(BlockDoor.HINGE))
    +                        .with(BlockDoor.POWERED, top.get(BlockDoor.POWERED));
    +                IBlockState correctedTop = top.with(BlockDoor.FACING, bottom.get(BlockDoor.FACING))
    +                        .with(BlockDoor.OPEN, bottom.get(BlockDoor.OPEN));
    +                corrections.add(correction(y, i, correctedBottom));
    +                corrections.add(correction(upperY, upperIndex, correctedTop));
    +            }
    +        }
    +        if (!corrections.isEmpty()) level.setTag(MARKER, corrections);
    +    }
    +
    +    static void apply(NBTTagCompound level) {
    +        NBTTagList corrections = level.getList(MARKER, 10);
    +        if (corrections.isEmpty()) return;
    +        Map> bySection = new HashMap<>();
    +        for (int i = 0; i < corrections.size(); i++) {
    +            NBTTagCompound change = corrections.getCompound(i);
    +            bySection.computeIfAbsent(change.getInt("Y"), key -> new HashMap<>())
    +                    .put(change.getInt("Index"), change.getCompound("State"));
    +        }
    +        NBTTagList sections = level.getList("Sections", 10);
    +        for (int i = 0; i < sections.size(); i++) {
    +            NBTTagCompound section = sections.getCompound(i);
    +            Map changes = bySection.get((int) section.getByte("Y"));
    +            if (changes == null) continue;
    +            NBTTagList palette = section.getList("Palette", 10);
    +            BitArray original = new BitArray(bits(palette.size()), SECTION_SIZE, section.getLongArray("BlockStates"));
    +            Map indices = new HashMap<>();
    +            changes.forEach((index, state) -> {
    +                int entry = palette.indexOf(state);
    +                if (entry < 0) {
    +                    entry = palette.size();
    +                    palette.add(state);
    +                }
    +                indices.put(index, entry);
    +            });
    +            BitArray flattened = new BitArray(bits(palette.size()), SECTION_SIZE);
    +            for (int index = 0; index < SECTION_SIZE; index++) {
    +                flattened.setAt(index, indices.getOrDefault(index, original.getAt(index)));
    +            }
    +            section.setLongArray("BlockStates", flattened.getBackingLongArray());
    +        }
    +        level.removeTag(MARKER);
    +    }
    +
    +    private static int bits(int states) { return Math.max(4, 32 - Integer.numberOfLeadingZeros(states - 1)); }
    +
    +    private static NBTTagCompound correction(int y, int index, IBlockState state) {
    +        NBTTagCompound tag = new NBTTagCompound();
    +        tag.setInt("Y", y);
    +        tag.setInt("Index", index);
    +        tag.setTag("State", NBTUtil.writeBlockState(state));
    +        return tag;
    +    }
    +
    +    private static final class RawSection {
    +        private final byte[] blocks, add, data;
    +
    +        RawSection(NBTTagCompound tag) {
    +            blocks = tag.getByteArray("Blocks");
    +            add = tag.getByteArray("Add");
    +            data = tag.getByteArray("Data");
    +        }
    +
    +        int id(int index) { return (blocks[index] & 255) | (nibble(add, index) << 8); }
    +        int meta(int index) { return nibble(data, index); }
    +
    +        private static int nibble(byte[] array, int index) {
    +            return array.length == 2048 ? (array[index >> 1] >> ((index & 1) * 4)) & 15 : 0;
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/basegems/migration/LegacyWorldDataHook.java
    new file mode 100644
    index 0000000..d538c5a
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/migration/LegacyWorldDataHook.java
    @@ -0,0 +1,466 @@
    +package zone.moddev.mc.basegems.migration;
    +
    +import java.io.File;
    +import java.io.FileInputStream;
    +import java.io.FileOutputStream;
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.lang.reflect.Field;
    +import java.lang.reflect.Modifier;
    +import java.nio.file.Files;
    +import java.nio.file.StandardCopyOption;
    +import java.util.ArrayList;
    +import java.util.Arrays;
    +import java.util.Collections;
    +import java.util.LinkedHashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Locale;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.concurrent.ConcurrentHashMap;
    +
    +import com.mojang.datafixers.Dynamic;
    +
    +import cpw.mods.modlauncher.api.INameMappingService;
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.content.CompatibilityDoubleSlabBlock;
    +import zone.moddev.mc.basegems.content.ModContent;
    +import zone.moddev.mc.basegems.content.PlateBlock;
    +
    +import net.minecraft.block.Block;
    +import net.minecraft.block.BlockButton;
    +import net.minecraft.block.BlockDoor;
    +import net.minecraft.block.BlockHorizontal;
    +import net.minecraft.block.BlockHorizontalFace;
    +import net.minecraft.block.BlockLever;
    +import net.minecraft.block.BlockPressurePlate;
    +import net.minecraft.block.BlockSlab;
    +import net.minecraft.block.BlockStairs;
    +import net.minecraft.block.BlockTrapDoor;
    +import net.minecraft.block.BlockFlowingFluid;
    +import net.minecraft.block.state.IBlockState;
    +import net.minecraft.nbt.CompressedStreamTools;
    +import net.minecraft.nbt.INBTBase;
    +import net.minecraft.nbt.NBTTagCompound;
    +import net.minecraft.nbt.NBTTagList;
    +import net.minecraft.nbt.NBTUtil;
    +import net.minecraft.state.properties.AttachFace;
    +import net.minecraft.state.properties.DoorHingeSide;
    +import net.minecraft.state.properties.DoubleBlockHalf;
    +import net.minecraft.state.properties.Half;
    +import net.minecraft.state.properties.SlabType;
    +import net.minecraft.state.properties.StairsShape;
    +import net.minecraft.util.EnumFacing;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.util.datafix.fixes.BlockStateFlatteningMap;
    +import net.minecraft.world.gen.WorldGenRegion;
    +import net.minecraft.world.storage.ISaveHandler;
    +import net.minecraft.world.storage.SaveHandler;
    +import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
    +import net.minecraftforge.registries.ForgeRegistries;
    +
    +/**
    + * Restores old modded block IDs before Minecraft's 1.13 world conversion can
    + * replace them with air. Also converts old item names, universal buckets and
    + * durability data before Minecraft reads the items.
    + */
    +public final class LegacyWorldDataHook {
    +    private static final ResourceLocation BLOCK_REGISTRY = new ResourceLocation("minecraft", "blocks");
    +    private static final Map> LEGACY_GEMS_CHUNKS = new ConcurrentHashMap<>();
    +    private static final Map> INSTALLED_STATES = new LinkedHashMap<>();
    +    private static final Map> REPLACED_STATES = new LinkedHashMap<>();
    +    private static final Block[] LEGACY_DOORS = new Block[4096];
    +    private static final String PRESERVE_CHUNK_MARKER = "BaseGemsLegacyPreserveChunk";
    +    private static volatile boolean legacyWorldActive;
    +
    +    private LegacyWorldDataHook() {}
    +
    +    /** Save the world's registry once so unvisited chunks can be upgraded on later starts. */
    +    public static synchronized void prepareLegacyWorld(File levelDat, SaveHandler handler) {
    +        if (handler == null) return;
    +        legacyWorldActive = false;
    +        LEGACY_GEMS_CHUNKS.clear();
    +        Arrays.fill(LEGACY_DOORS, null);
    +        restorePreviousStates();
    +        File directory = levelDat.getParentFile();
    +        File sidecar = new File(directory, "basegems-legacy-registry.dat");
    +        try {
    +            NBTTagCompound root = read(levelDat);
    +            NBTTagCompound snapshot = read(sidecar);
    +            NBTTagCompound registry = snapshot == null ? legacyRegistry(root) : snapshot.getCompound("Blocks");
    +            if (registry == null && snapshot == null) {
    +                registry = legacyRegistry(read(new File(directory, "level.dat_old")));
    +            }
    +            if (registry == null) return;
    +            int states = installLegacyBlockStates(registry);
    +            if (snapshot != null && states == 0) {
    +                throw new IllegalStateException("Legacy Base Gems snapshot has no valid block IDs: " + sidecar);
    +            }
    +            if (states == 0) return;
    +            if (snapshot == null) {
    +                captureLegacyChunks(directory);
    +                snapshot = new NBTTagCompound();
    +                snapshot.setTag("Blocks", registry);
    +                NBTTagCompound chunks = new NBTTagCompound();
    +                LEGACY_GEMS_CHUNKS.forEach((dimension, positions) ->
    +                        chunks.setLongArray(dimension.toString(), new ArrayList<>(positions)));
    +                snapshot.setTag("Chunks", chunks);
    +                writeWithBackup(sidecar, snapshot);
    +            } else {
    +                NBTTagCompound chunks = snapshot.getCompound("Chunks");
    +                for (String dimension : chunks.keySet()) {
    +                    Set positions = new HashSet<>();
    +                    for (long position : chunks.getLongArray(dimension)) positions.add(position);
    +                    LEGACY_GEMS_CHUNKS.put(Integer.parseInt(dimension), Collections.unmodifiableSet(positions));
    +                }
    +            }
    +            if (root != null) migrateLoosePlayerData(levelDat, root);
    +            legacyWorldActive = true;
    +            BaseGems.LOGGER.info("Prepared {} legacy Base Gems states and protected {} existing chunks in '{}'",
    +                    states, LEGACY_GEMS_CHUNKS.values().stream().mapToInt(Set::size).sum(), directory);
    +        } catch (IOException exception) {
    +            throw new IllegalStateException("Could not prepare legacy Base Gems world " + directory, exception);
    +        }
    +    }
    +
    +    /** The optimiser can run before SaveHandler.loadWorldInfo, on a separate worker. */
    +    public static void prepareOptimizer(ISaveHandler handler) {
    +        if (handler instanceof SaveHandler) {
    +            SaveHandler save = (SaveHandler) handler;
    +            prepareLegacyWorld(new File(save.getWorldDirectory(), "level.dat"), save);
    +        }
    +    }
    +
    +    private static NBTTagCompound read(File file) throws IOException {
    +        if (!file.isFile()) return null;
    +        try (FileInputStream input = new FileInputStream(file)) {
    +            return CompressedStreamTools.readCompressed(input);
    +        }
    +    }
    +
    +    private static NBTTagCompound legacyRegistry(NBTTagCompound root) {
    +        if (root == null) return null;
    +        NBTTagCompound registry = root.getCompound("FML").getCompound("Registries")
    +                .getCompound(BLOCK_REGISTRY.toString());
    +        return registry.contains("ids", 9) ? registry : null;
    +    }
    +
    +    /** Called by the coremod immediately after a legacy chunk NBT is read. */
    +    public static synchronized void prepareLegacyChunk(NBTTagCompound root) {
    +        if (!legacyWorldActive || root == null || root.getInt("DataVersion") > 1343 || !root.contains("Level", 10)) return;
    +        migrateLegacyItems(root);
    +        NBTTagCompound level = root.getCompound("Level");
    +        LegacyDoorStates.capture(level, LEGACY_DOORS);
    +        // Chests or entities may contain the only Base Gems items in this chunk.
    +        // Mark it as finished terrain too, or 1.13 can regenerate it and lose those items.
    +        level.setBoolean("TerrainPopulated", true);
    +        level.setBoolean("LightPopulated", true);
    +        level.setBoolean(PRESERVE_CHUNK_MARKER, true);
    +    }
    +
    +    /** Called after vanilla datafixing, before a legacy chunk is returned. */
    +    public static NBTTagCompound finalizeLegacyChunk(NBTTagCompound root) {
    +        if (root != null && root.contains("Level", 10)) {
    +            NBTTagCompound level = root.getCompound("Level");
    +            if (level.getBoolean(PRESERVE_CHUNK_MARKER)) {
    +                level.setString("Status", "postprocessed");
    +                LegacyDoorStates.apply(level);
    +                level.removeTag(PRESERVE_CHUNK_MARKER);
    +            }
    +        }
    +        return root;
    +    }
    +
    +    public static boolean shouldBlockWorldgenWrite(WorldGenRegion region, net.minecraft.util.math.BlockPos position) {
    +        if (!legacyWorldActive) return false;
    +        Set chunks = LEGACY_GEMS_CHUNKS.get(region.getDimension().getType().getId());
    +        return chunks != null && chunks.contains(chunkKey(position.getX() >> 4, position.getZ() >> 4));
    +    }
    +
    +    private static void restorePreviousStates() {
    +        if (INSTALLED_STATES.isEmpty()) return;
    +        Dynamic[] table = expandFlatteningTable(0);
    +        INSTALLED_STATES.forEach((id, state) -> {
    +            if (table[id] == state) table[id] = REPLACED_STATES.get(id);
    +        });
    +        INSTALLED_STATES.clear();
    +        REPLACED_STATES.clear();
    +    }
    +
    +    private static int installLegacyBlockStates(NBTTagCompound snapshot) {
    +        Map ids = new LinkedHashMap();
    +        NBTTagList savedIds = snapshot.getList("ids", 10);
    +        int highestState = 0;
    +        for (int index = 0; index < savedIds.size(); index++) {
    +            NBTTagCompound entry = savedIds.getCompound(index);
    +            String normalized = normalizeLegacyRegistryName(entry.getString("K"));
    +            int separator = normalized.indexOf(':');
    +            if (separator < 0) continue;
    +            String namespace = normalized.substring(0, separator);
    +            if (!BaseGems.MOD_ID.equals(namespace)) continue;
    +            ResourceLocation id = new ResourceLocation(normalized);
    +            int numeric = entry.getInt("V");
    +            if (numeric <= 0 || numeric >= 4096) throw new IllegalStateException("Invalid legacy Base Gems ID " + numeric);
    +            ids.put(id, Integer.valueOf(numeric));
    +            highestState = Math.max(highestState, (numeric << 4) | 15);
    +        }
    +        if (ids.isEmpty()) return 0;
    +        Dynamic[] table = expandFlatteningTable(highestState + 1);
    +        int mapped = 0;
    +        for (Map.Entry entry : ids.entrySet()) {
    +            Block block = resolveCurrentBlock(entry.getKey());
    +            if (block instanceof BlockDoor) LEGACY_DOORS[entry.getValue()] = block;
    +            for (int meta = 0; meta < 16; meta++) {
    +                IBlockState state = legacyState(block, entry.getKey().getPath(), meta);
    +                int id = (entry.getValue() << 4) | meta;
    +                Dynamic fixed = BlockStateFlatteningMap.makeDynamic(NBTUtil.writeBlockState(state).toString());
    +                REPLACED_STATES.put(id, table[id]);
    +                INSTALLED_STATES.put(id, fixed);
    +                table[id] = fixed;
    +                mapped++;
    +            }
    +        }
    +        return mapped;
    +    }
    +
    +    private static Block resolveCurrentBlock(ResourceLocation oldId) {
    +        String path = oldId.getPath();
    +        Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(BaseGems.MOD_ID, path));
    +        if (block == null) throw new IllegalStateException("No Base Gems replacement for legacy block " + oldId);
    +        return block;
    +    }
    +
    +    static IBlockState legacyState(Block block, String path, int meta) {
    +        IBlockState state = block.getDefaultState();
    +        if (block instanceof PlateBlock) return state.with(PlateBlock.FACING, EnumFacing.byIndex(meta));
    +        if (block instanceof CompatibilityDoubleSlabBlock) return state.with(BlockSlab.TYPE, SlabType.DOUBLE);
    +        if (block instanceof BlockSlab) return state.with(BlockSlab.TYPE,
    +                (meta & 8) == 0 ? SlabType.BOTTOM : SlabType.TOP);
    +        if (block instanceof BlockStairs) {
    +            EnumFacing facing = EnumFacing.byIndex(5 - (meta & 3));
    +            return state.with(BlockStairs.FACING, facing)
    +                    .with(BlockStairs.HALF, (meta & 4) == 0 ? Half.BOTTOM : Half.TOP)
    +                    .with(BlockStairs.SHAPE, StairsShape.STRAIGHT)
    +                    .with(BlockStairs.WATERLOGGED, Boolean.FALSE);
    +        }
    +        if (block instanceof BlockDoor) {
    +            if ((meta & 8) != 0) {
    +                return state.with(BlockDoor.HALF, DoubleBlockHalf.UPPER)
    +                        .with(BlockDoor.HINGE, (meta & 1) != 0 ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
    +                        .with(BlockDoor.POWERED, Boolean.valueOf((meta & 2) != 0));
    +            }
    +            return state.with(BlockDoor.HALF, DoubleBlockHalf.LOWER)
    +                    .with(BlockDoor.FACING, EnumFacing.byHorizontalIndex(meta & 3).rotateYCCW())
    +                    .with(BlockDoor.OPEN, Boolean.valueOf((meta & 4) != 0));
    +        }
    +        if (block instanceof BlockTrapDoor) {
    +            EnumFacing[] facing = { EnumFacing.NORTH, EnumFacing.SOUTH, EnumFacing.WEST, EnumFacing.EAST };
    +            return state.with(BlockHorizontal.HORIZONTAL_FACING, facing[meta & 3])
    +                    .with(BlockTrapDoor.OPEN, Boolean.valueOf((meta & 4) != 0))
    +                    .with(BlockTrapDoor.HALF, (meta & 8) == 0 ? Half.BOTTOM : Half.TOP)
    +                    .with(BlockTrapDoor.POWERED, Boolean.FALSE)
    +                    .with(BlockTrapDoor.WATERLOGGED, Boolean.FALSE);
    +        }
    +        if (block instanceof BlockButton) return attachedState(state, meta, BlockButton.POWERED);
    +        if (block instanceof BlockLever) return legacyLeverState(state, meta);
    +        if (block instanceof BlockPressurePlate) return state.with(BlockPressurePlate.POWERED,
    +                Boolean.valueOf(meta > 0));
    +        if (block instanceof BlockFlowingFluid) return state.with(BlockFlowingFluid.LEVEL,
    +                Integer.valueOf(Math.min(15, meta)));
    +        return state;
    +    }
    +
    +    private static IBlockState attachedState(IBlockState state, int meta,
    +            net.minecraft.state.BooleanProperty powered) {
    +        EnumFacing oldFacing;
    +        switch (meta & 7) {
    +            case 0: oldFacing = EnumFacing.DOWN; break;
    +            case 1: oldFacing = EnumFacing.EAST; break;
    +            case 2: oldFacing = EnumFacing.WEST; break;
    +            case 3: oldFacing = EnumFacing.SOUTH; break;
    +            case 4: oldFacing = EnumFacing.NORTH; break;
    +            default: oldFacing = EnumFacing.UP;
    +        }
    +        AttachFace face = oldFacing == EnumFacing.DOWN ? AttachFace.CEILING
    +                : oldFacing == EnumFacing.UP ? AttachFace.FLOOR : AttachFace.WALL;
    +        EnumFacing horizontal = oldFacing.getAxis().isHorizontal() ? oldFacing : EnumFacing.NORTH;
    +        return state.with(BlockHorizontalFace.FACE, face)
    +                .with(BlockHorizontal.HORIZONTAL_FACING, horizontal)
    +                .with(powered, Boolean.valueOf((meta & 8) != 0));
    +    }
    +
    +    private static IBlockState legacyLeverState(IBlockState state, int meta) {
    +        int orientation = meta & 7;
    +        AttachFace face;
    +        EnumFacing horizontal;
    +        if (orientation == 0 || orientation == 7) {
    +            face = AttachFace.CEILING;
    +            horizontal = orientation == 0 ? EnumFacing.EAST : EnumFacing.NORTH;
    +        } else if (orientation == 5 || orientation == 6) {
    +            face = AttachFace.FLOOR;
    +            horizontal = orientation == 6 ? EnumFacing.EAST : EnumFacing.NORTH;
    +        } else {
    +            face = AttachFace.WALL;
    +            horizontal = new EnumFacing[] { EnumFacing.NORTH, EnumFacing.EAST, EnumFacing.WEST,
    +                    EnumFacing.SOUTH, EnumFacing.NORTH }[orientation];
    +        }
    +        return state.with(BlockHorizontalFace.FACE, face)
    +                .with(BlockHorizontal.HORIZONTAL_FACING, horizontal)
    +                .with(BlockLever.POWERED, Boolean.valueOf((meta & 8) != 0));
    +    }
    +
    +    public static String normalizeLegacyRegistryName(String name) {
    +        if (name == null) return "";
    +        String value = name.trim().toLowerCase(Locale.ROOT);
    +        return value;
    +    }
    +
    +    private static void migrateLoosePlayerData(File levelDat, NBTTagCompound root) throws IOException {
    +        boolean changed = migrateLegacyItems(root) > 0;
    +        if (changed && levelDat.isFile()) writeWithBackup(levelDat, root);
    +        File playerData = new File(levelDat.getParentFile(), "playerdata");
    +        File[] files = playerData.listFiles((directory, name) -> name.endsWith(".dat"));
    +        if (files == null) return;
    +        Arrays.sort(files);
    +        for (File file : files) {
    +            try (FileInputStream input = new FileInputStream(file)) {
    +                NBTTagCompound player = CompressedStreamTools.readCompressed(input);
    +                if (migrateLegacyItems(player) > 0) writeWithBackup(file, player);
    +            }
    +        }
    +    }
    +
    +    private static int migrateLegacyItems(INBTBase value) {
    +        int changed = 0;
    +        if (value instanceof NBTTagCompound) {
    +            NBTTagCompound compound = (NBTTagCompound) value;
    +            String id = compound.getString("id");
    +            if ("forge:bucketfilled".equalsIgnoreCase(id) && compound.contains("tag", 10)) {
    +                NBTTagCompound tag = compound.getCompound("tag");
    +                String oldFluid = tag.getString("FluidName").toLowerCase(Locale.ROOT);
    +                String target = oldFluid.startsWith("basegems:") || oldFluid.startsWith("basegems.")
    +                        ? oldFluid.substring("basegems:".length()) : oldFluid;
    +                if (ModContent.fluid(target) != null) {
    +                    compound.setString("id", BaseGems.MOD_ID + ":" + target + "_bucket");
    +                    tag.removeTag("FluidName");
    +                    tag.removeTag("Amount");
    +                    if (tag.isEmpty()) compound.removeTag("tag");
    +                    changed++;
    +                }
    +            } else if (compound.contains("Count", 99)) {
    +                ResourceLocation oldId = safeId(id);
    +                if (oldId != null && BaseGems.MOD_ID.equals(oldId.getNamespace())) {
    +                    String path = oldId.getPath();
    +                    if (path.endsWith("_door_item")) path = path.substring(0, path.length() - 5);
    +                    ResourceLocation target = ModContent.id(path);
    +                    if (!target.toString().equals(id)) {
    +                        compound.setString("id", target.toString());
    +                        changed++;
    +                    }
    +                    if (compound.getInt("Damage") > 0 && ModContent.item(path) != null && ModContent.item(path).isDamageable()) {
    +                        NBTTagCompound tag = compound.contains("tag", 10)
    +                                ? compound.getCompound("tag") : new NBTTagCompound();
    +                        if (!tag.contains("Damage", 99)) {
    +                            tag.setInt("Damage", compound.getInt("Damage"));
    +                            compound.setTag("tag", tag);
    +                            changed++;
    +                        }
    +                    }
    +                }
    +            }
    +            for (String key : new ArrayList(compound.keySet())) {
    +                INBTBase child = compound.getTag(key);
    +                if (child != null) changed += migrateLegacyItems(child);
    +            }
    +        } else if (value instanceof NBTTagList) {
    +            NBTTagList list = (NBTTagList) value;
    +            for (int index = 0; index < list.size(); index++) changed += migrateLegacyItems(list.get(index));
    +        }
    +        return changed;
    +    }
    +
    +    private static ResourceLocation safeId(String value) {
    +        try { return value == null || value.isEmpty() ? null : new ResourceLocation(value.toLowerCase(Locale.ROOT)); }
    +        catch (RuntimeException ignored) { return null; }
    +    }
    +
    +    private static void writeWithBackup(File file, NBTTagCompound data) throws IOException {
    +        File backup = new File(file.getParentFile(), file.getName() + ".basegems-legacy-backup");
    +        if (file.exists() && !backup.exists()) Files.copy(file.toPath(), backup.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
    +        File temporary = File.createTempFile(file.getName(), ".basegems.tmp", file.getParentFile());
    +        try (FileOutputStream output = new FileOutputStream(temporary)) {
    +            CompressedStreamTools.writeCompressed(data, output);
    +        }
    +        try {
    +            Files.move(temporary.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING,
    +                    StandardCopyOption.ATOMIC_MOVE);
    +        } catch (java.nio.file.AtomicMoveNotSupportedException ignored) {
    +            Files.move(temporary.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
    +        }
    +    }
    +
    +    private static void captureLegacyChunks(File directory) {
    +        indexLegacyChunks(new File(directory, "region"), 0);
    +        File[] dimensions = directory.listFiles(file -> file.isDirectory() && file.getName().matches("DIM-?\\d+"));
    +        if (dimensions == null) return;
    +        for (File dimension : dimensions) {
    +            indexLegacyChunks(new File(dimension, "region"), Integer.parseInt(dimension.getName().substring(3)));
    +        }
    +    }
    +
    +    private static void indexLegacyChunks(File regionDirectory, int dimension) {
    +        File[] regions = regionDirectory.listFiles((directory, name) -> name.matches("r\\.-?\\d+\\.-?\\d+\\.mc[ar]"));
    +        if (regions == null) return;
    +        Set chunks = new HashSet<>();
    +        byte[] locations = new byte[4096];
    +        for (File region : regions) {
    +            String[] parts = region.getName().split("\\.");
    +            if (parts.length != 4) continue;
    +            try (InputStream input = Files.newInputStream(region.toPath())) {
    +                int read = 0;
    +                while (read < locations.length) {
    +                    int count = input.read(locations, read, locations.length - read);
    +                    if (count < 0) break;
    +                    read += count;
    +                }
    +                int regionX = Integer.parseInt(parts[1]);
    +                int regionZ = Integer.parseInt(parts[2]);
    +                for (int index = 0; index < read / 4; index++) {
    +                    int offset = index * 4;
    +                    if ((locations[offset] | locations[offset + 1] | locations[offset + 2] | locations[offset + 3]) != 0) {
    +                        chunks.add(Long.valueOf(chunkKey(
    +                                regionX * 32 + (index & 31), regionZ * 32 + (index >> 5))));
    +                    }
    +                }
    +            } catch (IOException | NumberFormatException exception) {
    +                BaseGems.LOGGER.warn("Could not inspect legacy chunk locations in '{}'", region, exception);
    +            }
    +        }
    +        LEGACY_GEMS_CHUNKS.put(dimension, Collections.unmodifiableSet(chunks));
    +    }
    +
    +    private static long chunkKey(int x, int z) {
    +        return ((long) x & 0xffffffffL) << 32 | ((long) z & 0xffffffffL);
    +    }
    +
    +    @SuppressWarnings("unchecked")
    +    private static Dynamic[] expandFlatteningTable(int requiredLength) {
    +        try {
    +            String fieldName = ObfuscationReflectionHelper.remapName(INameMappingService.Domain.FIELD, "field_199200_b");
    +            Field valuesField = BlockStateFlatteningMap.class.getDeclaredField(fieldName);
    +            valuesField.setAccessible(true);
    +            Field modifiersField = Field.class.getDeclaredField("modifiers");
    +            modifiersField.setAccessible(true);
    +            modifiersField.setInt(valuesField, valuesField.getModifiers() & ~Modifier.FINAL);
    +            Dynamic[] current = (Dynamic[]) valuesField.get(null);
    +            if (current.length >= requiredLength) return current;
    +            Dynamic[] expanded = Arrays.copyOf(current, requiredLength);
    +            valuesField.set(null, expanded);
    +            return expanded;
    +        } catch (ReflectiveOperationException exception) {
    +            throw new IllegalStateException("Could not expand Minecraft's legacy block-state flattening table", exception);
    +        }
    +    }
    +
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/proxy/ClientProxy.java b/src/main/java/zone/moddev/mc/basegems/proxy/ClientProxy.java
    deleted file mode 100644
    index cd71612..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/proxy/ClientProxy.java
    +++ /dev/null
    @@ -1,46 +0,0 @@
    -package zone.moddev.mc.basegems.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.basegems.BaseGems;
    -import zone.moddev.mc.basegems.client.BaseGemsConfigScreen;
    -import zone.moddev.mc.basegems.init.Blocks;
    -import zone.moddev.mc.basegems.init.Fluids;
    -import zone.moddev.mc.basegems.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(BaseGems.MODID, BaseGemsConfigScreen::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 && BaseGems.MODID.equals(name.getNamespace());
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/proxy/CommonProxy.java b/src/main/java/zone/moddev/mc/basegems/proxy/CommonProxy.java
    deleted file mode 100644
    index 6924e3a..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/proxy/CommonProxy.java
    +++ /dev/null
    @@ -1,23 +0,0 @@
    -package zone.moddev.mc.basegems.proxy;
    -
    -import zone.moddev.mc.basegems.init.ItemGroups;
    -import zone.moddev.mc.basegems.init.VillagerTrades;
    -import zone.moddev.mc.basegems.util.Config;
    -import zone.moddev.mc.basegems.worldgen.GemWorldgen;
    -import net.minecraftforge.fml.common.event.FMLInitializationEvent;
    -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
    -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
    -
    -public class CommonProxy {
    -    public void preInit(FMLPreInitializationEvent event) { }
    -
    -    public void init(FMLInitializationEvent event) {
    -        ItemGroups.setupIcons();
    -        VillagerTrades.init();
    -        GemWorldgen.enqueue();
    -    }
    -
    -    public void postInit(FMLPostInitializationEvent event) {
    -        Config.postInit();
    -    }
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/proxy/ServerProxy.java b/src/main/java/zone/moddev/mc/basegems/proxy/ServerProxy.java
    deleted file mode 100644
    index da82025..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/proxy/ServerProxy.java
    +++ /dev/null
    @@ -1,10 +0,0 @@
    -package zone.moddev.mc.basegems.proxy;
    -
    -/**
    - * Base Gems Server Proxy
    - *
    - * @author Jasmine Iwanek
    - *
    - */
    -public class ServerProxy extends CommonProxy {
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/recipe/ContentCraftingRecipe.java b/src/main/java/zone/moddev/mc/basegems/recipe/ContentCraftingRecipe.java
    new file mode 100644
    index 0000000..ea8c517
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/recipe/ContentCraftingRecipe.java
    @@ -0,0 +1,127 @@
    +package zone.moddev.mc.basegems.recipe;
    +
    +import com.google.gson.JsonObject;
    +import com.google.gson.JsonSyntaxException;
    +import net.minecraft.inventory.IInventory;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.crafting.IRecipe;
    +import net.minecraft.item.crafting.IRecipeSerializer;
    +import net.minecraft.item.crafting.RecipeSerializers;
    +import net.minecraft.item.crafting.ShapedRecipe;
    +import net.minecraft.item.crafting.ShapelessRecipe;
    +import net.minecraft.network.PacketBuffer;
    +import net.minecraft.util.JsonUtils;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.world.World;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +
    +/** Keeps recipe IDs stable between modes, including IDs stored in an old recipe book. */
    +public final class ContentCraftingRecipe {
    +    public static final IRecipeSerializer SERIALIZER = new Serializer();
    +    private ContentCraftingRecipe() {}
    +
    +    private static boolean allowed(IRecipe recipe) {
    +        return GemConfig.allows(recipe.getRecipeOutput().getItem().getRegistryName().getPath());
    +    }
    +
    +    private static IRecipe wrap(IRecipe recipe) {
    +        // Recipe-book placement needs the shaped recipe's width and height, not just its ingredients.
    +        if (recipe instanceof ShapedRecipe) return new Shaped((ShapedRecipe) recipe);
    +        if (recipe instanceof ShapelessRecipe) return new Shapeless((ShapelessRecipe) recipe);
    +        throw new IllegalArgumentException("Not an ordinary crafting recipe: " + recipe.getId());
    +    }
    +
    +    private static final class Shaped extends ShapedRecipe {
    +        private final ShapedRecipe plain;
    +
    +        private Shaped(ShapedRecipe plain) {
    +            super(plain.getId(), plain.getGroup(), plain.getWidth(), plain.getHeight(),
    +                    plain.getIngredients(), plain.getRecipeOutput());
    +            this.plain = plain;
    +        }
    +
    +        @Override
    +        public boolean matches(IInventory inventory, World world) {
    +            return allowed(this) && super.matches(inventory, world);
    +        }
    +
    +        @Override
    +        public ItemStack getCraftingResult(IInventory inventory) {
    +            return allowed(this) ? super.getCraftingResult(inventory) : ItemStack.EMPTY;
    +        }
    +
    +        @Override
    +        public boolean isDynamic() {
    +            return !allowed(this);
    +        }
    +
    +        @Override
    +        public IRecipeSerializer getSerializer() {
    +            return SERIALIZER;
    +        }
    +    }
    +
    +    private static final class Shapeless extends ShapelessRecipe {
    +        private final ShapelessRecipe plain;
    +
    +        private Shapeless(ShapelessRecipe plain) {
    +            super(plain.getId(), plain.getGroup(), plain.getRecipeOutput(), plain.getIngredients());
    +            this.plain = plain;
    +        }
    +
    +        @Override
    +        public boolean matches(IInventory inventory, World world) {
    +            return allowed(this) && super.matches(inventory, world);
    +        }
    +
    +        @Override
    +        public ItemStack getCraftingResult(IInventory inventory) {
    +            return allowed(this) ? super.getCraftingResult(inventory) : ItemStack.EMPTY;
    +        }
    +
    +        @Override
    +        public boolean isDynamic() {
    +            return !allowed(this);
    +        }
    +
    +        @Override
    +        public IRecipeSerializer getSerializer() {
    +            return SERIALIZER;
    +        }
    +    }
    +
    +    private static final class Serializer implements IRecipeSerializer {
    +        private static final ResourceLocation NAME = new ResourceLocation("basegems", "content_crafting");
    +
    +        @Override
    +        public IRecipe read(ResourceLocation id, JsonObject json) {
    +            JsonObject nested = JsonUtils.getJsonObject(json, "recipe");
    +            String type = JsonUtils.getString(nested, "type");
    +            if (!"minecraft:crafting_shaped".equals(type) && !"minecraft:crafting_shapeless".equals(type)) {
    +                throw new JsonSyntaxException("Content-mode crafting requires an ordinary shaped or shapeless recipe");
    +            }
    +
    +            return wrap(RecipeSerializers.deserialize(id, nested));
    +        }
    +
    +        @Override
    +        public IRecipe read(ResourceLocation id, PacketBuffer buffer) {
    +            IRecipe nested = RecipeSerializers.read(buffer);
    +            if (!nested.getId().equals(id) || nested.getSerializer() == SERIALIZER) {
    +                throw new IllegalArgumentException("Invalid nested content-mode recipe " + id);
    +            }
    +            return wrap(nested);
    +        }
    +
    +        @Override
    +        public void write(PacketBuffer buffer, IRecipe value) {
    +            IRecipe plain = value instanceof Shaped ? ((Shaped) value).plain : ((Shapeless) value).plain;
    +            RecipeSerializers.write(plain, buffer);
    +        }
    +
    +        @Override
    +        public ResourceLocation getName() {
    +            return NAME;
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/recipe/CrushingRecipe.java b/src/main/java/zone/moddev/mc/basegems/recipe/CrushingRecipe.java
    new file mode 100644
    index 0000000..80fa841
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/recipe/CrushingRecipe.java
    @@ -0,0 +1,115 @@
    +package zone.moddev.mc.basegems.recipe;
    +
    +import javax.annotation.Nullable;
    +
    +import com.google.gson.JsonObject;
    +
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +
    +import net.minecraft.inventory.IInventory;
    +import net.minecraft.inventory.InventoryBasic;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.crafting.IRecipe;
    +import net.minecraft.item.crafting.IRecipeSerializer;
    +import net.minecraft.item.crafting.Ingredient;
    +import net.minecraft.item.crafting.RecipeSerializers;
    +import net.minecraft.network.PacketBuffer;
    +import net.minecraft.util.NonNullList;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.util.text.TextComponentString;
    +import net.minecraft.world.World;
    +import net.minecraftforge.common.crafting.RecipeType;
    +import net.minecraftforge.common.crafting.CraftingHelper;
    +import net.minecraftforge.fml.ModList;
    +
    +public final class CrushingRecipe implements IRecipe {
    +    public static final RecipeType TYPE = RecipeType.get(
    +            new ResourceLocation(BaseGems.MOD_ID, "crushing"), CrushingRecipe.class);
    +    private static final ResourceLocation BASE_METALS_TYPE = new ResourceLocation("basemetals", "crushing");
    +    private static final ResourceLocation MODERN_METALS_TYPE = new ResourceLocation("modernmetals", "crushing");
    +    public static final IRecipeSerializer SERIALIZER = new Serializer();
    +    public static final IRecipeSerializer PLATE_REPAIR_SERIALIZER =
    +            new PlateRepairRecipe.Serializer();
    +    private static boolean registered;
    +
    +    private final ResourceLocation id;
    +    private final Ingredient ingredient;
    +    private final ItemStack result;
    +
    +    public CrushingRecipe(ResourceLocation id, Ingredient ingredient, ItemStack result) {
    +        this.id = id;
    +        this.ingredient = ingredient;
    +        this.result = result.copy();
    +    }
    +
    +    public static synchronized void register() {
    +        if (registered) return;
    +        RecipeSerializers.register(SERIALIZER);
    +        RecipeSerializers.register(PLATE_REPAIR_SERIALIZER);
    +        RecipeSerializers.register(ContentCraftingRecipe.SERIALIZER);
    +        RecipeSerializers.register(GemSmeltingRecipe.SERIALIZER);
    +        registered = true;
    +    }
    +
    +    @Nullable
    +    public static IRecipe find(World world, ItemStack input) {
    +        InventoryBasic inventory = new InventoryBasic(new TextComponentString("crushing"), 1);
    +        inventory.setInventorySlotContents(0, input);
    +        for (IRecipe recipe : world.getRecipeManager().getRecipes()) {
    +            ResourceLocation type = recipe.getType().getId();
    +            if ((recipe instanceof CrushingRecipe || BASE_METALS_TYPE.equals(type) || MODERN_METALS_TYPE.equals(type))
    +                    && recipe.matches(inventory, world)) {
    +                return recipe;
    +            }
    +        }
    +        return null;
    +    }
    +
    +    @Override public boolean matches(IInventory inventory, World world) {
    +        return GemConfig.allows(result.getItem().getRegistryName().getPath())
    +                && inventory.getSizeInventory() > 0 && ingredient.test(inventory.getStackInSlot(0));
    +    }
    +    @Override public ItemStack getCraftingResult(IInventory inventory) { return result.copy(); }
    +    @Override public boolean canFit(int width, int height) { return width * height >= 1; }
    +    @Override public ItemStack getRecipeOutput() { return result.copy(); }
    +    @Override public ResourceLocation getId() { return id; }
    +    @Override public IRecipeSerializer getSerializer() { return SERIALIZER; }
    +    @Override public RecipeType getType() { return TYPE; }
    +    @Override public NonNullList getIngredients() {
    +        NonNullList ingredients = NonNullList.create();
    +        ingredients.add(ingredient);
    +        return ingredients;
    +    }
    +    @Override public boolean isDynamic() { return true; }
    +
    +    public static final class Serializer implements IRecipeSerializer {
    +        private Serializer() {}
    +        @Override public IRecipe read(ResourceLocation id, JsonObject json) {
    +            ItemStack result = CraftingHelper.getItemStack(json.getAsJsonObject("result"), true);
    +            // Keep disabled gems in the local type, which still rejects them in matches().
    +            if (GemConfig.allows(result.getItem().getRegistryName().getPath())) {
    +                ModList mods = ModList.get();
    +                ResourceLocation host = mods.isLoaded("basemetals") ? BASE_METALS_TYPE
    +                        : mods.isLoaded("modernmetals") ? MODERN_METALS_TYPE : null;
    +                if (host != null) {
    +                    JsonObject shared = new JsonObject();
    +                    shared.addProperty("type", host.toString());
    +                    shared.add("ingredient", json.get("ingredient"));
    +                    shared.add("result", json.get("result"));
    +                    return RecipeSerializers.deserialize(id, shared);
    +                }
    +            }
    +            return new CrushingRecipe(id, Ingredient.fromJson(json.get("ingredient")), result);
    +        }
    +        @Override public CrushingRecipe read(ResourceLocation id, PacketBuffer buffer) {
    +            return new CrushingRecipe(id, Ingredient.fromBuffer(buffer), buffer.readItemStack());
    +        }
    +        @Override public void write(PacketBuffer buffer, IRecipe value) {
    +            CrushingRecipe recipe = (CrushingRecipe) value;
    +            recipe.ingredient.writeToBuffer(buffer);
    +            buffer.writeItemStack(recipe.result);
    +        }
    +        @Override public ResourceLocation getName() { return TYPE.getId(); }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/recipe/GemSmeltingRecipe.java b/src/main/java/zone/moddev/mc/basegems/recipe/GemSmeltingRecipe.java
    new file mode 100644
    index 0000000..ec314d7
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/recipe/GemSmeltingRecipe.java
    @@ -0,0 +1,62 @@
    +package zone.moddev.mc.basegems.recipe;
    +
    +import com.google.gson.JsonObject;
    +import net.minecraft.inventory.IInventory;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.crafting.FurnaceRecipe;
    +import net.minecraft.item.crafting.IRecipeSerializer;
    +import net.minecraft.item.crafting.Ingredient;
    +import net.minecraft.network.PacketBuffer;
    +import net.minecraft.util.JsonUtils;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.world.World;
    +import net.minecraftforge.common.crafting.CraftingHelper;
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +
    +/** Furnace recipes also honour the gem switches. */
    +public final class GemSmeltingRecipe extends FurnaceRecipe {
    +    public static final IRecipeSerializer SERIALIZER = new Serializer();
    +    private static final int COOKING_TIME = 200;
    +
    +    private GemSmeltingRecipe(ResourceLocation id, Ingredient ingredient, ItemStack result, float experience) {
    +        super(id, "", ingredient, result, experience, COOKING_TIME);
    +    }
    +
    +    private boolean allowed() {
    +        return GemConfig.allows(getRecipeOutput().getItem().getRegistryName().getPath());
    +    }
    +
    +    @Override public boolean matches(IInventory inventory, World world) {
    +        return allowed() && super.matches(inventory, world);
    +    }
    +
    +    @Override public ItemStack getCraftingResult(IInventory inventory) {
    +        return allowed() ? super.getCraftingResult(inventory) : ItemStack.EMPTY;
    +    }
    +
    +    @Override public boolean isDynamic() { return !allowed(); }
    +    @Override public IRecipeSerializer getSerializer() { return SERIALIZER; }
    +
    +    private static final class Serializer implements IRecipeSerializer {
    +        @Override public GemSmeltingRecipe read(ResourceLocation id, JsonObject json) {
    +            return new GemSmeltingRecipe(id, Ingredient.fromJson(json.get("ingredient")),
    +                    CraftingHelper.getItemStack(JsonUtils.getJsonObject(json, "result"), true),
    +                    JsonUtils.getFloat(json, "experience", 0.0F));
    +        }
    +
    +        @Override public GemSmeltingRecipe read(ResourceLocation id, PacketBuffer buffer) {
    +            return new GemSmeltingRecipe(id, Ingredient.fromBuffer(buffer), buffer.readItemStack(), buffer.readFloat());
    +        }
    +
    +        @Override public void write(PacketBuffer buffer, GemSmeltingRecipe recipe) {
    +            recipe.getIngredients().get(0).writeToBuffer(buffer);
    +            buffer.writeItemStack(recipe.getRecipeOutput());
    +            buffer.writeFloat(recipe.getExperience());
    +        }
    +
    +        @Override public ResourceLocation getName() {
    +            return new ResourceLocation(BaseGems.MOD_ID, "smelting");
    +        }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/recipe/PlateRepairRecipe.java b/src/main/java/zone/moddev/mc/basegems/recipe/PlateRepairRecipe.java
    new file mode 100644
    index 0000000..29f1781
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/recipe/PlateRepairRecipe.java
    @@ -0,0 +1,123 @@
    +package zone.moddev.mc.basegems.recipe;
    +
    +import com.google.gson.JsonObject;
    +import com.google.gson.JsonSyntaxException;
    +
    +import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +
    +import net.minecraft.init.Items;
    +import net.minecraft.inventory.IInventory;
    +import net.minecraft.item.Item;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.item.crafting.IRecipeHidden;
    +import net.minecraft.item.crafting.IRecipeSerializer;
    +import net.minecraft.item.crafting.Ingredient;
    +import net.minecraft.network.PacketBuffer;
    +import net.minecraft.util.JsonUtils;
    +import net.minecraft.util.NonNullList;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.world.World;
    +import net.minecraftforge.registries.ForgeRegistries;
    +
    +public final class PlateRepairRecipe extends IRecipeHidden {
    +    private final Item target;
    +    private final Ingredient plate;
    +
    +    public PlateRepairRecipe(ResourceLocation id, Item target, Ingredient plate) {
    +        super(id);
    +        this.target = target;
    +        this.plate = plate;
    +    }
    +
    +    @Override
    +    public boolean matches(IInventory inventory, World world) {
    +        if (!GemConfig.allows(target.getRegistryName().getPath())) return false;
    +        ItemStack foundTarget = ItemStack.EMPTY;
    +        boolean foundPlate = false;
    +
    +        for (int slot = 0; slot < inventory.getSizeInventory(); slot++) {
    +            ItemStack stack = inventory.getStackInSlot(slot);
    +
    +            if (stack.isEmpty()) {
    +                continue;
    +            }
    +
    +            if (stack.getItem() == target && stack.isDamaged() && foundTarget.isEmpty()) {
    +                foundTarget = stack;
    +            } else if (plate.test(stack) && !foundPlate) {
    +                foundPlate = true;
    +            } else {
    +                return false;
    +            }
    +        }
    +
    +        return !foundTarget.isEmpty() && foundPlate;
    +    }
    +
    +    @Override
    +    public ItemStack getCraftingResult(IInventory inventory) {
    +        if (!matches(inventory, null)) {
    +            return ItemStack.EMPTY;
    +        }
    +
    +        for (int slot = 0; slot < inventory.getSizeInventory(); slot++) {
    +            ItemStack stack = inventory.getStackInSlot(slot);
    +
    +            if (stack.getItem() == target && stack.isDamaged()) {
    +                ItemStack repaired = stack.copy();
    +                repaired.setCount(1);
    +                repaired.setDamage(0);
    +
    +                return repaired;
    +            }
    +        }
    +
    +        return ItemStack.EMPTY;
    +    }
    +    @Override
    +    public boolean canFit(int width, int height) {
    +        return width * height >= 2;
    +    }
    +
    +    @Override
    +    public ItemStack getRecipeOutput() {
    +        return new ItemStack(target);
    +    }
    +
    +    @Override public boolean isDynamic() { return !GemConfig.allows(target.getRegistryName().getPath()); }
    +
    +    @Override
    +    public NonNullList getIngredients() {
    +        NonNullList ingredients = NonNullList.create();
    +        ingredients.add(Ingredient.fromItems(target));
    +        ingredients.add(plate);
    +
    +        return ingredients;
    +    }
    +
    +    @Override
    +    public IRecipeSerializer getSerializer() {
    +        return CrushingRecipe.PLATE_REPAIR_SERIALIZER;
    +    }
    +
    +    public static final class Serializer implements IRecipeSerializer {
    +        private final ResourceLocation name = new ResourceLocation(BaseGems.MOD_ID, "plate_repair");
    +        @Override public PlateRepairRecipe read(ResourceLocation id, JsonObject json) {
    +            ResourceLocation targetId = new ResourceLocation(JsonUtils.getString(json, "target"));
    +            Item target = ForgeRegistries.ITEMS.getValue(targetId);
    +            if (target == null || target == Items.AIR) throw new JsonSyntaxException("Unknown target " + targetId);
    +            return new PlateRepairRecipe(id, target, Ingredient.fromJson(JsonUtils.getJsonObject(json, "plate")));
    +        }
    +        @Override public PlateRepairRecipe read(ResourceLocation id, PacketBuffer buffer) {
    +            Item target = ForgeRegistries.ITEMS.getValue(buffer.readResourceLocation());
    +            if (target == null || target == Items.AIR) throw new IllegalStateException("Missing target");
    +            return new PlateRepairRecipe(id, target, Ingredient.fromBuffer(buffer));
    +        }
    +        @Override public void write(PacketBuffer buffer, PlateRepairRecipe recipe) {
    +            buffer.writeResourceLocation(recipe.target.getRegistryName());
    +            recipe.plate.writeToBuffer(buffer);
    +        }
    +        @Override public ResourceLocation getName() { return name; }
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/trade/GemTrades.java b/src/main/java/zone/moddev/mc/basegems/trade/GemTrades.java
    new file mode 100644
    index 0000000..229dbae
    --- /dev/null
    +++ b/src/main/java/zone/moddev/mc/basegems/trade/GemTrades.java
    @@ -0,0 +1,62 @@
    +package zone.moddev.mc.basegems.trade;
    +
    +import java.util.ArrayList;
    +import java.util.EnumMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +import net.minecraft.init.Items;
    +import net.minecraft.item.ItemStack;
    +import net.minecraft.util.ResourceLocation;
    +import net.minecraft.village.MerchantRecipe;
    +import net.minecraftforge.fml.common.registry.VillagerRegistry.VillagerCareer;
    +import net.minecraftforge.fml.common.registry.VillagerRegistry.VillagerProfession;
    +import net.minecraftforge.registries.ForgeRegistries;
    +import zone.moddev.mc.basegems.config.GemConfig;
    +import zone.moddev.mc.basegems.content.ModContent;
    +import zone.moddev.mc.basegems.data.Gem;
    +import zone.moddev.mc.basegems.data.GemRarity;
    +
    +public final class GemTrades {
    +    private static final String[] TOOLS = {"crackhammer", "axe", "hoe", "shovel", "pickaxe"};
    +
    +    private GemTrades() {}
    +
    +    public static void register() {
    +        VillagerProfession smith = ForgeRegistries.VILLAGER_PROFESSIONS.getValue(new ResourceLocation("minecraft", "smith"));
    +        if (smith == null) return;
    +        Map> tiers = new EnumMap<>(GemRarity.class);
    +        for (Gem gem : Gem.ALL) tiers.computeIfAbsent(GemRarity.of(gem.name()), key -> new ArrayList<>()).add(gem);
    +        for (int careerId : new int[]{1, 2, 3}) {
    +            VillagerCareer career = smith.getCareer(careerId);
    +            tiers.forEach((rarity, gems) -> career.addTrade(rarity.tradeLevel, (merchant, offers, random) -> {
    +                List available = new ArrayList<>();
    +                for (Gem gem : gems) if (GemConfig.enabled(gem.name())) available.add(gem);
    +                if (available.isEmpty()) return;
    +                String name = available.get(random.nextInt(available.size())).name();
    +                ItemStack bundle = new ItemStack(ModContent.item(name + "_ingot"), rarity.gemsPerTrade);
    +                offers.add(new MerchantRecipe(bundle, ItemStack.EMPTY, new ItemStack(Items.EMERALD), 0, 6));
    +                offers.add(new MerchantRecipe(new ItemStack(Items.EMERALD, rarity.emeraldsToBuy),
    +                        ItemStack.EMPTY, bundle.copy(), 0, 6));
    +            }));
    +        }
    +        VillagerCareer weapons = smith.getCareer(2);
    +        VillagerCareer tools = smith.getCareer(3);
    +        for (Gem gem : Gem.ALL) {
    +            float value = gem.equipmentTradeValue();
    +            int level = Math.max(1, Math.min(4, (int) (value * 0.1F)));
    +            int price = Math.max(1, (int) (value * 0.2F));
    +            for (String tool : TOOLS) addEquipment(tools, level, price, gem.name() + "_" + tool);
    +            addEquipment(weapons, level, price + (int) (gem.baseAttackDamage() / 2) - 1, gem.name() + "_sword");
    +        }
    +    }
    +
    +    private static void addEquipment(VillagerCareer career, int level, int price, String path) {
    +        career.addTrade(level, (merchant, offers, random) -> {
    +            if (!GemConfig.allows(path)) return;
    +            int markup = price > 1 ? random.nextInt(Math.max(2, price / 4) + 1) : 0;
    +            offers.add(new MerchantRecipe(new ItemStack(Items.EMERALD, Math.max(1, price + markup)),
    +                    new ItemStack(ModContent.item(path))));
    +        });
    +    }
    +}
    diff --git a/src/main/java/zone/moddev/mc/basegems/util/Config.java b/src/main/java/zone/moddev/mc/basegems/util/Config.java
    deleted file mode 100644
    index 56035cf..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/util/Config.java
    +++ /dev/null
    @@ -1,146 +0,0 @@
    -package zone.moddev.mc.basegems.util;
    -
    -import java.io.File;
    -import java.util.Locale;
    -
    -import com.mcmoddev.lib.registry.CrusherRecipeRegistry;
    -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.basegems.BaseGems;
    -
    -/**
    - * @author Jasmine Iwanek
    - *
    - */
    -public class Config {
    -
    -	private static Configuration configuration;
    -	private static final String CONFIG_FILE = "config/BaseGems.cfg";
    -	private static final String HIGH_FANTASY = "high_fantasy";
    -	private static final String LOW_FANTASY = "low_fantasy";
    -	public static final String GENERAL_CAT = "General";
    -	public static final String MATERIALS_CAT = "Gems";
    -	private static boolean lowFantasy;
    -
    -	@SubscribeEvent
    -	public void onConfigChange(ConfigChangedEvent.OnConfigChangedEvent e) {
    -		if (e.getModID().equals(BaseGems.MODID) && configuration.hasChanged()) {
    -			configuration.save();
    -		}
    -	}
    -
    -	public static void init() {
    -		if (configuration == null) {
    -			configuration = new Configuration(new File(CONFIG_FILE));
    -			MinecraftForge.EVENT_BUS.register(new Config());
    -			Property mode = configuration.get(GENERAL_CAT, "contentMode", HIGH_FANTASY,
    -					"Fantasy level for Base Gems crafting. Requires a game restart.");
    -			mode.setValidValues(new String[] { HIGH_FANTASY, LOW_FANTASY });
    -			mode.setLanguageKey("config.basegems.content_mode");
    -			mode.setRequiresMcRestart(true);
    -			lowFantasy = LOW_FANTASY.equals(mode.getString().trim().toLowerCase(Locale.ROOT));
    -			mode.set(lowFantasy ? LOW_FANTASY : HIGH_FANTASY);
    -		}
    -
    -		// GEMS
    -		Options.enableAgate = gemEnabled("EnableAgate", "Enable Agate Items and Materials");
    -		Options.enableAlexandrite = gemEnabled("EnableAlexandrite", "Enable Alexandrite Items and Materials");
    -		Options.enableAmber = gemEnabled("EnableAmber", "Enable Amber Items and Materials");
    -		Options.enableAmethyst = gemEnabled("EnableAmethyst", "Enable Amethyst Items and Materials");
    -		Options.enableAmetrine = gemEnabled("EnableAmetrine", "Enable Ametrine Items and Materials");
    -		Options.enableAquamarine = gemEnabled("EnableAquamarine", "Enable Aquamarine Items and Materials");
    -		Options.enableBeryl = gemEnabled("EnableBeryl", "Enable Beryl Items and Materials");
    -		Options.enableBlackDiamond = gemEnabled("EnableBlackDiamond", "Enable Black Diamond Items and Materials");
    -		Options.enableBlueTopaz = gemEnabled("EnableBlueTopaz", "Enable Blue Topaz Items and Materials");
    -		Options.enableCarnelian = gemEnabled("EnableCarnelian", "Enable Carnelian Items and Materials");
    -		Options.enableCitrine = gemEnabled("EnableCitrine", "Enable Citrine Items and Materials");
    -		Options.enableGarnet = gemEnabled("EnableGarnet", "Enable Garnet Items and Materials");
    -		Options.enableGoldenBeryl = gemEnabled("EnableGoldenBeryl", "Enable Golden Beryl Items and Materials");
    -		Options.enableHeliodor = gemEnabled("EnableHeliodor", "Enable Heliodor Items and Materials");
    -		Options.enableIndicolite = gemEnabled("EnableIndicolite", "Enable Indicolite Items and Materials");
    -		Options.enableIolite = gemEnabled("EnableIolite", "Enable Iolite Items and Materials");
    -		Options.enableJade = gemEnabled("EnableJade", "Enable Jade Items and Materials");
    -		Options.enableJasper = gemEnabled("EnableJasper", "Enable Jasper Items and Materials");
    -		Options.enableLepidolite = gemEnabled("EnableLepidolite", "Enable Lepidolite Items and Materials");
    -		Options.enableMalachite = gemEnabled("EnableMalachite", "Enable Malachite Items and Materials");
    -		Options.enableMoldavite = gemEnabled("EnableMoldovite", "Enable Moldavite Items and Materials");
    -		Options.enableMoonstone = gemEnabled("EnableMoonstone", "Enable Moonstone Items and Materials");
    -		Options.enableMorganite = gemEnabled("EnableMorganite", "Enable Morganite Items and Materials");
    -		Options.enableOnyx = gemEnabled("EnableOnyx", "Enable Onyx Items and Materials");
    -		Options.enableOpal = gemEnabled("EnableOpal", "Enable Opal Items and Materials");
    -		Options.enablePeridot = gemEnabled("EnablePeridot", "Enable Peridot Items and Materials");
    -		Options.enableRuby = gemEnabled("EnableRuby", "Enable Ruby Items and Materials");
    -		Options.enableSapphire = gemEnabled("EnableSapphire", "Enable Sapphire Items and Materials");
    -		Options.enableSpinel = gemEnabled("EnableSpinel", "Enable Spinel Items and Materials");
    -		Options.enableTanzanite = gemEnabled("EnableTanzanite", "Enable Tanzanite Items and Materials");
    -		Options.enableTopaz = gemEnabled("EnableTopaz", "Enable Topaz Items and Materials");
    -		Options.enableTurquoise = gemEnabled("EnableTurquoise", "Enable Turquoise Items and Materials");
    -		Options.enableVioletSapphire = gemEnabled("EnableVioletSapphire", "Enable Violet Sapphire Items and Materials");
    -
    -		if (configuration.hasChanged()) {
    -			configuration.save();
    -		}
    -
    -	}
    -
    -	private static boolean gemEnabled(final String key, final String comment) {
    -		return configuration.getBoolean(key, MATERIALS_CAT, true, comment, "config.basegems." + key);
    -	}
    -
    -	public static Configuration getConfiguration() {
    -		return configuration;
    -	}
    -
    -	public static boolean lowFantasy() {
    -		return lowFantasy;
    -	}
    -
    -	public static class Options {
    -
    -		// MATERIALS
    -		public static boolean enableAgate = true;
    -		public static boolean enableAlexandrite = true;
    -		public static boolean enableAmber = true;
    -		public static boolean enableAmethyst = true;
    -		public static boolean enableAmetrine = true;
    -		public static boolean enableAquamarine = true;
    -		public static boolean enableBeryl = true;
    -		public static boolean enableBlackDiamond = true;
    -		public static boolean enableBlueTopaz = true;
    -		public static boolean enableCarnelian = true;
    -		public static boolean enableCitrine = true;
    -		public static boolean enableGarnet = true;
    -		public static boolean enableGoldenBeryl = true;
    -		public static boolean enableHeliodor = true;
    -		public static boolean enableIndicolite = true;
    -		public static boolean enableIolite = true;
    -		public static boolean enableJade = true;
    -		public static boolean enableJasper = true;
    -		public static boolean enableLepidolite = true;
    -		public static boolean enableMalachite = true;
    -		public static boolean enableMoldavite = true;
    -		public static boolean enableMoonstone = true;
    -		public static boolean enableMorganite = true;
    -		public static boolean enableOnyx = true;
    -		public static boolean enableOpal = true;
    -		public static boolean enablePeridot = true;
    -		public static boolean enableRuby = true;
    -		public static boolean enableSapphire = true;
    -		public static boolean enableSpinel = true;
    -		public static boolean enableTanzanite = true;
    -		public static boolean enableTopaz = true;
    -		public static boolean enableTurquoise = true;
    -		public static boolean enableVioletSapphire = true;
    -
    -		private Options() {
    -			throw new IllegalAccessError("Not a instantiable class");
    -		}
    -	}
    -
    -	public static void postInit() {
    -		CrusherRecipeRegistry.getInstance().clearCache();
    -	}
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/util/ContentPolicy.java b/src/main/java/zone/moddev/mc/basegems/util/ContentPolicy.java
    deleted file mode 100644
    index d2a70db..0000000
    --- a/src/main/java/zone/moddev/mc/basegems/util/ContentPolicy.java
    +++ /dev/null
    @@ -1,33 +0,0 @@
    -package zone.moddev.mc.basegems.util;
    -
    -import net.minecraft.item.ItemStack;
    -import net.minecraft.util.ResourceLocation;
    -import zone.moddev.mc.basegems.BaseGems;
    -
    -/** Keeps gem processing and storage recipes available in Low Fantasy. */
    -public final class ContentPolicy {
    -    private ContentPolicy() {}
    -
    -    public static boolean allows(ItemStack output) {
    -        if (output == null || output.isEmpty()) return true;
    -        ResourceLocation id = output.getItem().getRegistryName();
    -        if (id == null || !BaseGems.MODID.equals(id.getNamespace())) return true;
    -
    -        String path = id.getPath();
    -        int separator = path.indexOf('_');
    -        if (separator < 0) return true; // Molten gem blocks use the bare material name.
    -        switch (path.substring(separator + 1)) {
    -            case "ore":
    -            case "ingot":
    -            case "nugget":
    -            case "powder":
    -            case "smallpowder":
    -            case "plate":
    -            case "block":
    -                return true;
    -            default:
    -                return false;
    -        }
    -    }
    -
    -}
    diff --git a/src/main/java/zone/moddev/mc/basegems/worldgen/GemWorldgen.java b/src/main/java/zone/moddev/mc/basegems/worldgen/GemWorldgen.java
    index 504d380..967679f 100644
    --- a/src/main/java/zone/moddev/mc/basegems/worldgen/GemWorldgen.java
    +++ b/src/main/java/zone/moddev/mc/basegems/worldgen/GemWorldgen.java
    @@ -1,8 +1,9 @@
     package zone.moddev.mc.basegems.worldgen;
     
     import net.minecraft.util.ResourceLocation;
    -import net.minecraftforge.fml.common.registry.ForgeRegistries;
    +import net.minecraftforge.registries.ForgeRegistries;
     import zone.moddev.mc.basegems.BaseGems;
    +import zone.moddev.mc.basegems.config.GemConfig;
     import zone.moddev.mc.orespawn.api.GeologyFamily;
     import zone.moddev.mc.orespawn.api.OreHeightDistribution;
     import zone.moddev.mc.orespawn.api.OrePattern;
    @@ -62,15 +63,14 @@ public final class GemWorldgen {
         private GemWorldgen() { }
     
         public static void enqueue() {
    -        WorldgenProvider.Builder provider = WorldgenProvider.builder(BaseGems.MODID, 1);
    +        WorldgenProvider.Builder provider = WorldgenProvider.builder(BaseGems.MOD_ID, 1);
             int count = 0;
             for (Gem gem : GEMS) {
    -            ResourceLocation block = new ResourceLocation(BaseGems.MODID, gem.name + "_ore");
    -            // The legacy material toggles and Base Metals' ore switch can omit the block.
    -            if (!ForgeRegistries.BLOCKS.containsKey(block)) {
    +            ResourceLocation block = new ResourceLocation(BaseGems.MOD_ID, gem.name + "_ore");
    +            if (!GemConfig.enabled(gem.name) || !ForgeRegistries.BLOCKS.containsKey(block)) {
                     continue;
                 }
    -            ResourceLocation rule = new ResourceLocation(BaseGems.MODID, "ore/" + gem.name);
    +            ResourceLocation rule = new ResourceLocation(BaseGems.MOD_ID, "ore/" + gem.name);
                 provider.ore(WorldgenProvider.OreDefinition.builder(rule, block)
                         .material(new ResourceLocation("orespawn", gem.name))
                         .retrogen(false)
    diff --git a/src/main/resources/META-INF/coremods.json b/src/main/resources/META-INF/coremods.json
    new file mode 100644
    index 0000000..314e694
    --- /dev/null
    +++ b/src/main/resources/META-INF/coremods.json
    @@ -0,0 +1,3 @@
    +{
    +  "basegems_legacy_worlds": "coremods/basegems_legacy_worlds.js"
    +}
    diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml
    new file mode 100644
    index 0000000..9e86ac6
    --- /dev/null
    +++ b/src/main/resources/META-INF/mods.toml
    @@ -0,0 +1,33 @@
    +modLoader="javafml"
    +loaderVersion="[25,)"
    +license="LGPL-2.1"
    +issueTrackerURL="https://github.com/MinecraftModDevelopmentMods/BaseGems/issues"
    +
    +[[mods]]
    +modId="basegems"
    +version="${version}"
    +displayName="Base Gems"
    +displayURL="https://www.curseforge.com/minecraft/mc-mods/basegems"
    +authors="Jasmine Iwanek, SkyBlade1978 and the MMD Team"
    +description='''Mine and trade 33 gems, then use them to make blocks, tools and equipment. OreSpawn places their deposits.'''
    +
    +[[dependencies.basegems]]
    +modId="orespawn"
    +mandatory=true
    +versionRange="${oreSpawnRange}"
    +ordering="AFTER"
    +side="BOTH"
    +
    +[[dependencies.basegems]]
    +modId="basemetals"
    +mandatory=false
    +versionRange="[3.0.3.113021,4.0.0)"
    +ordering="AFTER"
    +side="BOTH"
    +
    +[[dependencies.basegems]]
    +modId="minecraft"
    +mandatory=true
    +versionRange="[1.13.2]"
    +ordering="NONE"
    +side="BOTH"
    diff --git a/src/main/resources/coremods/basegems_legacy_worlds.js b/src/main/resources/coremods/basegems_legacy_worlds.js
    new file mode 100644
    index 0000000..a770c2f
    --- /dev/null
    +++ b/src/main/resources/coremods/basegems_legacy_worlds.js
    @@ -0,0 +1,204 @@
    +var Opcodes = Java.type('org.objectweb.asm.Opcodes');
    +var InsnNode = Java.type('org.objectweb.asm.tree.InsnNode');
    +var InsnList = Java.type('org.objectweb.asm.tree.InsnList');
    +var FieldInsnNode = Java.type('org.objectweb.asm.tree.FieldInsnNode');
    +var JumpInsnNode = Java.type('org.objectweb.asm.tree.JumpInsnNode');
    +var LabelNode = Java.type('org.objectweb.asm.tree.LabelNode');
    +var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode');
    +var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode');
    +
    +function initializeCoreMod() {
    +    return {
    +        'basegems_forge25_leaves_fixer': {
    +            'target': { 'type': 'CLASS', 'name': 'net.minecraft.util.datafix.fixes.LeavesFix$Section' },
    +            'transformer': function(classNode) {
    +                var patched = false;
    +                for (var i = 0; i < classNode.methods.size(); ++i) {
    +                    var method = classNode.methods.get(i);
    +                    if (method.desc !== '(Lcom/mojang/datafixers/Typed;)Lcom/mojang/datafixers/Typed;') continue;
    +                    for (var instruction = method.instructions.getFirst(); instruction !== null;
    +                            instruction = instruction.getNext()) {
    +                        // Some Forge installations already contain the corrected erased overload.
    +                        if (instruction.getOpcode() === Opcodes.INVOKEVIRTUAL
    +                                && instruction.owner === 'com/mojang/datafixers/Typed' && instruction.name === 'set'
    +                                && instruction.desc === '(Lcom/mojang/datafixers/OpticFinder;' +
    +                                        'Ljava/lang/Object;)Lcom/mojang/datafixers/Typed;') {
    +                            patched = true;
    +                            continue;
    +                        }
    +                        if (instruction.getOpcode() !== Opcodes.CHECKCAST
    +                                || instruction.desc !== 'com/mojang/datafixers/Typed') continue;
    +                        var call = instruction.getNext();
    +                        while (call !== null && call.getOpcode() < 0) call = call.getNext();
    +                        if (call === null || call.getOpcode() !== Opcodes.INVOKEVIRTUAL
    +                                || call.owner !== 'com/mojang/datafixers/Typed' || call.name !== 'set'
    +                                || call.desc !== '(Lcom/mojang/datafixers/OpticFinder;' +
    +                                        'Lcom/mojang/datafixers/Typed;)Lcom/mojang/datafixers/Typed;') continue;
    +                        method.instructions.remove(instruction);
    +                        call.desc = '(Lcom/mojang/datafixers/OpticFinder;' +
    +                                'Ljava/lang/Object;)Lcom/mojang/datafixers/Typed;';
    +                        patched = true;
    +                        break;
    +                    }
    +                }
    +                if (!patched) throw new Error('Base Gems could not repair the Forge 25 leaves data fixer');
    +                return classNode;
    +            }
    +        },
    +        'basegems_legacy_world_info': {
    +            'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.SaveFormatOld' },
    +            'transformer': function(classNode) {
    +                for (var i = 0; i < classNode.methods.size(); ++i) {
    +                    var method = classNode.methods.get(i);
    +                    if (method.desc !== '(Ljava/io/File;Lcom/mojang/datafixers/DataFixer;' +
    +                            'Lnet/minecraft/world/storage/SaveHandler;)Lnet/minecraft/world/storage/WorldInfo;') continue;
    +                    var prefix = new InsnList();
    +                    prefix.add(new VarInsnNode(Opcodes.ALOAD, 0));
    +                    prefix.add(new VarInsnNode(Opcodes.ALOAD, 2));
    +                    prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                            'zone/moddev/mc/basegems/migration/LegacyWorldDataHook',
    +                            'prepareLegacyWorld', '(Ljava/io/File;Lnet/minecraft/world/storage/SaveHandler;)V', false));
    +                    method.instructions.insert(prefix);
    +                }
    +                return classNode;
    +            }
    +        },
    +        'basegems_legacy_optimizer': {
    +            'target': { 'type': 'CLASS', 'name': 'net.minecraft.util.WorldOptimizer' },
    +            'transformer': function(classNode) {
    +                var saveHandler = 'Lnet/minecraft/world/storage/ISaveHandler;';
    +                for (var i = 0; i < classNode.methods.size(); ++i) {
    +                    var method = classNode.methods.get(i);
    +                    if (method.name !== '') continue;
    +                    for (var insn = method.instructions.getFirst(); insn !== null; insn = insn.getNext()) {
    +                        if (insn.getOpcode() !== Opcodes.PUTFIELD || insn.desc !== saveHandler) continue;
    +                        var hook = new InsnList();
    +                        hook.add(new VarInsnNode(Opcodes.ALOAD, 0));
    +                        hook.add(new FieldInsnNode(Opcodes.GETFIELD, insn.owner, insn.name, insn.desc));
    +                        hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                                'zone/moddev/mc/basegems/migration/LegacyWorldDataHook',
    +                                'prepareOptimizer', '(' + saveHandler + ')V', false));
    +                        method.instructions.insert(insn, hook);
    +                        return classNode;
    +                    }
    +                }
    +                throw new Error('Base Gems could not prepare legacy IDs before optimising the world');
    +            }
    +        },
    +        'basegems_legacy_chunk_status': {
    +            'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.chunk.storage.AnvilChunkLoader' },
    +            'transformer': function(classNode) {
    +                var descriptor = '(Lnet/minecraft/world/dimension/DimensionType;' +
    +                        'Lnet/minecraft/world/storage/WorldSavedDataStorage;II)' +
    +                        'Lnet/minecraft/nbt/NBTTagCompound;';
    +                var patchedRead = false;
    +                var patchedReturn = false;
    +                for (var i = 0; i < classNode.methods.size(); ++i) {
    +                    var method = classNode.methods.get(i);
    +                    if (method.desc !== descriptor) continue;
    +                    for (var instruction = method.instructions.getFirst(); instruction !== null;
    +                            instruction = instruction.getNext()) {
    +                        var previous = instruction.getPrevious();
    +                        if (instruction.getOpcode() === Opcodes.ASTORE && previous !== null
    +                                && previous.getOpcode() === Opcodes.INVOKESTATIC
    +                                && previous.owner === 'net/minecraft/nbt/CompressedStreamTools'
    +                                && previous.desc.endsWith(')Lnet/minecraft/nbt/NBTTagCompound;')) {
    +                            var prepare = new InsnList();
    +                            prepare.add(new VarInsnNode(Opcodes.ALOAD, instruction.var));
    +                            prepare.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                                    'zone/moddev/mc/basegems/migration/LegacyWorldDataHook',
    +                                    'prepareLegacyChunk', '(Lnet/minecraft/nbt/NBTTagCompound;)V', false));
    +                            method.instructions.insert(instruction, prepare);
    +                            patchedRead = true;
    +                        } else if (instruction.getOpcode() === Opcodes.ARETURN) {
    +                            method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                                    'zone/moddev/mc/basegems/migration/LegacyWorldDataHook',
    +                                    'finalizeLegacyChunk',
    +                                    '(Lnet/minecraft/nbt/NBTTagCompound;)Lnet/minecraft/nbt/NBTTagCompound;', false));
    +                            patchedReturn = true;
    +                        }
    +                    }
    +                }
    +                if (!patchedRead || !patchedReturn) throw new Error('Base Gems could not patch the Forge 25 legacy chunk loader');
    +                return classNode;
    +            }
    +        },
    +        'basegems_legacy_worldgen_guard': {
    +            'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.gen.WorldGenRegion' },
    +            'transformer': function(classNode) {
    +                var descriptor = '(Lnet/minecraft/util/math/BlockPos;' +
    +                        'Lnet/minecraft/block/state/IBlockState;I)Z';
    +                var patched = false;
    +                for (var i = 0; i < classNode.methods.size(); ++i) {
    +                    var method = classNode.methods.get(i);
    +                    if (method.desc !== descriptor) continue;
    +                    var allowed = new LabelNode();
    +                    var prefix = new InsnList();
    +                    prefix.add(new VarInsnNode(Opcodes.ALOAD, 0));
    +                    prefix.add(new VarInsnNode(Opcodes.ALOAD, 1));
    +                    prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                            'zone/moddev/mc/basegems/migration/LegacyWorldDataHook',
    +                            'shouldBlockWorldgenWrite', '(Lnet/minecraft/world/gen/WorldGenRegion;Lnet/minecraft/util/math/BlockPos;)Z', false));
    +                    prefix.add(new JumpInsnNode(Opcodes.IFEQ, allowed));
    +                    prefix.add(new InsnNode(Opcodes.ICONST_0));
    +                    prefix.add(new InsnNode(Opcodes.IRETURN));
    +                    prefix.add(allowed);
    +                    method.instructions.insert(prefix);
    +                    patched = true;
    +                }
    +                if (!patched) throw new Error('Base Gems could not patch the Forge 25 legacy-world write guard');
    +                return classNode;
    +            }
    +        },
    +        'basegems_molten_metal_renderer': {
    +            'target': { 'type': 'CLASS', 'name': 'net.minecraft.client.renderer.BlockFluidRenderer' },
    +            'transformer': function(classNode) {
    +                var descriptor = '(Lnet/minecraft/world/IWorldReader;Lnet/minecraft/util/math/BlockPos;' +
    +                        'Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/fluid/IFluidState;)Z';
    +                for (var i = 0; i < classNode.methods.size(); ++i) {
    +                    var method = classNode.methods.get(i);
    +                    if (method.name !== 'render' || method.desc !== descriptor) continue;
    +                    var flag = false, sprites = false, color = false;
    +                    for (var instruction = method.instructions.getFirst(); instruction !== null;
    +                            instruction = instruction.getNext()) {
    +                        if (!flag && instruction.getOpcode() === Opcodes.ISTORE && instruction.var === 5) {
    +                            var hookFlag = new InsnList();
    +                            hookFlag.add(new VarInsnNode(Opcodes.ALOAD, 4));
    +                            hookFlag.add(new VarInsnNode(Opcodes.ILOAD, 5));
    +                            hookFlag.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                                    'zone/moddev/mc/basegems/client/ClientMoltenGemRenderer',
    +                                    'useOpaqueFluidPath', '(Lnet/minecraft/fluid/IFluidState;Z)Z', false));
    +                            hookFlag.add(new VarInsnNode(Opcodes.ISTORE, 5));
    +                            method.instructions.insert(instruction, hookFlag);
    +                            flag = true;
    +                        } else if (!sprites && instruction.getOpcode() === Opcodes.ASTORE && instruction.var === 6) {
    +                            var hookSprites = new InsnList();
    +                            hookSprites.add(new VarInsnNode(Opcodes.ALOAD, 4));
    +                            hookSprites.add(new VarInsnNode(Opcodes.ALOAD, 6));
    +                            hookSprites.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                                    'zone/moddev/mc/basegems/client/ClientMoltenGemRenderer',
    +                                    'overrideSprites',
    +                                    '(Lnet/minecraft/fluid/IFluidState;[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;)' +
    +                                    '[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;', false));
    +                            hookSprites.add(new VarInsnNode(Opcodes.ASTORE, 6));
    +                            method.instructions.insert(instruction, hookSprites);
    +                            sprites = true;
    +                        } else if (!color && instruction.getOpcode() === Opcodes.ISTORE && instruction.var === 7) {
    +                            var hookColor = new InsnList();
    +                            hookColor.add(new VarInsnNode(Opcodes.ALOAD, 4));
    +                            hookColor.add(new VarInsnNode(Opcodes.ILOAD, 7));
    +                            hookColor.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
    +                                    'zone/moddev/mc/basegems/client/ClientMoltenGemRenderer',
    +                                    'overrideColor', '(Lnet/minecraft/fluid/IFluidState;I)I', false));
    +                            hookColor.add(new VarInsnNode(Opcodes.ISTORE, 7));
    +                            method.instructions.insert(instruction, hookColor);
    +                            color = true;
    +                        }
    +                    }
    +                    if (!flag || !sprites || !color) throw new Error('Base Gems could not patch the Forge 25 fluid renderer');
    +                }
    +                return classNode;
    +            }
    +        }
    +    };
    +}
    diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info
    deleted file mode 100644
    index a9651be..0000000
    --- a/src/main/resources/mcmod.info
    +++ /dev/null
    @@ -1,16 +0,0 @@
    -[
    -{
    -  "modid": "basegems",
    -  "name": "Base Gems",
    -  "description": "Base Gems Addon",
    -  "version": "${version}",
    -  "mcversion": "${mcversion}",
    -  "url": "http://minecraft.curseforge.com/projects/base-gems",
    -  "updateUrl": "",
    -  "authorList": ["cyb3rkat"],
    -  "credits": "DrCyano (aka Cyanobacterium); artwork by Eejit, Aedda, jmlovelace, KiriCattus and the Tinkers' Construct team. See LICENSE-ART.txt.",
    -  "logoFile": "",
    -  "screenshots": [],
    -  "dependencies": []
    -}
    -]
    diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta
    index d20341b..343110a 100644
    --- a/src/main/resources/pack.mcmeta
    +++ b/src/main/resources/pack.mcmeta
    @@ -1,6 +1,6 @@
     {
    -  "pack":{
    -    "pack_format":3,
    -    "description":"Base Gems"
    +  "pack": {
    +    "description": "Base Gems resources",
    +    "pack_format": 4
       }
    -}
    \ No newline at end of file
    +}
    diff --git a/tools/test_fluid_registration.js b/tools/test_fluid_registration.js
    new file mode 100644
    index 0000000..1030be1
    --- /dev/null
    +++ b/tools/test_fluid_registration.js
    @@ -0,0 +1,18 @@
    +const assert = require('node:assert/strict');
    +const test = require('node:test');
    +const fs = require('node:fs');
    +const path = require('node:path');
    +const root = path.join(__dirname, '../src/main/java/zone/moddev/mc/basegems');
    +const mod = fs.readFileSync(path.join(root, 'BaseGems.java'), 'utf8');
    +const content = fs.readFileSync(path.join(root, 'content/ModContent.java'), 'utf8');
    +
    +test('parallel construction does not write the shared fluid registry', () => {
    +  assert.ok(!/initializeFluids\s*\(/.test(mod));
    +});
    +test('the sequential block event binds fluids before creating blocks', () => {
    +  assert.ok(/@SubscribeEvent\s+public static void registerBlocks\(RegistryEvent\.Register event\)\s*\{\s*initializeFluids\(\);/.test(content));
    +  assert.equal((content.match(/initializeFluids\(\);/g) || []).length, 1);
    +});
    +test('fluid registration is private to its registry subscriber', () => {
    +  assert.ok(/private static void initializeFluids\(\)/.test(content));
    +});