diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..237217aa --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,58 @@ +# Java Gradle CircleCI 2.0 Configuration file. +version: 2 +jobs: + build: + docker: + - image: circleci/openjdk:8-jdk + + working_directory: ~/repo + + environment: + JVM_OPTS: -Xmx3200m + TERM: dumb + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "build.gradle" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: Grant access to ./gradlew. + command: | + chmod +x gradlew + + - run: + name: Show the Gradle version in use with other details. + command: | + ./gradlew --version + + - run: + name: Set up the workspace for ci build + command: | + ./gradlew setupCiWorkspace + + - run: + name: Clean the workspace ready for a fresh build. + command: | + ./gradlew clean + + - run: + name: Attempt to build the mod. + command: | + ./gradlew build + + - store_artifacts: + path: ./build/libs + + - save_cache: + paths: + - ~/.gradle + key: v1-dependencies-{{ checksum "build.gradle" }} + + # run tests! + - run: ./gradlew test diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7c2ae4c0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.java] +charset = utf-8 +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d8b984a8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,77 @@ +# Store and check out repository text as LF on every platform. Windows command +# files are the sole exception below. +* text=auto eol=lf + +*.bat text eol=crlf +#*.bat text eol=lf +*.gradle text diff=java +mcmod.info text +*.java text +*.json text +*.lang text +*.mcmeta text +*.md text +*.properties text +gradlew text eol=lf +*.sh text eol=lf +*.txt text +*.xml text +*.yml text +*.yaml text +*.toml text + +# Documents +*.pdf binary + +# Graphics +*.eps binary +*.gif binary +*.ico binary +*.jpg binary +*.jpeg binary +*.png binary +*.psd binary +# SVG treated as an asset (binary) by default. If you want to treat it as text, +# comment-out the following line and uncomment the line after. +*.svg binary +#*.svg text +*.tif binary +*.tiff binary +*.webp binary +*.xcf binary + +# Other +*.exe binary +*.jar binary + +############################### +# Git Large File System (LFS) # +############################### + +# Archives +#*.7z filter=lfs diff=lfs merge=lfs -text +#*.br filter=lfs diff=lfs merge=lfs -text +#*.bz2 filter=lfs diff=lfs merge=lfs -text +#*.gz filter=lfs diff=lfs merge=lfs -text +#*.tar filter=lfs diff=lfs merge=lfs -text +#*.zip filter=lfs diff=lfs merge=lfs -text + +# Documents +#*.pdf filter=lfs diff=lfs merge=lfs -text + +# Graphics +#*.eps filter=lfs diff=lfs merge=lfs -text +#*.gif filter=lfs diff=lfs merge=lfs -text +#*.ico filter=lfs diff=lfs merge=lfs -text +#*.jpg filter=lfs diff=lfs merge=lfs -text +#*.jpeg filter=lfs diff=lfs merge=lfs -text +#*.png filter=lfs diff=lfs merge=lfs -text +#*.psd filter=lfs diff=lfs merge=lfs -text +#*.tif filter=lfs diff=lfs merge=lfs -text +#*.tiff filter=lfs diff=lfs merge=lfs -text +#*.webp filter=lfs diff=lfs merge=lfs -text +#*.xcf filter=lfs diff=lfs merge=lfs -text + +# Other +#*.exe filter=lfs diff=lfs merge=lfs -text +#*.jar filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a8c4b4c1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,148 @@ +name: OreSpawn 1.11.2 CI + +on: + push: + branches: + - master-1.11.2 + - 'feature/**' + pull_request: + branches: + - master-1.11.2 + +permissions: + contents: read + +concurrency: + group: orespawn-1.11.2-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cold-forge-bootstrap: + name: Cold Forge bootstrap + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Bootstrap Forge from an empty cache + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle + run: | + set -euo pipefail + test ! -e .gradle + test ! -e "$GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + chmod +x ./gradlew + gradle_args=( + classes verifyLegacyFixtures + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Verify same-cache bootstrap offline + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle + run: | + set -euo pipefail + gradle_args=( + classes verifyLegacyFixtures + --rerun-tasks --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + build: + name: Build, test, and audit + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - name: Make the wrapper executable + run: chmod +x ./gradlew + + - name: Build, test, and audit release artifacts + run: >- + ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums + verifyEclipseProductionClasspath --no-daemon --stacktrace + -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 + + - name: Upload audited release candidate + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: OreSpawn-1.11.2-${{ github.sha }} + if-no-files-found: error + retention-days: 30 + path: | + build/libs/OreSpawn-4.0.16.111021.jar + build/libs/OreSpawn-4.0.16.111021-sources.jar + build/libs/OreSpawn-4.0.16.111021-javadoc.jar + build/release/SHA256SUMS + CHANGELOG.txt + + - name: Upload diagnostics on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: OreSpawn-1.11.2-diagnostics-${{ github.sha }} + if-no-files-found: ignore + retention-days: 14 + path: | + build/test-results/** + build/reports/** + build/*-run/logs/** + build/legacy-abi/**/run/logs/** + build/*-integration-run/**/*.properties + build/problems/** diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..24d3f67a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +name: CodeQL + +on: + push: + branches: + - master-1.11.2 + - 'feature/**' + pull_request: + branches: + - master-1.11.2 + schedule: + - cron: '43 7 * * 4' + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze Java + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + with: + languages: java-kotlin + + - name: Compile production code + run: | + chmod +x ./gradlew + gradle_args=( + clean classes + --no-daemon --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Analyze + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml new file mode 100644 index 00000000..83bc858f --- /dev/null +++ b/.github/workflows/release-on-tag.yml @@ -0,0 +1,94 @@ +name: Start OreSpawn release from tag + +on: + push: + tags: + - '*.*.*.*' + +permissions: + actions: read + contents: read + +concurrency: + group: orespawn-release-starter-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + validate-release-tag: + name: Validate tag for manual release confirmation + if: github.repository == 'MinecraftModDevelopmentMods/OreSpawn' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - name: Validate release tag, target metadata, and prior CI + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + value() { sed -n "s/^$1=//p" gradle.properties; } + release_version="$(value mod_version)" + minecraft_version="$(value minecraft_version)" + loader_name="$(value loader_name)" + loader_code="$(value loader_code)" + + IFS=. read -r mc_major mc_minor mc_patch extra <<<"$minecraft_version" + if [[ -n "${extra:-}" || -z "${mc_major:-}" || -z "${mc_minor:-}" ]]; then + echo "Invalid minecraft_version=$minecraft_version" >&2 + exit 1 + fi + mc_patch="${mc_patch:-0}" + if [[ ! "$mc_major" =~ ^[0-9]+$ || ! "$mc_minor" =~ ^[0-9]+$ || ! "$mc_patch" =~ ^[0-9]+$ ]]; then + echo "Invalid minecraft_version=$minecraft_version" >&2 + exit 1 + fi + case "$loader_name:$loader_code" in + forge:1|neoforge:2) ;; + *) echo "Invalid loader metadata $loader_name/$loader_code" >&2; exit 1 ;; + esac + printf -v minor_padded '%02d' "$((10#$mc_minor))" + printf -v patch_padded '%02d' "$((10#$mc_patch))" + target_suffix="${mc_major}${minor_padded}${patch_padded}${loader_code}" + + if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.${target_suffix}$ ]]; then + echo "mod_version $release_version does not match $minecraft_version $loader_name target $target_suffix" >&2 + exit 1 + fi + if [[ "$GITHUB_REF_NAME" != "$release_version" ]]; then + echo "Release tag must equal mod_version $release_version; found $GITHUB_REF_NAME" >&2 + exit 1 + fi + + successful_ci="$(gh api \ + "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?per_page=100" \ + --jq '[.check_runs[] | select(.name == "Build, test, and audit" and .conclusion == "success")] | length')" + if [[ "$successful_ci" -lt 1 ]]; then + echo "The tagged commit has no successful Build, test, and audit check" >&2 + exit 1 + fi + + - name: Record the required manual publication step + env: + RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/workflows/deploy-release.yml + run: | + { + echo "## Release candidate validated" + echo + echo "Tag \`$GITHUB_REF_NAME\` matches the selected target and has a successful Build, test, and audit check." + echo + echo "**Nothing has been published.**" + echo + echo "To continue, open [Deploy OreSpawn release]($RELEASE_WORKFLOW_URL), select **Run workflow**, and enter:" + echo + echo "- release_version: \`$GITHUB_REF_NAME\`" + echo "- curseforge_release_level: \`release\`, \`beta\`, or \`alpha\`" + echo "- confirm_live_publication: \`true\`" + echo + echo "The dispatcher builds and audits the immutable bundle before the separate \`release\` environment approval gate." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml new file mode 100644 index 00000000..0b533e41 --- /dev/null +++ b/.github/workflows/validate-gradle-build.yml @@ -0,0 +1,23 @@ +name: Validate Gradle Wrapper + +on: + push: + branches: + - master-1.11.2 + - 'feature/**' + pull_request: + branches: + - master-1.11.2 + +permissions: + contents: read + +jobs: + validation: + name: Validation + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate wrapper integrity + uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 diff --git a/.gitignore b/.gitignore index 4f38052b..8ef90b58 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,11 @@ bin *.launch .settings +.settings/* .metadata .classpath .project +/.eclipse/ # idea out @@ -20,8 +22,42 @@ build # other eclipse run +/run-data/ +/run.* +/*.log +classes +logs +/mcmodsrepo/ + +# machine-specific agent context (public integration notes live under /docs) +/AGENTS.md +/AGENT.md +/agent-notes/ +/.agent/ +/.agents/ +/.codex/ +/.claude/ +/.gemini/ +/CLAUDE.md +/agent-*.md +/codex-*.md + +# local regression, benchmark, and profiling evidence +/run-*/ +/benchmark-*/ +/regression-*/ +/evidence/ +/evidence-*/ +/*-evidence/ +/*.jfr +/*.hprof +/hs_err_pid*.log +/replay_pid*.log /README.txt /forge-*-changelog.txt + +secret.json +libs/* + .DS_Store -.DS_Store/* diff --git a/.travis.yml b/.travis.yml index 4553e94c..dd5385bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,29 @@ language: java -sudo: false +os: linux +dist: trusty +addons: + apt: + update: true + packages: +# - oracle-java8-installer +git: + quiet: true +arch: amd64 +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ cache: directories: - - $HOME/.gradle + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ notifications: email: false jdk: - oraclejdk8 -before_install: +before_install: skip +install: skip +before_script: - chmod a+x gradlew -install: ./gradlew setupCIWorkspace -S -script: ./gradlew clean build -S +script: + - ./gradlew setupCIWorkspace -S + - ./gradlew clean build -S diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d119e6d8..76ae71f9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1 +1,99 @@ -Version 3.2.1 +Version 4.0.16.111021 + +* Adopt the shared 4.0.16 release identity. Forge 1.11 has neither + three-dimensional biome-cell attribution nor a server-side GameTest harness, + so the 4.0.15 attribution and 4.0.16 GameTest lifecycle repairs are not + applicable on this target. +* Ordinary dedicated benchmark servers continue to stop automatically when + requested. + +Version 4.0.14.111021 + +* Convert exposed one-layer vanilla Snow from the first free cell immediately + above Minecraft's motion-blocking surface when a dimension supplies a custom + snow material. +* Retain the existing surface Ice conversion and leave buried or authored Snow + and Ice unchanged. + +Version 4.0.13.111021 + +* Give the public ore-dimension builder the exact biome include/exclude and + biome-dictionary filter support already available in provider JSON. +* Accept valid namespaced geome IDs in both creation-editor validation paths + while preserving legacy unnamespaced geome keys. +* API major 1, schemas, existing profiles, generated chunks, and worldgen + behaviour are unchanged. + +Version 4.0.10.111021 + +* Evaluate Stable Layers rock min_y and max_y bounds against actual world Y + instead of the vertically shifted formation coordinate. +* Preserve shifted formation identity for layer, family, and rock choice while + preventing vanilla Stone fallback near the Y 0/255 dimension bounds. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + +Version 4.0.9.111021 + +* Replace provider-declared natural terrain hosts during the existing early + geology scan before structures and vegetation can author matching blocks. +* Keep air, liquids, bedrock, and block entities protected even when their + block IDs are mistakenly declared as terrain hosts. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + +Version 4.0.8.111021 + +* Preserve long host, tag, and biome-list values when OreSpawn editors load + and save an existing profile without user changes. +* Add reproducible ForgeGradle 7 builds and guarded Maven, CurseForge, and + GitHub release automation. +* Retain packaged access-transformer declaration and exact Forge 13 runtime + qualification. Version 4.0.7 was a Forge 1.12.2-only repair. + +Version 4.0.6.111021 + +* Adopt target-qualified four-component versions so Minecraft and loader compatibility can be identified from the mod version. +* Establish the OS4 product line on Minecraft 1.11.2 and Forge 13.20.1.2588 + using Java 8 bytecode and the modern ForgeGradle 7 release pipeline. +* Add a deprecated OS3 binary/configuration compatibility bridge for existing + 3.2.2 and 3.3.1 consumers, translating plugins and registrations into one + deduplicated OS4 scheduler with atomic backups and an idempotent report. +* Preserve 1.11 metadata block states, legacy dimension/biome selectors, + weighted outputs, retrogen flags, vanilla suppression, and flat bedrock. +* Keep existing Mineralogy 3 worlds on the matching Cyano engine contract, + distinguishing carried 1.10, native 1.11, and native 1.12 configurations, + exact rock order, disabled geology, family lists, and realistic coal behavior. +* Write deterministic human-readable upgrade reports for consumed OS3 and + Mineralogy configuration alongside the detailed machine-readable report. +* Fix provider top and filler materials being generated one block below exposed ground. +* Apply underwater materials from the corrected ground and ceiling materials to roof undersides. +* Preserve trees, vegetation, structures and block entities by running surface replacement before late features. +* Preserve exact custom-biome rules and stagger close Stable Layers geome boundaries. +* Retain the target-native Edge Detail and managed vanilla-ore defaults for Minecraft 1.11.2. +* Apply vanilla-ore suppression at Forge's final standard event priority so + later ordinary mod listeners cannot re-enable claimed vanilla ore features. +* Keep air-exposure inspection inside the active chunk so edge candidates do + not load neighbouring chunks or depend on neighbour-generation order. +* Place dynamic fluid deposits through Forge's world-write path so scheduled + liquid ticks cannot retain OreSpawn's reusable generation cursor. +* Adapt geology, surfaces, ores, fluids, bedrock and static biome overlays to + Forge 1.11 terrain and registry events with one deduplicated IWorldGenerator. +* Retain the OreSpawn biome registrar and deferred handles on Forge 1.11 registry events. +* Preserve vanilla springs in provider rocks through Forge's native terrain hooks. +* Preserve Forge 1.11.2 editor backgrounds, tooltip clearing, legacy `.name` + translations, non-vanilla world-button identity, and explicit navigation. +* Existing chunks are not rewritten; the correction applies while generating new chunks. + +Version 4.0.5 + +* Complete native translations for every shipped non-English locale +* Add automatic fresh-and-reload validation for provider-owned custom biomes +* Verify both biome-registration helpers and the Forge 1.11 registrar lifecycle +* Confirm Minecraft 1.11.2 ResourceLocation documentation and ForgeGradle 7 workflow +* Preserve public API major 1 and provider/global/world schemas 4/6/5 + +Version 3.3.1 + +* Fix several bugs +* Add ability to completely replace oregeneration (By popular request) diff --git a/README.md b/README.md index 39dde390..6cd57ad4 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,145 @@ -# OreSpawn -Minecraft library mod that provides better control over the spawning of ores in Minecraft. - -## How it works -Ore Spawn parses all of the .json files found in `orespawn` and adds ore generators to the game based on those files. The JSON structure looks like this: - -```json -[ - { - "dimension": -1, - "ores": [ - { - "block": "minecraft:quartz_ore", - "size": 15, - "variation": 4, - "frequency": 7, - "min_height": 0, - "max_height": 128 - } - ] - }, - { - "ores": [ - { - "block": "minecraft:coal_ore", - "size": 25, - "variation": 12, - "frequency": 20, - "min_height": 0, - "max_height": 128 - }, - { - "block": "minecraft:iron_ore", - "size": 8, - "variation": 4, - "frequency": 20, - "min_height": 0, - "max_height": 64 - }, - { - "block": "minecraft:gold_ore", - "size": 8, - "variation": 2, - "frequency": 2, - "min_height": 0, - "max_height": 32 - }, - { - "block": "minecraft:diamond_ore", - "size": 6, - "variation": 3, - "frequency": 8, - "min_height": 0, - "max_height": 16 - }, - { - "block": "minecraft:lapis_ore", - "size": 5, - "variation": 2, - "frequency": 1, - "min_height": 0, - "max_height": 32 - }, - { - "block": "minecraft:emerald_ore", - "size": 1, - "variation": 0, - "frequency": 8, - "min_height": 4, - "max_height": 32, - "biomes": [ - "minecraft:extreme_hills", - "minecraft:smaller_extreme_hills" - ] - }, - { - "block": "minecraft:dirt", - "size": 112, - "variation": 50, - "frequency": 10, - "min_height": 0, - "max_height": 255 - }, - { - "block": "minecraft:gravel", - "size": 112, - "variation": 50, - "frequency": 8, - "min_height": 0, - "max_height": 255 - }, - { - "block": "minecraft:stone", - "state": "variant=granite", - "size": 112, - "variation": 50, - "frequency": 10, - "min_height": 0, - "max_height": 255 - }, - { - "block": "minecraft:stone", - "state": "variant=diorite", - "size": 112, - "variation": 50, - "frequency": 10, - "min_height": 0, - "max_height": 255 - }, - { - "block": "minecraft:stone", - "state": "variant=andesite", - "size": 112, - "variation": 50, - "frequency": 10, - "min_height": 0, - "max_height": 255 - } - ] - } -] +[![Discord](https://img.shields.io/badge/Discord-MMD-green.svg?style=flat&logo=Discord)](https://discord.moddev.zone) +[![CurseForge downloads](https://cf.way2muchnoise.eu/full_mmd-orespawn_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn) +[![Supported Minecraft versions](https://cf.way2muchnoise.eu/versions/Minecraft_mmd-orespawn_all.svg)](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn) +[![Build, test, and audit](https://github.com/MinecraftModDevelopmentMods/OreSpawn/actions/workflows/ci.yml/badge.svg?branch=master-1.11.2)](https://github.com/MinecraftModDevelopmentMods/OreSpawn/actions/workflows/ci.yml?query=branch%3Amaster-1.11.2) + +# MMD OreSpawn + +OreSpawn 4 is a provider-driven world-generation engine for Minecraft 1.11.2. +It gives mods and modpacks one place to configure ores, deposit shapes, optional +rock strata and geomes, provider-owned underground fluid deposits, biome +palettes and world materials, flat bedrock, and bounded ore retrogen. + +This branch builds target-qualified version `4.0.16.111021`: the OreSpawn 4.0.16 +feature set for Minecraft 1.11.2 and Forge. See the +[versioning policy](docs/VERSIONS.md) for the encoding and release convention. + +Its deprecated OS3 compatibility layer imports OreSpawn 3 configuration and +keeps existing OreSpawn 3 consumer jars working while translating their rules +into the OreSpawn 4 scheduler. It preserves ranged legacy block budgets, +metadata block states, exclusive legacy height ceilings, and the historical +"all dimensions except Nether and End" policy used by mods such as Base +Metals. OreSpawn never schedules both the original OS3 generator and its OS4 +translation. + +This is not the unrelated mod that adds mobs and dimensions under the same +name. + +## What Happens When It Is Installed? + +OreSpawn is deliberately passive on its own. It does not replace stone, remove +vanilla ores, or change the Nether merely because the jar is installed. A +provider mod or a modpack profile must opt features in. + +Mineralogy 6 is the first full provider. It supplies its rocks, ores, crude-oil +deposit, geomes, biome influences, and recommended settings to OreSpawn. An +ore-only provider such as Base Metals can supply ores and host tags without +enabling rock layers or biome replacement. A total-conversion provider can add +biomes and replace surfaces, aquifer fluids, snow, and ice without depending on +TerraBlender. + +## Players And Server Owners + +When a provider exposes world settings, use **OreSpawn...** on the Create World +screen. **Recommended Defaults** restores the settings supplied by the +installed mods and pack. The in-game **Help & Guide** explains the controls. + +Important files: + +| Location | Purpose | +|---|---| +| `config/orespawn-worldgen.json` | Defaults for newly created worlds | +| `/serverconfig/orespawn-worldgen.json` | Complete settings snapshot for one world | +| `config/-orespawn.json` | Optional modpack override for one provider | +| `config/orespawn-migration/migration-report.txt` | Deterministic OS3 import report and required actions | +| `config/orespawn-guide/README.md` | Guide exported automatically on first load | + +Profile edits affect newly generated chunks. Ore and flat-bedrock retrogen are +separate opt-in features; OreSpawn never retro-generates rock strata. + +When an existing world records Mineralogy 3 or earlier and has no OreSpawn 4 +world profile, OreSpawn preserves that world's Cyano geology contract before +new chunks generate. It distinguishes carried Mineralogy 1.10, native +Mineralogy 1.11, and native Mineralogy 1.12 configuration, including the +different ordered rock families, `REALISTIC_COAL_LAYERS`, and +`PLACE_MINERALOGY_ROCK`. A hybrid file created while upgrading is interpreted +using the Mineralogy version saved with the world; a genuinely ambiguous file +on this target uses the native 1.11 lineage and records a warning. Fresh worlds +still use the installed provider's recommended +engine; selecting Sky for an upgraded world is an explicit choice which may +create an old/new terrain seam. + +After an upgrade, read `config/orespawn-upgrade-report.txt` for translated OS3 +rules and `/serverconfig/orespawn-upgrade-report.txt` for the Mineralogy +handoff. The reports list the sources, selected lineage, preserved values and +anything needing review without rewriting existing chunks. + +To move a configured single-player world to a dedicated server, copy the +world's `serverconfig/orespawn-worldgen.json` with the world and install the +same provider mods on the server. + +## Mod And Modpack Integration + +Mods can provide declarative rules in either of these ways: + +- package `assets//orespawn/provider.json` in the mod jar; +- call `OreSpawnApi.enqueue(WorldgenProvider)` during normal Forge 1.11 + initialization, before post-initialization freezes provider discovery. + +Modpacks can override a provider with `config/-orespawn.json`. A present +override is authoritative and fails closed when invalid, so a broken pack file +cannot silently disable another mod's native ore generation. + +Only `zone.moddev.mc.orespawn.api` is supported Java API. API major version `1` +is also recorded in the jar manifest as `OreSpawn-API-Version`. + +Start with: + +- [Player guide](docs/PLAYER_GUIDE.md) +- [Developer guide](docs/DEVELOPER_GUIDE.md) +- [Configuration reference](docs/CONFIGURATION.md) +- [Provider JSON guide](docs/PROVIDERS.md) +- [Java API guide](docs/API.md) +- [Biome and world-material guide](docs/BIOMES.md) +- [Versioning and release policy](docs/VERSIONS.md) +- [Schemas and examples](docs/README.md) + +The full documentation bundle is packaged under `META-INF/orespawn/docs/` and +exported to `config/orespawn-guide/` without overwriting existing files. + +## Building + +Run Gradle with exact Temurin `17.0.1+12` from the repository root. Install +exact Temurin `25.0.3+9` for ForgeGradle's Mavenizer and exact Temurin +`8.0.502+7` for Minecraft 1.11.2 production and fixture compilation. Java 17 +remains the Gradle runtime and production bytecode remains Java 8; the build +rejects a different Java 8 toolchain. Hosted CI also proves an online bootstrap +from an empty Gradle home followed by an offline replay from that same cache: + +```powershell +.\gradlew.bat clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums --no-daemon +.\gradlew.bat genEclipseRuns verifyEclipseProductionClasspath --no-daemon ``` -### dimension -The number ID of a dimension. Don't specify any dimension to target all dimensions *that are not already specified*. -### ores -Array of JSON objects specifying ore generators for this dimension -### block -Text ID of a block (the same you would use in the /give command) -### state -The state of a block (typically used for colored blocks) -### size -The number of blocks to spawn. Unlike the default Minecraft world settings JSON, this is the actually number of blocks that will spawn. -### variation -How much to randomly vary the number of blocks spawned (I recommend making this value 50% of the *size* value) -### frequency -How often, per chunk, to attempt to spawn this ore block. This value can be a fraction less than 1. If this value is between 0 and 1, then not every chunk will have a spawn in it. For example, a frequency of 0.1 means that there will be one attempt to spawn the ore per 10 chunks. -### min_height -The lowest Y-coordinate that the ore is allowed to spawn at -### max_height -The highest Y-coordinate that the ore is allowed to spawn at -### biomes -If this array is not empty, then the biomes in which the ore will spawn is restricted to those specified by ID in this array. - -# API -Adding OreSpawn support to your mod is not hard. Look at `VanillaOreSpawn.java` for an example. +`build` runs the standard `check` lifecycle. In addition to the JUnit suite, +that lifecycle packages a test-only provider mod and verifies 2,304 exposed +surface columns per built-in normal-noise End and Nether dimension, including +underwater, immediate filler, and ceiling-underside behavior. It also proves +later vegetation, structures, and block entities +survive, validates provider-rock vanilla springs and an external ore-pattern +registration, then reopens and checks the exact saved world. The fixture is +not included in OreSpawn's published jars. + +Import or refresh the project with Eclipse Buildship, then run +`genEclipseRuns` and `verifyEclipseProductionClasspath`. This branch uses +ForgeGradle 7.0.34, the Gradle 9.6.1 wrapper, Forge 13.20.1.2588, the +`stable_32` MCP mappings, and pack format 2. Ordinary Eclipse launches exclude +tests and fixtures. Published jars are deterministic, SRG-reobfuscated for the +Forge 1.11 runtime, audited for their access transformer and contents, and +accompanied by SHA-256 checksums. + +Machine-specific `AGENTS.md` and `agent-notes/` files are intentionally ignored. +Public developer and AI integration guidance lives in `docs/` and is included +in the built jar. + +OreSpawn is licensed under LGPL-2.1. diff --git a/build.gradle b/build.gradle index 6e07e0ac..d1803906 100644 --- a/build.gradle +++ b/build.gradle @@ -1,316 +1,2097 @@ -def corePlugin = '' - -buildscript { - repositories { - jcenter() - maven { - name = 'forge' - url = 'http://files.minecraftforge.net/maven' - } - maven { - name = 'gradle' - url 'https://plugins.gradle.org/m2/' - } - maven { - name = 'sonatype' - url = 'https://oss.sonatype.org/content/groups/public' - } - } - dependencies { - classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT' - classpath 'gradle.plugin.com.matthewprenger:CurseGradle:1.0.11' - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2' - } -} - -apply plugin: 'net.minecraftforge.gradle.forge' -apply plugin: 'com.matthewprenger.cursegradle' -apply plugin: 'maven-publish' -apply plugin: 'org.sonarqube' - -javadoc { - failOnError = false -} - -description = 'OreSpawn' -def mod_file = getModFile() -def mc_version = "1.11.2" -def short_version = getVersion("VERSION", mod_file) -version = mc_version + "-" + short_version -if (System.getenv().BUILD_NUMBER) { - version += '.' + System.getenv().BUILD_NUMBER -} -group = "com.mcmoddev" -archivesBaseName = "OreSpawn" -sourceCompatibility = targetCompatibility = "1.8" - -class Secrets { - def data = null - - def getProperty(String key) { - return data ? data[key] : '' - } -} - -import groovy.json.JsonSlurper - -def secretFile -if (System.getenv().SECRET_FILE) { - secretFile = file System.getenv().SECRET_FILE -} else { - secretFile = file 'secret.json' -} - -project.ext.secret = new Secrets() -if (secretFile.exists()) { - secretFile.withReader { - project.ext.secret.data = new JsonSlurper().parse it - } -} - -minecraft { - version = '1.11.2-13.20.1.2386' - runDir = 'run' - mappings = 'stable_32' -// coreMod = '' - makeObfSourceJar = false - - replace '@FINGERPRINT@', project.findProperty('signSHA1') -} - -repositories { - maven { // Mantle, TCon, JEI - name 'DVS1 Maven FS' - url 'http://dvs1.progwml6.com/files/maven' - } - maven { // CCL, CCC, NEI - name 'Chickenbones Repo' - url 'http://chickenbones.net/maven/' - } - maven { // The One Probe - name 'tterrag' - url 'http://maven.tterrag.com/' - } - maven { - name 'MMD' - url 'https://maven.mcmoddev.com/' - } - maven { // MCMultipart - name 'amadornes' - url 'http://maven.amadornes.com/' - } - maven { // Tesla - name 'epoxide' - url 'http://maven.epoxide.org' - } - maven { // CraftTweaker, ZenScript - name 'jared maven' - url 'http://blamejared.com/maven' - } - maven { // IC2 - name 'industrialcraft' - url 'http://maven.ic2.player.to' - } - maven { // WAILA/HWYLA - name 'tehnut' - url 'http://tehnut.info/maven' - } - maven { // CoFH - name 'Covers Maven' - url 'http://maven.covers1624.net' - } - maven { - name = 'CurseForge' - url = 'https://minecraft.curseforge.com/api/maven/' - } - maven { - name 'opencomputers' - url 'http://maven.cil.li/' - } - maven { // Mekanism, TAIGA - name 'jitpack' - url 'https://jitpack.io' - } -} - -dependencies { - // None -} - -processResources { - inputs.property 'version', project.version - inputs.property 'mcversion', project.minecraft.version - - from (sourceSets.main.resources.srcDirs) { - include 'mcmod.info' - expand 'version': short_version, 'mcversion': project.minecraft.version - } - - from (sourceSets.main.resources.srcDirs) { - exclude 'mcmod.info' - } -} - -jar { - manifest { -// attributes 'FMLCorePluginContainsFMLMod': 'true' -// attributes 'FMLCorePlugin': corePlugin -// attributes 'FMLAT' : '' - } -} - -task apiJar(type: Jar, dependsOn: classes) { - classifier = 'api' - from sourceSets.main.allSource - exclude('com/mcmoddev/orespawn/impl/**') - exclude('com/mcmoddev/orespawn/impl/features/**') - exclude('com/mcmoddev/orespawn/json/**') - exclude('com/mcmoddev/orespawn/world/**') - exclude('com/mcmoddev/orespawn/commands/**') - exclude('com/mcmoddev/orespawn/data/**') - exclude('com/mcmoddev/orespawn/*.java') -} - -task devJar(type: Jar) { - classifier = 'dev' - from sourceSets.main.output -} - -task deobfJar(type: Jar) { - classifier = 'deobf' - from sourceSets.main.output -} - -task signJar(type: SignJar, dependsOn: reobfJar) { - - // Skips if the keyStore property is missing. - onlyIf { - project.hasProperty('keyStore') - } - - // findProperty allows us to reference the property without it existing. - // Using project.propName would cause the script to fail validation if - // the property did not exist. - keyStore = project.findProperty('keyStore') - alias = project.findProperty('keyStoreAlias') - storePass = project.findProperty('keyStorePass') - keyPass = project.findProperty('keyStoreKeyPass') - inputFile = jar.archivePath - outputFile = jar.archivePath -} - -build.dependsOn signJar - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.getDestinationDir() -} - -artifacts { - archives apiJar - archives devJar - archives deobfJar - archives sourceJar - archives javadocJar -} - -publishing { - publications { - mavenJava(MavenPublication) { - groupId project.group - artifactId project.archivesBaseName - version project.version - from components.java - artifact sourceJar { - classifier 'sources' - } - artifact apiJar { - classifier 'api' - } - artifact devJar { - classifier 'dev' - } - artifact deobfJar { - classifier 'deobf' - } - artifact javadocJar { - classifier 'javadoc' - } - } - } - repositories { - maven { - credentials { - username secret.username - password secret.password - } - url secret.url - } - } -} - -curseforge { - apiKey = secret.curseforgeAPIKey - project { - id = "245586" - changelog = file("CHANGELOG.txt") - releaseType = "release" - def projName = "OreSpawn" - def displayVersion = getVersion("VERSION", mod_file) - if (System.getenv().BUILD_NUMBER) { - displayVersion += '.' + System.getenv().BUILD_NUMBER - } - mainArtifact(jar) { - displayName = "$project.description $displayVersion" -// relations { -// } - } - addArtifact(apiJar) { - displayName = "$project.description $displayVersion API" - } - addArtifact(sourceJar) { - displayName = "$project.description $displayVersion Sources" - } - addArtifact(deobfJar) { - displayName = "$project.description $displayVersion Development" - } - addArtifact(javadocJar) { - displayName = "$project.description $displayVersion Javadoc" - } - } -} - -sonarqube { - properties { - property 'sonar.host.url', secret.sonarHost - property 'sonar.organization', secret.sonarOrganization - property 'sonar.login', secret.sonarToken - property 'sonar.projectName', project.archivesBaseName - property 'sonar.projectKey', "$project.group:$project.archivesBaseName" - } -} - -String getModFile() { - String path = 'src/main/java/com/mcmoddev/orespawn/data/Constants.java' - return path -} - -String getVersion(String type, String mod_file) { - String major = '0' - String revision = '0' - String patch = '0' - String prefix = "public static final String $type = \"" - File file = file(mod_file) - file.eachLine { String s -> - s = s.trim() - if (s.startsWith(prefix)) { - s = s.substring(prefix.length(), s.length() - 2) - String[] pts = s.split("\\.") - - major = pts[0] - revision = pts[1] - patch = pts[2] - } - } - return "$major.$revision.$patch" -} +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile +import org.apache.tools.ant.filters.FixCrLfFilter + +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'maven-publish' + id 'net.minecraftforge.renamer' version '1.1.5' + id 'net.minecraftforge.accesstransformers' version '2.0.0' + id 'net.minecraftforge.gradle' version '7.0.34' +} + +group = project.mod_group +version = project.mod_version +base.archivesName = 'OreSpawn' + +def versionParts = project.mod_version.toString().tokenize('.') +if (versionParts.size() != 4 || !versionParts.every { it ==~ /\d+/ }) { + throw new GradleException("mod_version must use Major.Minor.Bug.Target numeric form: ${project.mod_version}") +} + +def minecraftVersionParts = project.minecraft_version.toString().tokenize('.') +if (minecraftVersionParts.size() < 2 || minecraftVersionParts.size() > 3 + || !minecraftVersionParts.every { it ==~ /\d+/ }) { + throw new GradleException("minecraft_version must use major.minor or major.minor.patch numeric form: " + + project.minecraft_version) +} +def minecraftPatch = minecraftVersionParts.size() == 3 ? minecraftVersionParts[2] : '0' +def expectedTargetVersion = "${minecraftVersionParts[0]}" \ + + "${minecraftVersionParts[1].padLeft(2, '0')}" \ + + "${minecraftPatch.padLeft(2, '0')}" \ + + project.loader_code +if (versionParts[3] != expectedTargetVersion) { + throw new GradleException("mod_version target ${versionParts[3]} does not match " + + "Minecraft ${project.minecraft_version} ${project.loader_name} target ${expectedTargetVersion}") +} + +ext.functional_version = versionParts[0..2].join('.') +ext.display_version = project.mod_version +ext.release_tag = project.mod_version +def expectedMavenGroup = 'zone.moddev.mc.orespawn' +def expectedMavenArtifact = 'OreSpawn' +def expectedMavenCoordinate = "${expectedMavenGroup}:${expectedMavenArtifact}:${project.version}" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + vendor = JvmVendorSpec.ADOPTIUM + } + withSourcesJar() + withJavadocJar() +} + +tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(8) + vendor = JvmVendorSpec.ADOPTIUM + } + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + options.encoding = 'UTF-8' + options.compilerArgs.addAll(['-Xmaxerrs', '1000']) +} + +tasks.named('compileTestJava', JavaCompile) { + options.compilerArgs.add('-proc:none') +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() + workingDir = project.projectDir +} + +tasks.withType(Javadoc).configureEach { + failOnError = false + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') + options.addBooleanOption('notimestamp', true) +} + +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +def archiveTextSuffixes = [ + '.cfg', '.css', '.html', '.info', '.java', '.js', '.json', '.lang', + '.mcmeta', '.md', '.properties', '.txt', '.xml' +] +def archiveTextPatterns = archiveTextSuffixes.collect { "**/*${it}".toString() } +archiveTextPatterns.addAll(['**/element-list', '**/package-list']) +def normalizeArchiveLineEndings = { details -> + details.filter(FixCrLfFilter, + eol: FixCrLfFilter.CrLf.newInstance('lf'), + eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) +} + +minecraft { + if (gradle.startParameter.offline) { + mavenizerArguments.add('--offline') + } + mappings channel: project.mapping_channel, version: project.mapping_version + accessTransformer = 'META-INF/accesstransformer.cfg' + + runs { + configureEach { + workingDir.convention layout.projectDirectory.dir('run') + systemProperty 'forge.logging.console.level', 'debug' + } + register('client') + register('server') { + // Forge 13's legacy launcher expects the bare trailing token used + // by the official server command line: `java -jar forge.jar nogui`. + args 'nogui' + } + } +} + +// Forge 1.11 expects each development mod to expose classes and resources from +// one classpath root. ForgeGradle 7 splits those outputs, so build a separate +// merged launch root without contaminating either canonical source-set output. +def forge11DevelopmentOutput = layout.buildDirectory.dir('forge11-development/main') +def syncForge11DevelopmentResources = tasks.register('syncForge11DevelopmentResources', Sync) { + group = 'ide' + description = 'Merges processed resources into the Forge 1.11 development launch root.' + dependsOn tasks.named('classes') + from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory } + from sourceSets.main.output.resourcesDir + into forge11DevelopmentOutput +} +tasks.configureEach { + if (name == 'runClient' || name == 'runServer') { + dependsOn syncForge11DevelopmentResources + doFirst { + Set splitMainOutputs = [ + tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile, + sourceSets.main.output.resourcesDir + ] as Set + classpath = files(forge11DevelopmentOutput, classpath.filter { + !splitMainOutputs.contains(it) + }) + } + } +} + +repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven + + exclusiveContent { + forRepository { + maven { + name = 'Sponge' + url = 'https://repo.spongepowered.org/repository/maven-public' + } + } + filter { includeGroupAndSubgroups('org.spongepowered') } + } + + mavenCentral() + maven { + name = 'MinecraftLibraries' + url = 'https://libraries.minecraft.net/' + } +} + +def legacyFixtureRoot = file("${rootDir}/ci-fixtures") +def legacyFixtureArtifacts = new File(legacyFixtureRoot, 'artifacts') +def legacyFixtureWorlds = new File(legacyFixtureRoot, 'worlds') +def legacyMineralogy110OracleJar = new File( + legacyFixtureArtifacts, 'Mineralogy-1.10.2-3.3.8.26.jar') +def legacyMineralogy111OracleJar = new File( + legacyFixtureArtifacts, 'Mineralogy-1.11.2-3.3.0.jar') +def legacyMineralogy112OracleJar = new File( + legacyFixtureArtifacts, 'Mineralogy-1.12.2-3.8.0.53.jar') +def legacyOreSpawn111Jar = new File( + legacyFixtureArtifacts, 'OreSpawn-1.11.2-3.2.2.jar') +def legacyFixtureHashes = [ + 'artifacts/Mineralogy-1.10.2-3.3.8.26.jar': + '88A6237C9A0E2C8891718B68C373E741C78B8494F5E68D8093CA9339F3BC4D87', + 'artifacts/Mineralogy-1.11.2-3.3.0.jar': + '5737C0FA65CB334D191FCD147B4FB16C26823F8EE9882E70A6888E32DA8A9440', + 'artifacts/Mineralogy-1.12.2-3.8.0.53.jar': + 'C42E608E5662A94138BD2461019D33283F96E3FB66E28DB91C00A49E9A8005CD', + 'artifacts/OreSpawn-1.11.2-3.2.2.jar': + '8CAB9D60C988C6239FCFCAC4AF7886E63A77BD97A1FDFD82DE5EBE30987F9614', + 'artifacts/OreSpawn_1.10.2-1.1.0.jar': + '91345E4B825AA06E05F470066F467AABA0C7A2804A8DBCA4D3CD1905ED53607C', + 'worlds/os3-331-default-source.zip': + '2852FA549C7A952CCC0EAE1454057CA81BD91F5BB323BEA1030452BB1D82FDFD' +] + +task verifyLegacyFixtures { + group = 'verification' + description = 'Verifies the sealed ABI and migration corpus used only by isolated tests.' + inputs.files legacyFixtureHashes.keySet().collect { new File(legacyFixtureRoot, it) } + doLast { + legacyFixtureHashes.each { String relativePath, String expectedHash -> + File fixture = new File(legacyFixtureRoot, relativePath) + if (!fixture.isFile()) { + throw new GradleException("Missing sealed legacy fixture: ${fixture}") + } + MessageDigest digest = MessageDigest.getInstance('SHA-256') + fixture.withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + String actualHash = digest.digest().encodeHex().toString().toUpperCase() + if (actualHash != expectedHash) { + throw new GradleException("Legacy fixture hash mismatch for ${relativePath}: " + + "expected ${expectedHash}, found ${actualHash}") + } + } + } +} + +dependencies { + implementation minecraft.dependency( + "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}") + compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +tasks.named('compileTestJava', JavaCompile) { + dependsOn verifyLegacyFixtures +} +tasks.named('test', Test) { + dependsOn verifyLegacyFixtures + systemProperty 'orespawn.mineralogy110Oracle', legacyMineralogy110OracleJar.absolutePath + systemProperty 'orespawn.mineralogy111Oracle', legacyMineralogy111OracleJar.absolutePath + systemProperty 'orespawn.mineralogy112Oracle', legacyMineralogy112OracleJar.absolutePath +} + +tasks.register('verifyLegacyOracleIsolation') { + group = 'verification' + description = 'Keeps the sealed Mineralogy oracle test-visible but production-invisible.' + dependsOn verifyLegacyFixtures + + doLast { + Set oracles = [legacyMineralogy110OracleJar.canonicalFile, + legacyMineralogy111OracleJar.canonicalFile, + legacyMineralogy112OracleJar.canonicalFile, + legacyOreSpawn111Jar.canonicalFile] as Set + def canonicalFiles = { FileCollection classpath -> + classpath.files.collect { it.canonicalFile } as Set + } + Map productionClasspaths = [ + mainCompile: sourceSets.main.compileClasspath, + mainRuntime: sourceSets.main.runtimeClasspath, + testCompile: sourceSets.test.compileClasspath, + testRuntime: sourceSets.test.runtimeClasspath + ] + productionClasspaths.each { String name, FileCollection classpath -> + Set leaked = canonicalFiles(classpath).intersect(oracles) + if (!leaked.isEmpty()) { + throw new GradleException("The sealed Mineralogy oracles leaked into ${name}: ${leaked}") + } + } + } +} +tasks.named('check') { + dependsOn tasks.named('verifyLegacyOracleIsolation') +} + +def java8Launcher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + vendor = JvmVendorSpec.ADOPTIUM +} + +tasks.register('verifyJava8Toolchain') { + group = 'verification' + description = 'Requires the checksum-pinned Temurin 8.0.502+7 release toolchain.' + doLast { + def metadata = java8Launcher.get().metadata + if (project.java_toolchain_version != '8.0.502+7' + || metadata.vendor.toString() != 'Eclipse Temurin' + || metadata.javaRuntimeVersion != '1.8.0_502-b07') { + throw new GradleException("Expected Temurin ${project.java_toolchain_version}, found " + + "${metadata.vendor} ${metadata.javaRuntimeVersion} at ${metadata.installationPath}") + } + } +} +tasks.named('check') { + dependsOn tasks.named('verifyJava8Toolchain') +} +def configureFromForgeRun = { JavaExec process, String runTaskName -> + process.actions.clear() + process.dependsOn { + JavaExec run = tasks.getByName(runTaskName) as JavaExec + run.taskDependencies.getDependencies(run) + } + process.doLast { + JavaExec run = tasks.getByName(runTaskName) as JavaExec + String runConfigurationName = runTaskName.substring(3).uncapitalize() + def runOptions = minecraft.runs.getByName(runConfigurationName) + File originalConfiguredWorkingDir = runOptions.workingDir.get().asFile + File originalWorkingDir = run.workingDir + List originalArgs = new ArrayList<>(run.args) + List originalJvmArgs = new ArrayList<>(run.jvmArgs) + Map originalSystemProperties = new LinkedHashMap<>(run.systemProperties) + Map originalEnvironment = new LinkedHashMap<>(run.environment) + FileCollection originalClasspath = files(run.classpath.files) + String originalMinHeap = run.minHeapSize + String originalMaxHeap = run.maxHeapSize + String originalMainClass = run.mainClass.orNull + try { + File forgeConfig = new File(process.workingDir, 'config/forge.cfg') + if (!forgeConfig.isFile()) { + forgeConfig.parentFile.mkdirs() + forgeConfig.setText('''general { + B:disableVersionCheck=true +} + +version_checking { + B:Global=false +} +''', 'UTF-8') + } + runOptions.workingDir.set(process.workingDir) + run.workingDir(process.workingDir) + // Preserve the target run's bare nogui argument while adding any + // fixture-specific arguments. Replacing the argument list here + // caused every automated Forge 1.11 server to open its Swing GUI. + List effectiveArgs = new ArrayList<>(originalArgs) + effectiveArgs.addAll(process.args) + run.setArgs(effectiveArgs) + run.jvmArgs(process.jvmArgs) + run.systemProperties(process.systemProperties) + run.environment(process.environment) + Set splitMainOutputs = [ + tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile, + sourceSets.main.output.resourcesDir + ] as Set + run.setClasspath(files(forge11DevelopmentOutput, run.classpath.filter { + !splitMainOutputs.contains(it) + })) + run.exec() + } finally { + runOptions.workingDir.set(originalConfiguredWorkingDir) + run.workingDir(originalWorkingDir) + run.setArgs(originalArgs) + run.setJvmArgs(originalJvmArgs) + run.setSystemProperties(originalSystemProperties) + run.setEnvironment(originalEnvironment) + run.setClasspath(originalClasspath) + run.minHeapSize = originalMinHeap + run.maxHeapSize = originalMaxHeap + if (originalMainClass == null) { + run.mainClass.unset() + } else { + run.mainClass.set(originalMainClass) + } + } + } +} + +// Runtime processes are not green merely because they exit with code zero. +// Forge 13 can log a fatal callback or worldgen failure and still stop cleanly. +def acceptedForge13LogNoise = [ + ~/Apache Maven library folder was not in the format expected/, + ~/\[FML\]: Full: .*maven-artifact-/, + ~/\[FML\]: Trimmed: .*maven-artifact/, + ~/FML appears to be missing any signature data/, + ~/The binary patch set is missing\. Either you are in a development environment, or things are not going to work!/, + ~/Unable to read a class file correctly/, + ~/There was a problem reading the entry (?:META-INF\/versions\/9\/)?module-info\.class .*probably a corrupt zip/, + ~/There was a problem reading the entry META-INF\/versions\/11\/net\/minecraftforge\/launcher\/shadow\/util\/download\/DownloadUtilsImpl\.class in the jar .*slime-launcher-0\.2\.2\.jar - probably a corrupt zip/ +] + +def knownMinecraft112OnlyRegistryNames = [ + 'black_glazed_terracotta', 'blue_glazed_terracotta', 'brown_glazed_terracotta', + 'concrete', 'concrete_powder', 'cyan_glazed_terracotta', 'gray_glazed_terracotta', + 'green_glazed_terracotta', 'knowledge_book', 'light_blue_glazed_terracotta', + 'lime_glazed_terracotta', 'magenta_glazed_terracotta', 'orange_glazed_terracotta', + 'pink_glazed_terracotta', 'purple_glazed_terracotta', 'red_glazed_terracotta', + 'silver_glazed_terracotta', 'white_glazed_terracotta', 'yellow_glazed_terracotta' +] as Set + +def assertRuntimeLogsClean = { File runDirectory, String context, + boolean allowMinecraft112DowngradeMappings = false -> + File crashDirectory = new File(runDirectory, 'crash-reports') + if (crashDirectory.isDirectory()) { + List crashes = fileTree(crashDirectory) { include '**/*' }.files.findAll { it.isFile() } + if (!crashes.isEmpty()) { + throw new GradleException("${context} produced crash report ${crashes.first()}") + } + } + + Set runtimeLogs = [] as Set + File logsDirectory = new File(runDirectory, 'logs') + if (logsDirectory.isDirectory()) { + runtimeLogs.addAll(fileTree(logsDirectory) { + include '**/*.log' + include '**/*.txt' + }.files) + } + runtimeLogs.addAll(fileTree(runDirectory) { include '*-console.txt' }.files) + + List failures = [] + runtimeLogs.each { File log -> + String completeLog = log.getText('UTF-8') + boolean cleanroomWindowsEpollFallback = completeLog.contains( + '[io.netty.channel.epoll.Epoll]: Epoll support is not available') + && completeLog.contains('Caused by: java.lang.IllegalStateException: Only supported on Linux') + && completeLog.contains('[net.minecraft.network.NetworkSystem]: Using default channel type') + boolean legacySlimeLauncherScanNoise = completeLog.contains('Unable to read a class file correctly') + && completeLog.contains('slime-launcher-0.2.2.jar') + && completeLog.contains('java.lang.IllegalArgumentException') + int lineNumber = 0 + log.eachLine('UTF-8') { String line -> + lineNumber++ + boolean unexpectedSeverity = (line ==~ /.*\/(?:ERROR|FATAL)\].*/ + || line ==~ /.*\s(?:ERROR|FATAL)\s.*/) + def downgradeMappingMatch = line =~ /.*\[FML\/?\]: Unidentified (?:block|item): minecraft:([a-z0-9_]+), id [0-9]+$/ + boolean knownMinecraft112DowngradeMapping = allowMinecraft112DowngradeMappings + && ((line.contains('[FML]: There are unidentified mappings in this world - we are going to attempt to process anyway') + || line.contains('[FML/]: There are unidentified mappings in this world - we are going to attempt to process anyway')) + || (downgradeMappingMatch.matches() + && knownMinecraft112OnlyRegistryNames.contains(downgradeMappingMatch.group(1)))) + boolean unsignedSourceBuiltMineralogy = allowMinecraft112DowngradeMappings + && line.contains('[mineralogy') + && line.contains('The mod mineralogy is expecting signature @FINGERPRINT@') + && line.contains('however there is no signature matching that description') + boolean knownNoise = acceptedForge13LogNoise.any { line =~ it } + || knownMinecraft112DowngradeMapping + || unsignedSourceBuiltMineralogy + || (legacySlimeLauncherScanNoise + && line.contains('There was a problem reading the entry') + && line.contains('slime-launcher-0.2.2.jar') + && line.contains('probably a corrupt zip')) + || (line.trim() == 'java.lang.ExceptionInInitializerError' + && cleanroomWindowsEpollFallback) + boolean oreSpawnCascadingLoad = line.contains('cascading worldgen lag') + && line.contains('OreSpawn loaded a new chunk') + boolean fatalText = line.contains('Encountered an unexpected exception') + || line.contains('Exception stopping the server') + || line.contains('Migration audit failed') + || line.contains('java.lang.Error:') + || line.contains('IllegalAccessError') + || line.contains('NoSuchFieldError') + || line.contains('LinkageError') + || line.contains('NoSuchMethodError') + || line.contains('NoClassDefFoundError') + || line.contains('ExceptionInInitializerError') + || line.contains('Tried to assign a mutable BlockPos') + || oreSpawnCascadingLoad + if (!knownNoise && (unexpectedSeverity || fatalText)) { + failures.add("${log.name}:${lineNumber}: ${line}") + } + } + } + if (!failures.isEmpty()) { + throw new GradleException("${context} logged unexpected errors:\n" + + failures.take(20).join('\n')) + } +} + +tasks.register('verifyExternalRuntimeLogs') { + group = 'verification' + description = 'Applies the release log/crash scanner to a disposable packaged runtime.' + doLast { + if (!project.hasProperty('runtimeLogDirectory')) { + throw new GradleException('runtimeLogDirectory is required') + } + File runtime = file(project.property('runtimeLogDirectory')) + if (!runtime.isDirectory()) { + throw new GradleException("Runtime log directory does not exist: ${runtime}") + } + assertRuntimeLogsClean(runtime, "external packaged runtime ${runtime.name}") + } +} + +tasks.register('runtimeLogScannerTest') { + group = 'verification' + description = 'Proves Forge 13 log validation accepts known noise and rejects runtime failures.' + doLast { + File probe = file("${buildDir}/runtime-log-scanner-test") + delete probe + File logs = new File(probe, 'logs') + logs.mkdirs() + new File(logs, 'latest.log').setText( + '[main/ERROR] [FML]: The binary patch set is missing. Either you are in a development environment, or things are not going to work!\n' + + '[main/ERROR] [FML]: FML appears to be missing any signature data. This is not a good thing\n' + + '[Server thread/ERROR] [FML]: Unable to read a class file correctly\n' + + 'java.lang.IllegalArgumentException\n' + + '[Server thread/ERROR] [FML]: There was a problem reading the entry C:/cache/slime-launcher-0.2.2.jar in the jar net.minecraftforge.fml.common.LoaderException: java.lang.IllegalArgumentException - probably a corrupt zip\n' + + '[Server thread/ERROR] [FML]: There was a problem reading the entry META-INF/versions/11/net/minecraftforge/launcher/shadow/util/download/DownloadUtilsImpl.class in the jar C:/cache/slime-launcher-0.2.2.jar - probably a corrupt zip\n' + + '[Server thread/INFO] [FML]: Done\n', 'UTF-8') + assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe') + new File(logs, 'latest.log').setText( + '[Server thread/DEBUG] [io.netty.channel.epoll.Epoll]: Epoll support is not available\n' + + 'java.lang.ExceptionInInitializerError\n' + + 'Caused by: java.lang.IllegalStateException: Only supported on Linux\n' + + '[Server thread/INFO] [net.minecraft.network.NetworkSystem]: Using default channel type\n', + 'UTF-8') + assertRuntimeLogsClean(probe, 'scanner-cleanroom-windows-epoll-probe') + new File(logs, 'latest.log').setText('java.lang.ExceptionInInitializerError\n', 'UTF-8') + boolean rejected = false + try { assertRuntimeLogsClean(probe, 'scanner-unaccounted-initializer-probe') } + catch (GradleException expected) { rejected = true } + if (!rejected) throw new GradleException('Runtime scanner accepted an unaccounted initializer failure') + new File(logs, 'latest.log').setText( + '[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8') + rejected = false + try { assertRuntimeLogsClean(probe, 'scanner-rejection-probe') } + catch (GradleException expected) { rejected = true } + if (!rejected) throw new GradleException('Runtime scanner accepted a mutable BlockPos leak') + new File(logs, 'latest.log').setText( + '[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8') + rejected = false + try { assertRuntimeLogsClean(probe, 'scanner-error-severity-probe') } + catch (GradleException expected) { rejected = true } + if (!rejected) throw new GradleException('Runtime scanner accepted an unexpected ERROR line') + new File(logs, 'latest.log').setText( + '[Server thread/FATAL] [FML]: There are unidentified mappings in this world - we are going to attempt to process anyway\n' + + '[Server thread/FATAL] [FML]: Unidentified block: minecraft:concrete, id 251\n' + + '[Server thread/FATAL] [FML]: Unidentified item: minecraft:knowledge_book, id 453\n', 'UTF-8') + assertRuntimeLogsClean(probe, 'scanner-expected-1.12-downgrade-probe', true) + rejected = false + try { assertRuntimeLogsClean(probe, 'scanner-unscoped-1.12-downgrade-probe') } + catch (GradleException expected) { rejected = true } + if (!rejected) throw new GradleException('Runtime scanner accepted downgrade mappings outside the copied corpus gate') + new File(logs, 'latest.log').setText( + '[Server thread/FATAL] [FML]: Unidentified block: example:unknown, id 999\n', 'UTF-8') + rejected = false + try { assertRuntimeLogsClean(probe, 'scanner-unknown-downgrade-probe', true) } + catch (GradleException expected) { rejected = true } + if (!rejected) throw new GradleException('Runtime scanner accepted an unknown mapping in the downgrade corpus') + delete probe + } +} + +tasks.named('check') { + dependsOn tasks.named('runtimeLogScannerTest') +} + +def surfaceIntegrationClasses = file("${buildDir}/surface-integration-fixture/classes") +task compileSurfaceIntegrationTestMod(type: JavaCompile, dependsOn: classes) { + source fileTree('src/biomeIntegrationTest/java') + classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) + destinationDirectory = surfaceIntegrationClasses + options.encoding = 'UTF-8' +} + +task surfaceIntegrationTestModJar(type: Jar, dependsOn: compileSurfaceIntegrationTestMod) { + archiveFileName = 'surfaceprobe.jar' + destinationDirectory = file("${buildDir}/surface-integration-fixture") + from surfaceIntegrationClasses + from 'src/biomeIntegrationTest/resources' +} + +// Keep the mapped development fixture separate from the copy transformed for +// Forge's real packaged-mod discovery. +def packagedSurfaceIntegrationTestModJar = renamer.classes( + tasks.named('surfaceIntegrationTestModJar', Jar)) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('surface-integration-fixture/surfaceprobe-reobf.jar') +} + +def surfaceIntegrationRunDirectory = file("${buildDir}/surface-integration-run") +task prepareSurfaceIntegrationTest(dependsOn: surfaceIntegrationTestModJar) { + doLast { + delete surfaceIntegrationRunDirectory + surfaceIntegrationRunDirectory.mkdirs() + copy { + from surfaceIntegrationTestModJar.archiveFile + into new File(surfaceIntegrationRunDirectory, 'mods') + } + new File(surfaceIntegrationRunDirectory, 'server.properties').setText('''\ +level-name=surface-integration-world +level-seed=zsjpxah +level-type=default +online-mode=false +server-port=0 +allow-nether=true +generate-structures=false +spawn-protection=0 +max-tick-time=-1 +''', 'UTF-8') + new File(surfaceIntegrationRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } +} + +def createSurfaceProcess = { String phase, Object dependency -> + task("surfaceIntegration${phase}Process", type: JavaExec, dependsOn: dependency) { + group = 'verification' + workingDir surfaceIntegrationRunDirectory + systemProperty 'surfaceprobe.integrationPhase', phase.toLowerCase() + configureFromForgeRun(delegate, 'runServer') + } +} + +def surfaceIntegrationFreshProcess = createSurfaceProcess('Fresh', prepareSurfaceIntegrationTest) +surfaceIntegrationFreshProcess.doLast { + File marker = new File(surfaceIntegrationRunDirectory, + 'surface-integration-world/surfaceprobe-integration.properties') + if (!marker.isFile()) { + throw new GradleException("Fresh surface integration completion marker is missing: ${marker}") + } + assertRuntimeLogsClean(surfaceIntegrationRunDirectory, 'surface integration fresh phase') +} +def surfaceIntegrationReloadProcess = createSurfaceProcess('Reload', surfaceIntegrationFreshProcess) +surfaceIntegrationReloadProcess.doLast { + assertRuntimeLogsClean(surfaceIntegrationRunDirectory, 'surface integration reload phase') +} + +task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) { + group = 'verification' + doLast { + File marker = new File(surfaceIntegrationRunDirectory, + 'surface-integration-world/surfaceprobe-integration.properties') + Properties result = new Properties() + marker.withInputStream { result.load(it) } + if (result.getProperty('reload_verified') != 'true') { + throw new GradleException("Surface integration reload was not verified: ${marker}") + } + if (!(result.getProperty('dynamic_fluid_placements') ?: '0').isInteger() + || result.getProperty('dynamic_fluid_placements').toInteger() <= 0) { + throw new GradleException("Dynamic fluid-deposit probe did not place any blocks: ${marker}") + } + logger.lifecycle('Provider surfaces and exact-biome geology verified: {} dimensions, {} columns each, fresh + reload', + result.getProperty('dimensions'), result.getProperty('columns_per_dimension')) + } +} + +check.dependsOn surfaceIntegrationTest + +def migrationIntegrationClasses = file("${buildDir}/migration-integration-fixture/classes") +task compileMigrationIntegrationTestMod(type: JavaCompile, dependsOn: classes) { + source fileTree('src/migrationIntegrationTest/java') + classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) + destinationDirectory = migrationIntegrationClasses + options.encoding = 'UTF-8' +} + +task migrationIntegrationTestModJar(type: Jar, dependsOn: compileMigrationIntegrationTestMod) { + archiveFileName = 'migrationprobe.jar' + destinationDirectory = file("${buildDir}/migration-integration-fixture") + from migrationIntegrationClasses + from 'src/migrationIntegrationTest/resources' +} + +def baseMetals111FixtureClasses = file("${buildDir}/basemetals-111-fixture/classes") +task compileBaseMetals111Fixture(type: JavaCompile, dependsOn: verifyLegacyFixtures) { + source fileTree('src/baseMetals111Fixture/java') + classpath = files(sourceSets.main.compileClasspath, legacyOreSpawn111Jar) + destinationDirectory = baseMetals111FixtureClasses + options.encoding = 'UTF-8' +} + +task baseMetals111FixtureJar(type: Jar, dependsOn: compileBaseMetals111Fixture) { + archiveFileName = 'basemetals-111-provider-fixture.jar' + destinationDirectory = file("${buildDir}/basemetals-111-fixture") + from baseMetals111FixtureClasses + from 'src/baseMetals111Fixture/resources' +} + +def baseMetals111RunDirectory = file("${buildDir}/basemetals-111-provider-run") +task prepareBaseMetals111ProviderRun(dependsOn: [migrationIntegrationTestModJar, + baseMetals111FixtureJar]) { + doLast { + delete baseMetals111RunDirectory + File mods = new File(baseMetals111RunDirectory, 'mods') + copy { + from migrationIntegrationTestModJar.archiveFile + from baseMetals111FixtureJar.archiveFile + into mods + } + new File(baseMetals111RunDirectory, 'server.properties').setText('''\ +level-name=world +level-seed=zsjpxah +level-type=default +online-mode=false +server-port=0 +allow-nether=true +generate-structures=false +spawn-protection=0 +max-tick-time=-1 +spawn-animals=false +spawn-monsters=false +''', 'UTF-8') + new File(baseMetals111RunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } +} + +def createBaseMetals111Process = { String phase, Object dependency -> + tasks.create("baseMetals111Provider${phase.capitalize()}", JavaExec) { + group = 'verification' + dependsOn dependency + workingDir baseMetals111RunDirectory + systemProperty 'orespawn.migrationFamily', 'basemetals-fresh-install' + systemProperty 'orespawn.migrationPhase', phase + configureFromForgeRun(delegate, 'runServer') + doLast { + File marker = new File(baseMetals111RunDirectory, + 'world/orespawn4-migration-probe.properties') + if (!marker.isFile()) { + throw new GradleException("Base Metals 1.11 ${phase} marker is missing") + } + Properties values = new Properties() + marker.withInputStream { values.load(it) } + if (values.getProperty("${phase}_complete") != 'true') { + throw new GradleException("Base Metals 1.11 ${phase} did not complete") + } + assertRuntimeLogsClean(baseMetals111RunDirectory, + "Base Metals 1.11 ${phase} provider phase") + } + } +} + +def baseMetals111Fresh = createBaseMetals111Process('fresh', prepareBaseMetals111ProviderRun) +def baseMetals111Reload = createBaseMetals111Process('reload', baseMetals111Fresh) +task baseMetals111ProviderTest(dependsOn: baseMetals111Reload) { + group = 'verification' + description = 'Qualifies the historical Forge 1.11 Base Metals OS3 plugin and embedded provider.' +} +check.dependsOn baseMetals111ProviderTest + +if (project.hasProperty('migrationRunDir')) { + def migrationRunDirectory = file(project.property('migrationRunDir')) + task prepareMigrationIntegrationRun(dependsOn: migrationIntegrationTestModJar) { + doLast { + copy { from migrationIntegrationTestModJar.archiveFile; into new File(migrationRunDirectory, 'mods') } + new File(migrationRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + File serverProperties = new File(migrationRunDirectory, 'server.properties') + Properties values = new Properties() + if (serverProperties.isFile()) serverProperties.withInputStream { values.load(it) } + values.setProperty('spawn-animals', 'false') + values.setProperty('spawn-monsters', 'false') + serverProperties.withOutputStream { values.store(it, 'OreSpawn migration qualification') } + } + } + task migrationIntegrationProcess(type: JavaExec, dependsOn: prepareMigrationIntegrationRun) { + group = 'verification' + workingDir migrationRunDirectory + systemProperty 'orespawn.migrationFamily', project.findProperty('migrationFamily') ?: 'unspecified' + systemProperty 'orespawn.migrationPhase', project.findProperty('migrationPhase') ?: 'fresh' + configureFromForgeRun(delegate, 'runServer') + doLast { + String phase = project.findProperty('migrationPhase') ?: 'fresh' + File marker = new File(migrationRunDirectory, 'world/orespawn4-migration-probe.properties') + if (!marker.isFile()) throw new GradleException("Migration probe did not produce ${marker}") + Properties values = new Properties(); marker.withInputStream { values.load(it) } + if (values.getProperty("${phase}_complete") != 'true') { + throw new GradleException("Migration ${phase} phase did not complete: ${marker}") + } + File latest = new File(migrationRunDirectory, 'logs/latest.log') + if (latest.isFile() && (latest.text.contains('Migration audit failed') + || latest.text.contains('Encountered an unexpected exception'))) { + throw new GradleException("Migration ${phase} phase logged a server failure: ${latest}") + } + } + } +} + +def legacyMineralogyMigrationRunDirectory = file("${buildDir}/legacy-mineralogy-migration-run") +def legacyMineralogyMigrationArchive = new File(legacyFixtureWorlds, + 'sylvester-era-trio-source-v2.zip') + +task prepareLegacyMineralogyMigrationRun(dependsOn: [migrationIntegrationTestModJar, + verifyLegacyFixtures]) { + doLast { + delete legacyMineralogyMigrationRunDirectory + copy { from zipTree(legacyMineralogyMigrationArchive); into legacyMineralogyMigrationRunDirectory } + File mods = new File(legacyMineralogyMigrationRunDirectory, 'mods') + mods.mkdirs() + copy { + from new File(legacyFixtureArtifacts, 'BaseMetals_1.10.2-2.4.0.11.jar') + from new File(legacyFixtureArtifacts, 'Mineralogy-1.10.2-3.3.8.26.jar') + from migrationIntegrationTestModJar.archiveFile + into mods + } + new File(legacyMineralogyMigrationRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } +} + +def configureLegacyMineralogyMigrationProcess = { JavaExec process, String phase -> + process.group = 'verification' + process.workingDir legacyMineralogyMigrationRunDirectory + process.systemProperty 'orespawn.migrationFamily', 'legacy-mineralogy-cyano' + process.systemProperty 'orespawn.migrationPhase', phase + // The sealed corpus was last saved by 1.12.2. Forge 1.11 correctly asks + // before discarding only vanilla 1.12 additions from this disposable copy. + process.systemProperty 'fml.queryResult', 'confirm' + configureFromForgeRun(process, 'runServer') + process.doLast { + File marker = new File(legacyMineralogyMigrationRunDirectory, + 'world/orespawn4-migration-probe.properties') + if (!marker.isFile()) throw new GradleException("Legacy Mineralogy migration did not produce ${marker}") + Properties values = new Properties(); marker.withInputStream { values.load(it) } + if (values.getProperty("${phase}_complete") != 'true') { + throw new GradleException("Legacy Mineralogy migration ${phase} phase did not complete") + } + } +} + +task legacyMineralogyMigrationFresh(type: JavaExec, dependsOn: prepareLegacyMineralogyMigrationRun) +configureLegacyMineralogyMigrationProcess(legacyMineralogyMigrationFresh, 'fresh') + +task legacyMineralogyMigrationReload(type: JavaExec, dependsOn: legacyMineralogyMigrationFresh) +configureLegacyMineralogyMigrationProcess(legacyMineralogyMigrationReload, 'reload') + +task legacyMineralogyMigrationTest(dependsOn: legacyMineralogyMigrationReload) { + group = 'verification' + description = 'Proves an existing Mineralogy 3 world is pinned to Cyano settings across reload.' + doLast { + File marker = new File(legacyMineralogyMigrationRunDirectory, + 'world/orespawn4-migration-probe.properties') + Properties values = new Properties(); marker.withInputStream { values.load(it) } + ['fresh_complete', 'reload_complete', 'legacy_mineralogy_config_sha256', + 'legacy_mineralogy_world_profile_sha256'].each { key -> + if (!values.getProperty(key)) throw new GradleException("Missing legacy Mineralogy evidence ${key}") + } + logger.lifecycle('Existing Mineralogy 3 world retained Cyano profile and exact config/profile hashes across reload') + } +} + +// The Forge 1.10 migration task above is retained only as source-history +// reference. Forge 1.11 runs the target-native lineage gates below. + +def os1AbiFixtureClasses = file("${buildDir}/legacy-abi/os1/classes") +def os3AbiFixtureClasses = file("${buildDir}/legacy-abi/os3/classes") + +task compileOs1AbiFixture(type: JavaCompile, dependsOn: verifyLegacyFixtures) { + source fileTree('src/os1AbiFixture/java') + classpath = files(sourceSets.main.compileClasspath, + new File(legacyFixtureArtifacts, 'OreSpawn_1.10.2-1.1.0.jar')) + destinationDirectory = os1AbiFixtureClasses + options.encoding = 'UTF-8' +} + +task os1AbiFixtureJar(type: Jar, dependsOn: compileOs1AbiFixture) { + archiveFileName = 'os1abiprobe.jar' + destinationDirectory = file("${buildDir}/legacy-abi/os1") + from os1AbiFixtureClasses + from 'src/os1AbiFixture/resources' +} + +task compileOs3AbiFixture(type: JavaCompile, dependsOn: verifyLegacyFixtures) { + source fileTree('src/os3AbiFixture/java') + classpath = files(sourceSets.main.compileClasspath, + legacyOreSpawn111Jar) + destinationDirectory = os3AbiFixtureClasses + options.encoding = 'UTF-8' +} + +task os3AbiFixtureJar(type: Jar, dependsOn: compileOs3AbiFixture) { + archiveFileName = 'os3abiprobe.jar' + destinationDirectory = file("${buildDir}/legacy-abi/os3") + from os3AbiFixtureClasses + from 'src/os3AbiFixture/resources' +} + +def configureLegacyAbiProcess = { String generation, Task fixtureJar, String markerName -> + File runDirectory = file("${buildDir}/legacy-abi/${generation}/run") + Task prepare = tasks.create("prepare${generation.capitalize()}AbiRun", Copy) { + dependsOn fixtureJar + into new File(runDirectory, 'mods') + from fixtureJar.archiveFile + doFirst { project.delete(runDirectory) } + doLast { + new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + new File(runDirectory, 'server.properties').setText( + 'eula=true\nonline-mode=false\nlevel-name=world\nlevel-seed=zsjpxah\n' + + 'spawn-animals=false\nspawn-monsters=false\n', 'UTF-8') + } + } + tasks.create("${generation}AbiIntegrationProcess", JavaExec) { + group = 'verification' + dependsOn prepare + workingDir runDirectory + configureFromForgeRun(delegate, 'runServer') + doLast { + File marker = new File(runDirectory, "world/${markerName}") + if (!marker.isFile() || !marker.text.contains('registered=true')) { + throw new GradleException("${generation.toUpperCase()} binary ABI probe did not complete: ${marker}") + } + File latest = new File(runDirectory, 'logs/latest.log') + if (latest.isFile() && latest.text.contains('Encountered an unexpected exception')) { + throw new GradleException("${generation.toUpperCase()} binary ABI probe logged a server failure") + } + } + } +} + +configureLegacyAbiProcess('os1', os1AbiFixtureJar, 'os1-abi-probe.properties') +configureLegacyAbiProcess('os3', os3AbiFixtureJar, 'os3-abi-probe.properties') + +task legacyAbiIntegrationTest { + group = 'verification' + dependsOn os1AbiIntegrationProcess, os3AbiIntegrationProcess +} + +// Forge 1.11's native OS3 ABI fixture remains isolated from the ordinary +// production classpath and is exercised only by the explicit compatibility gate. + +def legacyMineralogyArchive = new File(legacyFixtureWorlds, 'os3-331-default-source.zip') +def legacyMineralogyJar = legacyMineralogy111OracleJar +def legacyMineralogyGates = [] +[ + '110': [version: '3.3.8.26', config: '''\ +world-gen { + I:GEOME_SIZE=144 + B:REALISTIC_COAL_LAYERS=true + S:ROCK_LAYER_NOISE=41.5 + I:ROCK_LAYER_THICKNESS=11 +} +'''], + '111': [version: '3.3.0', config: '''\ +world-gen { + I:GEOME_SIZE=100 + S:ROCK_LAYER_NOISE=32 + I:ROCK_LAYER_THICKNESS=8 +} +'''], + '112': [version: '3.8.0.53', config: '''\ +world-gen { + B:PLACE_MINERALOGY_ROCK=false + I:GEOME_SIZE=128 + S:ROCK_LAYER_NOISE=37.25 + I:ROCK_LAYER_THICKNESS=9 +} +'''] +].each { String lineage, Map fixture -> + String label = lineage + File runDirectory = file("${buildDir}/legacy-mineralogy-${lineage}-run") + Task prepareTask = tasks.create("prepareLegacyMineralogy${label}Run") { + dependsOn migrationIntegrationTestModJar, verifyLegacyFixtures + doLast { + delete runDirectory + copy { from zipTree(legacyMineralogyArchive); into runDirectory } + File mods = new File(runDirectory, 'mods') + mods.mkdirs() + copy { from migrationIntegrationTestModJar.archiveFile; from legacyMineralogyJar; into mods } + File config = new File(runDirectory, 'config/mineralogy.cfg') + config.parentFile.mkdirs() + config.setText(fixture.config as String, 'UTF-8') + new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } + } + Task seedMetadataTask = tasks.create("seedLegacyMineralogy${label}Metadata", JavaExec) { + group = 'verification' + dependsOn prepareTask + javaLauncher = java8Launcher + mainClass = 'zone.moddev.mc.orespawn.migrationtest.LegacyMineralogyMetadataFixture' + classpath = files(migrationIntegrationClasses, sourceSets.main.runtimeClasspath) + args new File(runDirectory, 'world').absolutePath, fixture.version + } + + def createLegacyMineralogyProcess = { String phase, Object dependency -> + Task process = tasks.create("legacyMineralogy${label}${phase.capitalize()}", JavaExec) { + group = 'verification' + dependsOn dependency + workingDir runDirectory + systemProperty 'forge.logging.console.level', 'info' + systemProperty 'orespawn.migrationFamily', "legacy-mineralogy-${lineage}" + systemProperty 'orespawn.migrationPhase', phase + systemProperty 'fml.queryResult', 'confirm' + configureFromForgeRun(delegate, 'runServer') + doLast { + File marker = new File(runDirectory, 'world/orespawn4-migration-probe.properties') + if (!marker.isFile()) { + throw new GradleException("Legacy Mineralogy ${lineage} ${phase} marker is missing") + } + Properties values = new Properties() + marker.withInputStream { values.load(it) } + if (values.getProperty("${phase}_complete") != 'true') { + throw new GradleException("Legacy Mineralogy ${lineage} ${phase} did not complete") + } + assertRuntimeLogsClean(runDirectory, + "legacy Mineralogy ${lineage} ${phase} phase", true) + } + } + process + } + + Task freshTask = createLegacyMineralogyProcess('fresh', seedMetadataTask) + Task reloadTask = createLegacyMineralogyProcess('reload', freshTask) + Task gate = tasks.create("legacyMineralogy${label}MigrationTest") { + group = 'verification' + description = "Proves Mineralogy ${lineage} settings remain exact across upgrade and reload." + dependsOn reloadTask + } + legacyMineralogyGates.add(gate) +} + +tasks.named('check') { + dependsOn legacyMineralogyGates +} + +def clientIntegrationClasses = file("${buildDir}/client-integration-fixture/classes") +task compileClientIntegrationTestMod(type: JavaCompile, dependsOn: classes) { + source fileTree('src/clientIntegrationTest/java') + classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) + destinationDirectory = clientIntegrationClasses + options.encoding = 'UTF-8' +} + +task clientIntegrationTestModJar(type: Jar, dependsOn: compileClientIntegrationTestMod) { + archiveFileName = 'clientprobe.jar' + destinationDirectory = file("${buildDir}/client-integration-fixture") + from clientIntegrationClasses + from 'src/clientIntegrationTest/resources' +} + +def packagedClientProbeJar = renamer.classes(tasks.named('clientIntegrationTestModJar', Jar)) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('client-integration-fixture/clientprobe-reobf.jar') +} + +tasks.register('preparePackagedClientProbe') { + group = 'verification' + description = 'Builds the reobfuscated client probe used only with a disposable packaged Forge runtime.' + dependsOn packagedClientProbeJar +} + +def clientIntegrationRunDirectory = file("${buildDir}/client-integration-run") +task prepareClientIntegrationTest(dependsOn: clientIntegrationTestModJar) { + doLast { + delete clientIntegrationRunDirectory + clientIntegrationRunDirectory.mkdirs() + copy { + from clientIntegrationTestModJar.archiveFile + into new File(clientIntegrationRunDirectory, 'mods') + } + new File(clientIntegrationRunDirectory, 'options.txt').setText( + 'fullscreen:false\nforceUnicodeFont:false\nguiScale:2\n' + + 'renderDistance:4\nshowSubtitles:false\n', 'UTF-8') + } +} + +task clientIntegrationProcess(type: JavaExec, dependsOn: prepareClientIntegrationTest) { + group = 'verification' + workingDir clientIntegrationRunDirectory + systemProperty 'clientprobe.enabled', 'true' + configureFromForgeRun(delegate, 'runClient') + doLast { + File marker = new File(clientIntegrationRunDirectory, 'client-smoke-pass.properties') + if (!marker.isFile()) { + throw new GradleException("Client integration completion marker is missing: ${marker}") + } + Properties result = new Properties(); marker.withInputStream { result.load(it) } + if (result.getProperty('reload_rendered') != 'true' + || result.getProperty('world_settings_opened') != 'true' + || result.getProperty('long_editor_roundtrip') != 'true' + || Integer.parseInt(result.getProperty('editor_routes', '0')) < 5) { + throw new GradleException("Client integration result is incomplete: ${marker}") + } + assertRuntimeLogsClean(clientIntegrationRunDirectory, 'client integration fresh/reload phases') + } +} + +task syncForge11EclipseIntegrationLaunches { + group = 'ide' + doLast { + def writeGradleLaunch = { String fileName, String displayName, String arguments -> + File launch = file(fileName) + String escapedArguments = arguments + .replace('&', '&') + .replace('"', '"') + launch.setText(""" + + + + + + + + +""", 'UTF-8') + } + + writeGradleLaunch('OreSpawn_Surface_FreshReload.launch', + 'OreSpawn surface fresh/reload gate', + 'surfaceIntegrationTest --offline --no-daemon') + writeGradleLaunch('OreSpawn_Migration_Fresh.launch', + 'OreSpawn migration fresh gate', + 'migrationIntegrationProcess --offline --no-daemon ' + + '-PmigrationRunDir=build/eclipse-migration-run ' + + '-PmigrationFamily=eclipse-manual -PmigrationPhase=fresh') + writeGradleLaunch('OreSpawn_Migration_Reload.launch', + 'OreSpawn migration reload gate', + 'migrationIntegrationProcess --offline --no-daemon ' + + '-PmigrationRunDir=build/eclipse-migration-run ' + + '-PmigrationFamily=eclipse-manual -PmigrationPhase=reload') + writeGradleLaunch('OreSpawn_Client_FreshReload.launch', + 'OreSpawn client world and editor fresh/reload gate', + 'clientIntegrationProcess --offline --no-daemon') + } +} + +tasks.named('genEclipseRuns') { + finalizedBy syncForge11EclipseIntegrationLaunches +} + +if (project.hasProperty('benchmarkRunDir')) { + def benchmarkRunDirectory = file(project.property('benchmarkRunDir')) + task prepareWorldgenBenchmark { + doLast { + delete benchmarkRunDirectory + benchmarkRunDirectory.mkdirs() + new File(benchmarkRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + new File(benchmarkRunDirectory, 'server.properties').setText( + 'online-mode=false\nlevel-name=world\nlevel-type=default\n' + + "level-seed=${project.findProperty('benchmarkSeed') ?: '-4965128775892001975'}\n" + + 'generate-structures=false\nspawn-animals=false\nspawn-monsters=false\n' + + 'max-tick-time=-1\n', 'UTF-8') + } + } +task benchmarkIntegrationProcess(type: JavaExec, dependsOn: prepareWorldgenBenchmark) { + group = 'verification' + workingDir benchmarkRunDirectory + systemProperty 'orespawn.worldgenBenchmarkMode', project.findProperty('benchmarkMode') ?: 'sky' + systemProperty 'orespawn.worldgenBenchmarkRadius', project.findProperty('benchmarkRadius') ?: '4' + systemProperty 'orespawn.worldgenBenchmarkRepetitions', project.findProperty('benchmarkRepetitions') ?: '3' + systemProperty 'orespawn.worldgenBenchmarkCenterX', project.findProperty('benchmarkCenterX') ?: '256' + systemProperty 'orespawn.worldgenBenchmarkCenterZ', project.findProperty('benchmarkCenterZ') ?: '256' + systemProperty 'orespawn.worldgenBenchmarkCenterStep', project.findProperty('benchmarkCenterStep') ?: '64' + systemProperty 'orespawn.worldgenBenchmarkStopServer', 'true' + systemProperty 'orespawn.worldgenBenchmarkVanillaOres', + project.findProperty('benchmarkVanillaOres') ?: 'false' + systemProperty 'orespawn.worldgenBenchmarkOreAudit', + project.findProperty('benchmarkOreAudit') ?: 'false' + if (project.hasProperty('benchmarkBlockAudit')) { + systemProperty 'orespawn.worldgenBenchmarkBlockAudit', project.property('benchmarkBlockAudit') + } + configureFromForgeRun(delegate, 'runServer') + doLast { + String mode = project.findProperty('benchmarkMode') ?: 'sky' + String summary = "ORESPAWN_BENCHMARK summary mode=${mode}" + List benchmarkLogs = [ + new File(benchmarkRunDirectory, 'logs/fml-server-latest.log'), + new File(benchmarkRunDirectory, 'logs/latest.log') + ] + if (!benchmarkLogs.any { it.isFile() && it.getText('UTF-8').contains(summary) }) { + throw new GradleException("Missing ${mode} worldgen benchmark summary in ${benchmarkLogs}") + } + assertRuntimeLogsClean(benchmarkRunDirectory, "${mode} worldgen benchmark") + } + } +} + +tasks.named('processResources', ProcessResources) { + filteringCharset = 'UTF-8' + inputs.property('version', project.version) + inputs.property('mcversion', project.minecraft_version) + + filesMatching('mcmod.info') { + expand version: project.version, mcversion: project.minecraft_version + } + from('docs/AGENTS.md') { + into '' + rename { 'AGENTS.md' } + } + from('docs') { + into 'META-INF/orespawn/docs' + } + // Pack format 2 is wrapped by Minecraft 1.11's LegacyV2Adapter, which + // translates a requested lower-case locale such as en_us.lang to the + // pre-1.11 en_US.lang spelling. Keep the canonical lower-case sources but + // use target-runtime names in processed production resources. Renaming the + // single copy also keeps Windows and case-sensitive CI archives identical. + filesMatching('assets/orespawn/lang/*.lang') { + def locale = name =~ /^([a-z]{2})_([a-z]{2})\.lang$/ + if (!locale.matches()) { + throw new GradleException("Unexpected locale file name: ${name}") + } + name = "${locale.group(1)}_${locale.group(2).toUpperCase(java.util.Locale.ROOT)}.lang" + } + filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) +} + +def trackedDocumentationFiles = { + fileTree('docs') { include '**/*' }.files.findAll { it.isFile() } + .sort { project.relativePath(it) } +} +def documentationRelativePath = { File source -> + file('docs').toPath().relativize(source.toPath()).toString().replace('\\', '/') +} + +tasks.register('verifyDocumentationParity') { + group = 'verification' + description = 'Verifies every tracked guide has an identical processed production resource.' + dependsOn tasks.named('processResources') + doLast { + List tracked = trackedDocumentationFiles() + if (tracked.size() != 21) { + throw new GradleException("Expected 21 tracked documentation files, found ${tracked.size()}") + } + tracked.each { File source -> + String relative = documentationRelativePath(source) + File processed = layout.buildDirectory.file( + "resources/main/META-INF/orespawn/docs/${relative}").get().asFile + if (!processed.isFile() || !java.util.Arrays.equals(source.bytes, processed.bytes)) { + throw new GradleException("Processed documentation differs for ${relative}") + } + } + } +} + +def prepareEclipseResources = tasks.register('prepareEclipseResources') { + group = 'ide' + description = 'Copies Gradle-processed production resources into Eclipse merged output.' + dependsOn tasks.named('processResources') + doLast { + // Do not declare bin/main as a Gradle-owned output. Eclipse owns that + // directory and ForgeGradle inspects it while generating launches. + project.copy { + from(layout.buildDirectory.dir('resources/main')) + into(layout.projectDirectory.dir('bin/main')) + } + } +} + +tasks.named('jar', Jar) { + archiveClassifier = 'deobf' + destinationDirectory = layout.buildDirectory.dir('libs-dev') + manifest { + attributes([ + 'Specification-Title' : 'OreSpawn', + 'Specification-Vendor' : 'SkyBlade1978', + 'Specification-Version' : '1', + 'Implementation-Title' : base.archivesName.get(), + 'Implementation-Version' : project.version, + 'Implementation-Vendor' : 'SkyBlade1978', + 'OreSpawn-API-Version' : '1', + 'FMLAT' : 'accesstransformer.cfg', + 'Maven-Artifact' : expectedMavenCoordinate, + 'Built-On-Java' : '8', + 'Built-On' : "${project.minecraft_version}-${project.forge_version}" + ]) + } +} + +def releaseJar = renamer.classes(tasks.named('jar', Jar)) { + map.from minecraft.dependency.toSrgFile + archiveClassifier = null + accessTransformers = true + output = layout.buildDirectory.file("libs/OreSpawn-${project.version}.jar") +} + +// Release qualification also exercises the reobfuscated jar through Forge's +// packaged launcher. The official server jar and its library tree are +// machine-local inputs and are never copied into source control or artifacts. +def forge13UniversalRuntime = { + File runtime = new File(gradle.gradleUserHomeDir, + "caches/forge_gradle/maven_downloader/net/minecraftforge/forge/" + + "${minecraft_version}-${forge_version}/" + + "forge-${minecraft_version}-${forge_version}-universal.jar") + if (!runtime.isFile()) { + throw new GradleException("Forge 13 universal runtime is missing: ${runtime}") + } + runtime +} + +def packagedForgeRunDirectory = file("${buildDir}/packaged-forge-runtime-run") +def packagedMinecraftServer = { + if (!project.hasProperty('packagedMinecraftServerJar')) { + throw new GradleException('packagedMinecraftServerJar is required for packagedForgeRuntimeTest') + } + File server = file(project.property('packagedMinecraftServerJar')) + if (!server.isFile()) { + throw new GradleException("Minecraft 1.11.2 server jar is missing: ${server}") + } + server +} +def packagedForgeLibraries = { + if (!project.hasProperty('packagedForgeLibrariesRoot')) { + throw new GradleException('packagedForgeLibrariesRoot is required for packagedForgeRuntimeTest') + } + File libraries = file(project.property('packagedForgeLibrariesRoot')) + if (!libraries.isDirectory()) { + throw new GradleException("Forge 13 library root is missing: ${libraries}") + } + libraries +} +def packagedForgeRuntimeLayout = { + File forge = forge13UniversalRuntime() + File server = packagedMinecraftServer() + File libraries = packagedForgeLibraries() + java.util.jar.JarFile runtime = new java.util.jar.JarFile(forge) + String declared + try { + declared = runtime.manifest.mainAttributes.getValue('Class-Path') + } finally { + runtime.close() + } + if (declared == null || declared.trim().isEmpty()) { + throw new GradleException("Forge 13 runtime has no Class-Path manifest entry: ${forge}") + } + def dependencies = declared.trim().split(/\s+/).findAll { String entry -> + entry != "minecraft_server.${minecraft_version}.jar" + }.collect { String entry -> + String relative = entry.startsWith('libraries/') + ? entry.substring('libraries/'.length()) : entry + File dependency = new File(libraries, relative) + if (!dependency.isFile()) { + throw new GradleException("Forge 13 packaged-runtime dependency is missing: ${dependency}") + } + [source: dependency, relative: relative] + } + [forge: forge, server: server, dependencies: dependencies] +} + +tasks.register('preparePackagedForgeRuntimeTest') { + group = 'verification' + dependsOn releaseJar + dependsOn packagedSurfaceIntegrationTestModJar + doLast { + delete packagedForgeRunDirectory + def runtime = packagedForgeRuntimeLayout() + copy { from runtime.forge; into packagedForgeRunDirectory } + copy { + from runtime.server + into packagedForgeRunDirectory + rename { "minecraft_server.${minecraft_version}.jar" } + } + runtime.dependencies.each { Map dependency -> + File destination = new File(packagedForgeRunDirectory, + "libraries/${dependency.relative}").parentFile + copy { from dependency.source; into destination } + } + File mods = new File(packagedForgeRunDirectory, 'mods') + mods.mkdirs() + copy { from layout.buildDirectory.file("libs/OreSpawn-${project.version}.jar"); into mods } + copy { + from layout.buildDirectory.file('surface-integration-fixture/surfaceprobe-reobf.jar') + into mods + } + new File(packagedForgeRunDirectory, 'server.properties').setText('''\ +level-name=surface-integration-world +level-seed=zsjpxah +level-type=default +online-mode=false +server-port=0 +allow-nether=true +generate-structures=false +spawn-protection=0 +max-tick-time=-1 +''', 'UTF-8') + new File(packagedForgeRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } +} + +def createPackagedForgeProcess = { String phase, Object dependency -> + ByteArrayOutputStream console = new ByteArrayOutputStream() + File consoleFile = new File(packagedForgeRunDirectory, + "packaged-forge-${phase.toLowerCase()}-console.txt") + tasks.register("packagedForgeRuntime${phase}Process", Exec) { + group = 'verification' + dependsOn dependency + workingDir packagedForgeRunDirectory + doFirst { + console.reset() + File javaExecutable = java8Launcher.get().executablePath.asFile + File forge = new File(packagedForgeRunDirectory, forge13UniversalRuntime().name) + commandLine javaExecutable, + '-Dforge.logging.console.level=info', + "-Dsurfaceprobe.integrationPhase=${phase.toLowerCase()}", + '-jar', forge, 'nogui' + standardOutput = console + errorOutput = console + } + doLast { + consoleFile.setText(console.toString('UTF-8'), 'UTF-8') + } + } +} + +def packagedForgeFreshProcess = createPackagedForgeProcess( + 'Fresh', tasks.named('preparePackagedForgeRuntimeTest')) +packagedForgeFreshProcess.configure { + doLast { + File marker = new File(packagedForgeRunDirectory, + 'surface-integration-world/surfaceprobe-integration.properties') + if (!marker.isFile()) { + throw new GradleException("Packaged Forge fresh marker is missing: ${marker}") + } + assertRuntimeLogsClean(packagedForgeRunDirectory, 'packaged Forge fresh phase') + } +} +def packagedForgeReloadProcess = createPackagedForgeProcess('Reload', packagedForgeFreshProcess) +packagedForgeReloadProcess.configure { + doLast { + assertRuntimeLogsClean(packagedForgeRunDirectory, 'packaged Forge reload phase') + } +} + +tasks.register('packagedForgeRuntimeTest') { + group = 'verification' + description = 'Creates and reloads a world using the reobfuscated OreSpawn jar under real Forge 13.' + dependsOn packagedForgeReloadProcess + doLast { + File marker = new File(packagedForgeRunDirectory, + 'surface-integration-world/surfaceprobe-integration.properties') + Properties result = new Properties() + marker.withInputStream { result.load(it) } + if (result.getProperty('reload_verified') != 'true') { + throw new GradleException("Packaged Forge reload was not verified: ${marker}") + } + logger.lifecycle('Packaged Forge jar created and reloaded {} dimensions with {} audited columns each', + result.getProperty('dimensions'), result.getProperty('columns_per_dimension')) + } +} + +if (project.hasProperty('packagedMinecraftServerJar') + && project.hasProperty('packagedForgeLibrariesRoot')) { + tasks.named('check') { + dependsOn tasks.named('packagedForgeRuntimeTest') + } +} + +def apiJar = tasks.register('apiJar', Jar) { + dependsOn tasks.named('classes') + archiveClassifier = 'api' + destinationDirectory = layout.buildDirectory.dir('libs-dev') + from sourceSets.main.output + include 'zone/moddev/mc/orespawn/api/**' + include 'com/mcmoddev/orespawn/api/**' + include 'com/mojang/serialization/**' + include 'cyano/orespawn/**' + manifest { + attributes([ + 'Implementation-Title' : 'OreSpawn-api', + 'Implementation-Version': project.version, + 'OreSpawn-API-Version' : '1' + ]) + } +} + +tasks.register('deobfJar') { + group = 'build' + description = 'Builds the local deobfuscated development jar under build/libs-dev.' + dependsOn tasks.named('jar') +} + +tasks.named('sourcesJar', Jar) { + filteringCharset = 'UTF-8' + includeEmptyDirs = false + archiveClassifier = 'sources' + filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) + manifest { + attributes([ + 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}:sources", + 'Implementation-Title' : 'OreSpawn-sources', + 'Implementation-Version': project.version + ]) + } +} + +tasks.named('javadocJar', Jar) { + filteringCharset = 'UTF-8' + archiveClassifier = 'javadoc' + filesMatching(archiveTextPatterns, normalizeArchiveLineEndings) + manifest { + attributes([ + 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}:javadoc", + 'Implementation-Title' : 'OreSpawn-javadoc', + 'Implementation-Version': project.version + ]) + } +} + +['apiElements', 'runtimeElements'].each { configurationName -> + configurations.named(configurationName) { + artifacts.clear() + } + artifacts { + add(configurationName, releaseJar) + } +} + +tasks.named('assemble') { + dependsOn releaseJar + dependsOn apiJar + dependsOn tasks.named('sourcesJar') + dependsOn tasks.named('javadocJar') +} + +def expectedReleaseFiles = providers.provider { + def prefix = "${base.archivesName.get()}-${project.version}" + [ + "${prefix}.jar", + "${prefix}-sources.jar", + "${prefix}-javadoc.jar" + ] +} +def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') + +tasks.register('verifyReleaseConfiguration') { + group = 'verification' + description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' + + doLast { + if (project.mod_version != '4.0.16.111021' + || project.mod_group != expectedMavenGroup) { + throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") + } + if (project.minecraft_version != '1.11.2' + || project.forge_version != '13.20.1.2588' + || project.mapping_channel != 'stable' + || project.mapping_version != '32-1.11') { + throw new GradleException('Unexpected Minecraft, Forge, or mappings target') + } + if (project.loader_name != 'forge' || project.loader_code != '1' + || project.java_version != '8' || project.gradle_java_version != '17') { + throw new GradleException('Unexpected dispatcher target metadata') + } + if (project.group.toString() != expectedMavenGroup + || base.archivesName.get() != expectedMavenArtifact + || project.curseforge_project_id != '245586') { + throw new GradleException('Unexpected Maven or CurseForge publication identity') + } + + [ + 'src/main/java/zone/moddev/mc/orespawn/OreSpawn.java', + 'src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java', + 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', + 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', + 'README.md', 'CHANGELOG.txt' + ].each { path -> + if (!file(path).getText('UTF-8').contains('4.0.16.111021')) { + throw new GradleException("Authoritative release location does not contain 4.0.16.111021: ${path}") + } + } + if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { + throw new GradleException('Consumer compatibility floor must remain [4.0.6,5.0.0)') + } + if (!file('src/main/java/zone/moddev/mc/orespawn/api/OreSpawnApi.java') + .getText('UTF-8').contains('API_VERSION = 1')) { + throw new GradleException('OreSpawn API major must remain 1') + } + if (!file('src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java') + .getText('UTF-8').contains('SCHEMA_VERSION = 6') + || !file('src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java') + .getText('UTF-8').contains('SCHEMA_VERSION = 5')) { + throw new GradleException('Global/world schema versions must remain 6/5') + } + def providerSchema = new JsonSlurper().parse(file('docs/schemas/orespawn-provider.schema.json')) + if (!(providerSchema.properties.schema_version.enum as List).contains(4)) { + throw new GradleException('Provider schema must continue to support schema version 4') + } + } +} + +tasks.register('verifyReleaseArtifacts') { + group = 'verification' + description = 'Audits the exact three distributable jars and their release-critical contents.' + dependsOn tasks.named('verifyReleaseConfiguration') + dependsOn tasks.named('assemble') + + doLast { + File libs = layout.buildDirectory.dir('libs').get().asFile + List jars = (libs.listFiles() ?: [] as File[]) + .findAll { it.name.endsWith('.jar') } + .sort { it.name } + List actual = jars.collect { it.name } + List expected = expectedReleaseFiles.get().sort() + if (actual != expected) { + throw new GradleException("Expected exactly ${expected}, found ${actual}") + } + + jars.each { candidate -> + if (candidate.length() == 0L) { + throw new GradleException("Empty release artifact: ${candidate.name}") + } + ZipFile candidateZip = new ZipFile(candidate) + try { + candidateZip.entries().findAll { entry -> + !entry.isDirectory() + && (archiveTextSuffixes.any { entry.name.endsWith(it) } + || entry.name.endsWith('/element-list') + || entry.name.endsWith('/package-list')) + }.each { entry -> + boolean containsCarriageReturn = candidateZip.getInputStream(entry).withCloseable { input -> + input.bytes.any { value -> value == 13 } + } + if (containsCarriageReturn) { + throw new GradleException( + "${candidate.name}!/${entry.name} does not use canonical LF line endings") + } + } + ['src/test/', 'src/biomeIntegrationTest/', 'src/migrationIntegrationTest/', + 'src/clientIntegrationTest/', 'src/os1AbiFixture/', 'src/os3AbiFixture/', + 'src/baseMetals111Fixture/', + 'agent-notes/', 'surfaceprobe', 'migrationprobe', 'clientprobe', + 'org/junit/', 'org/mockito/', 'net/bytebuddy/'].each { forbidden -> + if (candidateZip.entries().any { it.name.contains(forbidden) }) { + throw new GradleException( + "${candidate.name} contains forbidden entry matching ${forbidden}") + } + } + } finally { + candidateZip.close() + } + } + + File mainJar = new File(libs, expectedReleaseFiles.get()[0]) + ZipFile zip = new ZipFile(mainJar) + try { + List names = zip.entries().collect { it.name } + [ + 'mcmod.info', + 'META-INF/accesstransformer.cfg', + 'zone/moddev/mc/orespawn/api/OreSpawnApi.class', + 'META-INF/orespawn/docs/VERSIONS.md', + 'META-INF/orespawn/docs/schemas/orespawn-provider.schema.json', + 'META-INF/orespawn/docs/schemas/orespawn-global.schema.json', + 'META-INF/orespawn/docs/schemas/orespawn-world.schema.json', + 'AGENTS.md' + ].each { required -> + if (!names.contains(required)) { + throw new GradleException("Release jar is missing ${required}") + } + } + fileTree('src/main/resources/assets/orespawn/lang') { + include '*.lang' + }.files.each { File source -> + def locale = source.name =~ /^([a-z]{2})_([a-z]{2})\.lang$/ + if (!locale.matches()) { + throw new GradleException("Unexpected locale file name: ${source.name}") + } + String alias = "${locale.group(1)}_${locale.group(2).toUpperCase(java.util.Locale.ROOT)}.lang" + def entry = zip.getEntry("assets/orespawn/lang/${alias}") + if (entry == null || !java.util.Arrays.equals(source.bytes, + zip.getInputStream(entry).withCloseable { it.bytes })) { + throw new GradleException("Packaged LegacyV2 locale alias differs for ${alias}") + } + } + List trackedDocs = trackedDocumentationFiles() + if (trackedDocs.size() != 21) { + throw new GradleException("Expected 21 tracked documentation files, found ${trackedDocs.size()}") + } + trackedDocs.each { File source -> + String relative = documentationRelativePath(source) + def entry = zip.getEntry("META-INF/orespawn/docs/${relative}") + if (entry == null || !java.util.Arrays.equals(source.bytes, + zip.getInputStream(entry).withCloseable { it.bytes })) { + throw new GradleException("Packaged documentation differs for ${relative}") + } + } + + String metadata = zip.getInputStream(zip.getEntry('mcmod.info')) + .getText(StandardCharsets.UTF_8.name()) + def parsed = new JsonSlurper().parseText(metadata) + def mod = parsed instanceof List ? parsed.first() : parsed + if (mod.modid != 'orespawn' || mod.version != project.mod_version + || mod.mcversion != project.minecraft_version) { + throw new GradleException('Packaged mcmod.info version or target is incorrect') + } + + String packagedAccessTransformer = zip.getInputStream( + zip.getEntry('META-INF/accesstransformer.cfg')) + .getText(StandardCharsets.UTF_8.name()) + List packagedAccessTransformerRules = packagedAccessTransformer.readLines() + .collect { it.replaceFirst(/\s*#.*/, '').trim() } + .findAll { !it.isEmpty() } + List expectedRuntimeAccessTransformerRules = [ + 'public-f net.minecraft.world.WorldProvider field_76578_c', + 'public-f net.minecraft.world.gen.ChunkProviderOverworld field_186001_t' + ] + if (packagedAccessTransformerRules != expectedRuntimeAccessTransformerRules) { + throw new GradleException('Packaged access transformer was not remapped to the runtime SRG rules') + } + + def manifestEntry = zip.getEntry('META-INF/MANIFEST.MF') + def manifest = manifestEntry == null ? null + : new Manifest(zip.getInputStream(manifestEntry)).mainAttributes + if (manifest == null + || manifest.getValue('Implementation-Version') != project.mod_version + || manifest.getValue('OreSpawn-API-Version') != '1' + || manifest.getValue('FMLAT') != 'accesstransformer.cfg' + || manifest.getValue('Maven-Artifact') != expectedMavenCoordinate + || manifest.getValue('Implementation-Timestamp') != null + || manifest.getValue('Timestamp') != null) { + throw new GradleException('Release manifest identity/API/FMLAT is incorrect or volatile') + } + + zip.entries().findAll { it.name.endsWith('.class') }.each { entry -> + byte[] header = new byte[8] + zip.getInputStream(entry).withCloseable { input -> + if (input.read(header) != header.length) { + throw new GradleException("Cannot inspect bytecode header for ${entry.name}") + } + } + int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) + if (major != 52) { + throw new GradleException("${entry.name} uses Java class major ${major}, expected 52") + } + } + } finally { + zip.close() + } + + File sources = new File(libs, expectedReleaseFiles.get()[1]) + new ZipFile(sources).withCloseable { sourceZip -> + if (sourceZip.getEntry('zone/moddev/mc/orespawn/OreSpawn.java') == null) { + throw new GradleException('Sources jar is missing OreSpawn.java') + } + } + File javadocs = new File(libs, expectedReleaseFiles.get()[2]) + new ZipFile(javadocs).withCloseable { javadocZip -> + if (javadocZip.getEntry('index.html') == null) { + throw new GradleException('Javadoc jar is missing index.html') + } + } + } +} + +tasks.register('writeReleaseChecksums') { + group = 'verification' + description = 'Writes SHA-256 checksums for the audited release jars.' + dependsOn tasks.named('verifyReleaseArtifacts') + def outputFile = layout.buildDirectory.file('release/SHA256SUMS') + inputs.files(providers.provider { + expectedReleaseFiles.get().collect { name -> + layout.buildDirectory.file("libs/${name}").get().asFile + } + }) + outputs.file(outputFile) + + doLast { + File output = outputFile.get().asFile + output.parentFile.mkdirs() + File libs = layout.buildDirectory.dir('libs').get().asFile + String contents = expectedReleaseFiles.get().sort().collect { name -> + MessageDigest digest = MessageDigest.getInstance('SHA-256') + new File(libs, name).withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + "${digest.digest().encodeHex().toString().toUpperCase()} ${name}" + }.join('\n') + '\n' + output.setText(contents, 'UTF-8') + } +} + +tasks.register('verifyPreparedReleaseArtifacts') { + group = 'verification' + description = 'Audits a previously built immutable release bundle before remote Maven publication.' + + doLast { + if (!preparedReleaseDir.isPresent()) { + throw new GradleException('preparedReleaseDir is required for prepared artifact publication') + } + File prepared = file(preparedReleaseDir.get()) + if (!prepared.isDirectory()) { + throw new GradleException("Prepared release directory does not exist: ${prepared}") + } + List expected = expectedReleaseFiles.get().sort() + List jars = (prepared.listFiles() ?: [] as File[]) + .findAll { it.name.endsWith('.jar') } + .sort { it.name } + if (jars.collect { it.name } != expected || jars.any { it.length() == 0L }) { + throw new GradleException("Prepared release must contain exactly the non-empty jars ${expected}") + } + File checksums = new File(prepared, 'SHA256SUMS') + File changelog = new File(prepared, 'CHANGELOG.txt') + if (!checksums.isFile() || !changelog.isFile()) { + throw new GradleException('Prepared release is missing SHA256SUMS or CHANGELOG.txt') + } + List actualChecksums = jars.collect { candidate -> + MessageDigest digest = MessageDigest.getInstance('SHA-256') + candidate.withInputStream { input -> + byte[] buffer = new byte[8192] + for (int read = input.read(buffer); read >= 0; read = input.read(buffer)) { + if (read > 0) digest.update(buffer, 0, read) + } + } + "${digest.digest().encodeHex().toString().toUpperCase()} ${candidate.name}" + }.sort() + List declaredChecksums = checksums.readLines('UTF-8') + .findAll { !it.trim().isEmpty() } + .sort() + if (actualChecksums != declaredChecksums) { + throw new GradleException('Prepared release checksums do not match the immutable jars') + } + } +} + +def mavenUploadUrl = providers.environmentVariable('MAVEN_UPLOAD_URL') + .orElse('https://invalid.invalid/missing-maven-upload-url') +def mavenUploadUsername = providers.environmentVariable('MAVEN_UPLOAD_USERNAME') +def mavenUploadPassword = providers.environmentVariable('MAVEN_UPLOAD_PASSWORD') + +publishing { + publications { + mavenJava(MavenPublication) { + groupId = expectedMavenGroup + artifactId = expectedMavenArtifact + version = project.version.toString() + if (preparedReleaseDir.isPresent()) { + File prepared = file(preparedReleaseDir.get()) + artifact(new File(prepared, expectedReleaseFiles.get()[0])) + artifact(new File(prepared, expectedReleaseFiles.get()[1])) { + classifier = 'sources' + } + artifact(new File(prepared, expectedReleaseFiles.get()[2])) { + classifier = 'javadoc' + } + } else { + artifact(releaseJar) + artifact(tasks.named('sourcesJar')) + artifact(tasks.named('javadocJar')) + } + pom { + name = 'MMD OreSpawn' + description = project.mod_description + url = 'https://github.com/MinecraftModDevelopmentMods/OreSpawn' + licenses { + license { + name = 'GNU Lesser General Public License, Version 2.1' + url = 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt' + } + } + scm { + connection = 'scm:git:https://github.com/MinecraftModDevelopmentMods/OreSpawn.git' + developerConnection = 'scm:git:ssh://git@github.com/MinecraftModDevelopmentMods/OreSpawn.git' + url = 'https://github.com/MinecraftModDevelopmentMods/OreSpawn' + } + } + } + } + repositories { + maven { + name = 'release' + url = uri(mavenUploadUrl.get()) + credentials { + username = mavenUploadUsername.orNull ?: '' + password = mavenUploadPassword.orNull ?: '' + } + } + } +} + +tasks.register('verifyMavenCoordinates') { + group = 'verification' + description = 'Verifies the generated POM uses OreSpawn\'s mod-specific Maven namespace.' + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + doLast { + File pomFile = layout.buildDirectory.file( + 'publications/mavenJava/pom-default.xml').get().asFile + if (!pomFile.isFile()) { + throw new GradleException("Generated Maven POM does not exist: ${pomFile}") + } + def pom = new XmlSlurper(false, false).parse(pomFile) + def actual = [pom.groupId.text(), pom.artifactId.text(), pom.version.text()] + def expected = [expectedMavenGroup, expectedMavenArtifact, project.version.toString()] + if (project.group.toString() != expectedMavenGroup || actual != expected) { + throw new GradleException("Expected Maven coordinate ${expected.join(':')}, " + + "found ${actual.join(':')}") + } + } +} + +tasks.register('validateMavenReleaseCredentials') { + group = 'publishing' + description = 'Prevents Maven publication from targeting a local or incomplete repository.' + doLast { + if (!providers.environmentVariable('MAVEN_UPLOAD_URL').isPresent() + || !mavenUploadUsername.isPresent() + || !mavenUploadPassword.isPresent()) { + throw new GradleException( + 'MAVEN_UPLOAD_URL, MAVEN_UPLOAD_USERNAME, and MAVEN_UPLOAD_PASSWORD are required') + } + String target = providers.environmentVariable('MAVEN_UPLOAD_URL').get() + if (target.startsWith('file:')) { + throw new GradleException('Maven release publication must use a remote repository') + } + } +} + +tasks.withType(PublishToMavenRepository).configureEach { + dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn tasks.named('verifyMavenCoordinates') + if (preparedReleaseDir.isPresent()) { + dependsOn tasks.named('verifyPreparedReleaseArtifacts') + } else { + dependsOn tasks.named('verifyReleaseArtifacts') + } +} + +tasks.named('check') { + dependsOn tasks.named('verifyMavenCoordinates') + dependsOn tasks.named('verifyDocumentationParity') +} + +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +// Older ForgeGradle workspaces can retain GradleStart launchers alongside the +// ForgeGradle 7 Buildship launches. Remove only those exact obsolete launch +// types; leave independently maintained launch files untouched. +def obsoleteForgeGradleEclipseLaunches = [ + 'OreSpawn_Client.launch': 'GradleStart', + 'OreSpawn_Server.launch': 'GradleStartServer' +] + +eclipse { + classpath { + downloadSources = true + downloadJavadoc = true + } + synchronizationTasks 'isolateEclipseProductionRuns' +} + +tasks.register('configureEclipseBuildship') { + group = 'ide' + description = 'Creates the Buildship project preferences used by ForgeGradle 7 imports.' + doLast { + File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs') + Properties preferences = new Properties() + Map requiredPreferences = [ + 'eclipse.preferences.version' : '1', + 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', + 'connection.gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, + 'connection.project.dir' : '', + 'gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, + 'override.workspace.settings' : 'true' + ] + requiredPreferences.each { key, value -> preferences.setProperty(key, value) } + preferencesFile.parentFile.mkdirs() + preferencesFile.withOutputStream { + preferences.store(it, + 'Generated by configureEclipseBuildship; keep Eclipse and command-line caches aligned.') + } + } +} + +tasks.register('isolateEclipseProductionRuns') { + group = 'ide' + description = 'Normalizes ForgeGradle 7 Eclipse launches and marks ordinary launches as production-only.' + dependsOn tasks.named('genEclipseRuns') + dependsOn tasks.named('configureEclipseBuildship') + dependsOn prepareEclipseResources + + doLast { + obsoleteForgeGradleEclipseLaunches.each { String name, String mainClass -> + File launch = file(name) + if (launch.isFile()) { + String contents = launch.getText('UTF-8') + String obsoleteMainType = + "org.eclipse.jdt.launching.MAIN_TYPE\" value=\"${mainClass}\"" + if (contents.contains(obsoleteMainType) && !launch.delete()) { + throw new GradleException("Could not remove obsolete ForgeGradle launch ${name}") + } + } + } + + // ForgeGradle 7's legacy 1.11 run generator does not know its own + // MC_VERSION token. It emits ${MC_VERSION} as an Eclipse variable, + // which prevents every generated launch from starting. Store the + // target version as a literal environment value instead. + fileTree(project.projectDir) { + include 'run*.launch' + }.files.each { File launch -> + String contents = launch.getText('UTF-8') + contents = contents.replace( + 'key="MC_VERSION" value="${MC_VERSION}"', + "key=\"MC_VERSION\" value=\"${minecraft_version}\"") + launch.setText(contents.replace('\r\n', '\n'), 'UTF-8') + } + + ['runClient.launch', 'runServer.launch'].each { name -> + File launch = file(name) + if (!launch.isFile()) { + throw new GradleException("ForgeGradle did not generate ${name}") + } + String contents = launch.getText('UTF-8') + if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) { + String marker = '' + String attribute = + ' ' + contents = contents.replace(marker, attribute + '\n' + marker) + } + launch.setText(contents.replace('\r\n', '\n'), 'UTF-8') + } + } +} + +tasks.register('verifyEclipseProductionClasspath') { + group = 'verification' + description = 'Verifies that ordinary generated Eclipse launches exclude tests, fixtures, and probe mods.' + dependsOn tasks.named('isolateEclipseProductionRuns') + dependsOn tasks.named('verifyLegacyOracleIsolation') + + doLast { + File buildshipPreferences = file('.settings/org.eclipse.buildship.core.prefs') + if (!buildshipPreferences.isFile()) { + throw new GradleException('Missing Eclipse Buildship project preferences') + } + File eclipseResources = file('bin/main') + List trackedDocs = trackedDocumentationFiles() + if (trackedDocs.size() != 21) { + throw new GradleException("Expected 21 tracked documentation files, found ${trackedDocs.size()}") + } + trackedDocs.each { File source -> + String relative = documentationRelativePath(source) + File eclipseCopy = new File(eclipseResources, "META-INF/orespawn/docs/${relative}") + if (!eclipseCopy.isFile() + || !java.util.Arrays.equals(source.bytes, eclipseCopy.bytes)) { + throw new GradleException("Eclipse documentation differs for ${relative}") + } + } + [ + 'mcmod.info', + 'META-INF/orespawn/docs/README.md', + 'META-INF/orespawn/docs/VERSIONS.md' + ].each { relative -> + File required = new File(eclipseResources, relative) + if (!required.isFile()) { + throw new GradleException( + "Eclipse production output is missing processed resource ${relative}") + } + } + String eclipseMetadata = new File(eclipseResources, 'mcmod.info').getText('UTF-8') + if (!eclipseMetadata.contains("\"version\": \"${project.version}\"") + || !eclipseMetadata.contains( + "\"mcversion\": \"${minecraft_version}\"")) { + throw new GradleException('Eclipse mcmod.info retains unexpanded build placeholders') + } + List launchFiles = fileTree(project.projectDir) { + include 'runClient.launch' + include 'runServer.launch' + include '.eclipse/runClient.launch' + include '.eclipse/runServer.launch' + }.files as List + if (launchFiles.size() < 2) { + throw new GradleException('ForgeGradle did not generate ordinary client/server Eclipse launches') + } + List allGeneratedLaunches = fileTree(project.projectDir) { + include 'run*.launch' + }.files as List + allGeneratedLaunches.each { launch -> + String contents = launch.getText('UTF-8') + if (contents.contains('${MC_VERSION}')) { + throw new GradleException( + "${launch.name} retains ForgeGradle's unresolved MC_VERSION token") + } + if (!contents.contains( + "key=\"MC_VERSION\" value=\"${minecraft_version}\"")) { + throw new GradleException( + "${launch.name} does not define literal MC_VERSION=${minecraft_version}") + } + } + obsoleteForgeGradleEclipseLaunches.each { String name, String mainClass -> + File launch = file(name) + if (launch.isFile() && launch.getText('UTF-8').contains( + "org.eclipse.jdt.launching.MAIN_TYPE\" value=\"${mainClass}\"")) { + throw new GradleException( + "Obsolete ForgeGradle ${mainClass} launch remains at ${name}") + } + } + List forbidden = [ + 'src/test', 'bin/test', 'build/classes/java/test', + 'biomeIntegrationTest', 'migrationIntegrationTest', 'clientIntegrationTest', + 'os1AbiFixture', 'os3AbiFixture', 'baseMetals111Fixture', + 'surfaceprobe', 'migrationprobe', 'clientprobe', + 'junit-', 'opentest4j-', 'junit-platform-', + 'Mineralogy-1.10.2-3.3.8.26.jar', 'Mineralogy-1.11.2-3.3.0.jar', + 'Mineralogy-1.12.2-3.8.0.53.jar', 'OreSpawn-1.11.2-3.2.2.jar' + ] + launchFiles.each { launch -> + String contents = launch.getText('UTF-8').replace('\\', '/') + List present = forbidden.findAll { contents.contains(it) } + if (!present.isEmpty()) { + throw new GradleException("${launch.name} exposes test code/dependencies: ${present}") + } + if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE') + || !contents.contains('value="true"')) { + throw new GradleException("${launch.name} does not exclude test code") + } + if (!contents.contains('PROJECT_ATTR" value="OreSpawn"')) { + throw new GradleException("${launch.name} targets the wrong Eclipse project") + } + } + } +} diff --git a/changelog.mustache b/changelog.mustache new file mode 100644 index 00000000..076ba598 --- /dev/null +++ b/changelog.mustache @@ -0,0 +1,10 @@ +{{#tags}} + {{#issues}} + {{#commits}} +**{{{messageTitle}}}** +{{#messageBodyItems}} + * {{.}} +{{/messageBodyItems}} + {{/commits}} + {{/issues}} +{{/tags}} diff --git a/ci-fixtures/README.md b/ci-fixtures/README.md new file mode 100644 index 00000000..ac0d5c00 --- /dev/null +++ b/ci-fixtures/README.md @@ -0,0 +1,28 @@ +# Forge 1.11.2 CI fixtures + +These exact published Mineralogy engines and the sealed generated OS3 world +make the Forge 1.11.2 migration and parity checks self-contained on hosted CI. +They are test inputs only and must never enter an OreSpawn release artifact or +an ordinary Eclipse launch. + +- `Mineralogy-1.10.2-3.3.8.26.jar` is the carried 1.10 Cyano-engine oracle. +- `Mineralogy-1.11.2-3.3.0.jar` is the native 1.11 Cyano-engine oracle rebuilt + from exact Mineralogy source commit + `727fec4c8fd9d874bf480c74ccd868804021137b`. +- `Mineralogy-1.12.2-3.8.0.53.jar` is the native 1.12 Cyano-engine oracle. +- `OreSpawn-1.11.2-3.2.2.jar` is the target-native OS3 ABI fixture rebuilt + from dormant-branch commit + `67ea7aebe766f00c1b7fe46488a5e0ab31e5cc6c`. +- `OreSpawn_1.10.2-1.1.0.jar` is the inherited published OS1 ABI fixture. +- `os3-331-default-source.zip` is the immutable generated-world source used by + the legacy-lineage fresh/reload gates. + +Both 1.11 jars were built with their historical Gradle 4.9 wrappers and exact +Temurin 8.0.502+7. The source trees were detached at the commits above. Only +obsolete CurseGradle/Sonar configuration was removed from the disposable +build scripts; no production source or resource was changed. The resulting +ordinary `jar` output was copied here, hashed, and is loaded only by isolated +test class loaders or explicit compatibility tasks. + +`SHA256SUMS` is authoritative. The Gradle build verifies every hash before +compiling tests or starting a migration runtime. diff --git a/ci-fixtures/SHA256SUMS b/ci-fixtures/SHA256SUMS new file mode 100644 index 00000000..2a94bb11 --- /dev/null +++ b/ci-fixtures/SHA256SUMS @@ -0,0 +1,6 @@ +88A6237C9A0E2C8891718B68C373E741C78B8494F5E68D8093CA9339F3BC4D87 artifacts/Mineralogy-1.10.2-3.3.8.26.jar +5737C0FA65CB334D191FCD147B4FB16C26823F8EE9882E70A6888E32DA8A9440 artifacts/Mineralogy-1.11.2-3.3.0.jar +C42E608E5662A94138BD2461019D33283F96E3FB66E28DB91C00A49E9A8005CD artifacts/Mineralogy-1.12.2-3.8.0.53.jar +8CAB9D60C988C6239FCFCAC4AF7886E63A77BD97A1FDFD82DE5EBE30987F9614 artifacts/OreSpawn-1.11.2-3.2.2.jar +91345E4B825AA06E05F470066F467AABA0C7A2804A8DBCA4D3CD1905ED53607C artifacts/OreSpawn_1.10.2-1.1.0.jar +2852FA549C7A952CCC0EAE1454057CA81BD91F5BB323BEA1030452BB1D82FDFD worlds/os3-331-default-source.zip diff --git a/ci-fixtures/artifacts/Mineralogy-1.10.2-3.3.8.26.jar b/ci-fixtures/artifacts/Mineralogy-1.10.2-3.3.8.26.jar new file mode 100644 index 00000000..13398b48 Binary files /dev/null and b/ci-fixtures/artifacts/Mineralogy-1.10.2-3.3.8.26.jar differ diff --git a/ci-fixtures/artifacts/Mineralogy-1.11.2-3.3.0.jar b/ci-fixtures/artifacts/Mineralogy-1.11.2-3.3.0.jar new file mode 100644 index 00000000..5f128408 Binary files /dev/null and b/ci-fixtures/artifacts/Mineralogy-1.11.2-3.3.0.jar differ diff --git a/ci-fixtures/artifacts/Mineralogy-1.12.2-3.8.0.53.jar b/ci-fixtures/artifacts/Mineralogy-1.12.2-3.8.0.53.jar new file mode 100644 index 00000000..28d9e785 Binary files /dev/null and b/ci-fixtures/artifacts/Mineralogy-1.12.2-3.8.0.53.jar differ diff --git a/ci-fixtures/artifacts/OreSpawn-1.11.2-3.2.2.jar b/ci-fixtures/artifacts/OreSpawn-1.11.2-3.2.2.jar new file mode 100644 index 00000000..0d054989 Binary files /dev/null and b/ci-fixtures/artifacts/OreSpawn-1.11.2-3.2.2.jar differ diff --git a/ci-fixtures/artifacts/OreSpawn_1.10.2-1.1.0.jar b/ci-fixtures/artifacts/OreSpawn_1.10.2-1.1.0.jar new file mode 100644 index 00000000..d5fa3940 Binary files /dev/null and b/ci-fixtures/artifacts/OreSpawn_1.10.2-1.1.0.jar differ diff --git a/ci-fixtures/worlds/os3-331-default-source.zip b/ci-fixtures/worlds/os3-331-default-source.zip new file mode 100644 index 00000000..db30c20d Binary files /dev/null and b/ci-fixtures/worlds/os3-331-default-source.zip differ diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000..7a3a620a --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 00000000..04e7b789 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,16 @@ +# OreSpawn Documentation Map + +This index is for navigating the documentation to learn how to integrate with +and use OreSpawn with a mod or modpack. Start with +[DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md). + +Use the focused guides for implementation details: + +- [API.md](API.md) for the supported Java API; +- [PROVIDERS.md](PROVIDERS.md) for packaged and configurable providers; +- [FEATURES.md](FEATURES.md) for rocks, ores, deposits, and geology; +- [BIOMES.md](BIOMES.md) and [DIMENSIONS.md](DIMENSIONS.md) for world integration; +- [TEMPLATES.md](TEMPLATES.md) for selectable world styles; +- [CONFIGURATION.md](CONFIGURATION.md) for configuration behavior; +- [VERSIONS.md](VERSIONS.md) for the shared four-component target-qualified versioning and branch-release convention; +- [README.md](README.md) for schemas, examples, and the complete documentation index. diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 00000000..8f73ca74 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,168 @@ +# Java API + +This branch targets Minecraft 1.11.2 and Forge 13. Public examples therefore +use the public `ResourceLocation(String, String)` constructor. Forge 13 has no +`DeferredRegister`, so provider mods use OreSpawn's API-major-1 +`BiomeRegistrar` while provider JSON, profiles, schemas, and biome meanings +remain identical to later ports. + +Only `zone.moddev.mc.orespawn.api` is supported API. Every other package is an +implementation detail. API major version is available as +`OreSpawnApi.API_VERSION` and in the jar manifest as +`OreSpawn-API-Version`. + +Provider mods must depend on the full OreSpawn mod at compile time and +runtime. Forge 1.11 declares the mandatory dependency on the mod annotation, +for example: + +```java +@Mod(modid = "examplemod", name = "Example Mod", version = "1.0.0", + dependencies = "required-after:orespawn@[4.0.6,5.0.0)") +``` + +Submit declarations during Forge's initialization event, before OreSpawn +freezes provider discovery during post-initialization: + +```java +@Mod.EventHandler +public void init(FMLInitializationEvent event) { + WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) + .rock(new ResourceLocation("examplemod", "slate"), GeologyFamily.METAMORPHIC, rock -> rock + .depth(12, 36) + .weight(1.2) + .oreReplaceable(true)) + .build(); + OreSpawnApi.enqueue(provider); +} +``` + +For a complete ore-only Java example, including dimensions, height curves, +patterns, and host tags, see `DEVELOPER_GUIDE.md`. + +Definitions are immutable after `build()`. Registry references remain +`ResourceLocation` values until OreSpawn validates and bakes them. Provider +declarations are collected during Forge initialization and frozen during +post-initialization; late mutation is rejected. + +Ore dimensions use `quantity(int)` for fixed budgets or +`quantityRange(min, max)` for inclusive random budgets. The compatibility +`quantity()` getter returns the rounded-up midpoint of a range; new code should +read `minQuantity()` and `maxQuantity()`. Add OS3-style ordinary-dimension +coverage with `OreDefinition.Builder.dimensionSelector(...)` and +`OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END`. Explicit dimensions +override that selector and prevent duplicate placement. + +The builder emits provider schema 4. Legacy provider schemas 1-3 remain +readable. Schema 4 is required for biome palettes and dimension materials. + +Provider-owned fluid deposits are declarative and may target several dimensions: + +```java +FormationDefinition formations = FormationDefinition.builder() + .horizontalSize(FormationPreset.HUGE) + .waviness(FormationPreset.LARGE) + .build(); +FluidDepositDefinition brine = FluidDepositDefinition.builder( + new ResourceLocation("examplemod", "fluid_deposit/brine"), + new ResourceLocation("examplemod", "brine")) + .dimension(new ResourceLocation("minecraft", "overworld"), placement -> placement + .yRange(0, 32) + .attempts(0.05) + .radius(4, 10) + .verticalRadius(2, 4) + .maxLobes(3) + .minSolidCover(2) + .minSolidShell(1) + .hostTag(new ResourceLocation("forge", "stone"))) + .build(); + +WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) + .fluidDeposit(brine) + .build(); +``` + +`OilDefinition` and template `.oil(...)` remain deprecated migration adapters +for one legacy oil rule. New integrations should use `FluidDepositDefinition`. + +Ore dimension builders expose the same biome filters as provider JSON and +fluid-deposit builders. Use `.biome(...)` and `.biomeDictionary(...)` for +inclusions, with `.excludeBiome(...)` and `.excludeBiomeDictionary(...)` for +exclusions. These methods work on both explicit `.dimension(...)` rules and +`.dimensionSelector(...)` fallbacks; built definitions and their returned +filter sets are immutable. + +Create one `BiomeRegistrar` during normal mod construction. It attaches to the +calling mod's event bus and defers biome factories until Forge's biome registry +event. `OreSpawnBiomes.copyAndRegister` clones a known biome without adding a +biome-framework dependency: + +```java +private static final OreSpawnBiomes.BiomeRegistrar BIOMES = + OreSpawnBiomes.registrar("examplemod"); + +private static final OreSpawnBiomes.BiomeReference CANDY_PLAINS = + OreSpawnBiomes.copyAndRegister( + BIOMES, "candy_plains", + () -> ForgeRegistries.BIOMES.getValue(new ResourceLocation("minecraft", "plains")), + builder -> builder.temperature(0.8F).downfall(0.4F)); +``` + +The returned handle implements `Supplier`, so `.get()` remains the +consumer pattern across OreSpawn versions. Call it only after registries have +completed; `getId()` is available as soon as the declaration is made. + +Then declare placement and materials through the same provider: + +```java +WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) + .biomePalette(new ResourceLocation("examplemod", "overworld"), + new ResourceLocation("minecraft", "overworld"), palette -> palette + .mode(BiomePlacementMode.REPLACE) + .scope(BiomeReplacementScope.MINECRAFT_ONLY) + .regionSize(BiomeRegionSize.LARGE) + .coverage(1.0) + .fallbackWeight(0.0) + .biome(new ResourceLocation("examplemod", "candy_plains"), biome -> biome + .weight(3.0) + .similarBiome(new ResourceLocation("minecraft", "plains")))) + .dimensionMaterials(new ResourceLocation("examplemod", "overworld_materials"), + new ResourceLocation("minecraft", "overworld"), materials -> materials + .defaultFluid(new ResourceLocation("examplemod", "lemonade")) + .snowBlock(new ResourceLocation("examplemod", "icing")) + .iceBlock(new ResourceLocation("examplemod", "frozen_lemonade"))) + .build(); +``` + +Biome selection stays declarative: arbitrary provider callbacks are not called +inside chunk generation. See `BIOMES.md` for replacement modes, compatibility +filters, surface blocks, materials, and automatic total-conversion templates. + +Query the active profile and sample exact production geology on the server: + +```java +OreSpawnApi.getActiveProfile(server).ifPresent(profile -> + LOGGER.info("Configured rocks: {}", profile.rockIds().size())); + +OreSpawnApi.createSampler(server.overworld()).ifPresent(sampler -> { + GeologyColumn column = sampler.sampleColumn(120, 20, 92); + LOGGER.info("{} / {} / {}", column.biome(), column.geome(), column.rockAt(20)); +}); +``` + +`sampleColumn` performs one biome/geome classification and reuses it for every +Y query. Sampling is read-only and is intended for gameplay decisions, +diagnostics, and compatible generation outside OreSpawn's block loops. +Callbacks inside OreSpawn generation loops are intentionally unsupported. + +Forge 13 does not replay registry events for a custom registry created during +pre-initialization. A custom-pattern mod with a required OreSpawn dependency +registers its named `OrePatternType` directly through +`OreSpawnPatternRegistry.registry()` during its own pre-initialization. An +`OrePatternType` contains a codec and a compiler from decoded settings to +`CompiledOrePattern`. Reference it from an ore dimension with +`pattern(patternId, settingsJson)`. OreSpawn decodes and compiles once while +baking the profile; only the compiled placement function runs during +generation. + +`OreSpawnOreIntegration` remains as a deprecated facade for early ore-provider +integrations. New code should use `OreSpawnApi`. diff --git a/docs/BIOMES.md b/docs/BIOMES.md new file mode 100644 index 00000000..be669ad1 --- /dev/null +++ b/docs/BIOMES.md @@ -0,0 +1,191 @@ +# Biomes And World Materials + +Minecraft 1.11.2 uses Forge 13's static biome registry and set-based +`BiomeProvider` contract. OreSpawn adapts those target APIs internally while +keeping the API-major-1 provider JSON, profile, and save contracts unchanged. + +OreSpawn can place provider biomes and replace their visible world materials +without requiring TerraBlender. It does not register biomes for a child mod: +the provider still registers ordinary Forge `Biome` objects, then supplies +declarative placement and material rules to OreSpawn. + +This feature is optional. Ore-only providers and existing Mineralogy profiles +with no biome palettes use Minecraft's original biome source unchanged. + +## How Placement Composes + +OreSpawn waits until a server level has its final `ChunkGenerator`, then wraps +the biome source already selected for that dimension. Vanilla, TerraBlender, +Biomes O' Plenty, or another framework therefore runs first. OreSpawn reads the +source biome once and applies pre-baked palette rules. + +The wrapper is native to OreSpawn and has no TerraBlender compile-time or +runtime dependency. This keeps simple child mods small while still allowing a +pack that already uses TerraBlender to compose safely. + +Each palette has: + +- `dimension`: the full dimension ID; +- `mode`: `augment` keeps the source as a weighted fallback, while `replace` + chooses only provider biomes when a rule applies; +- `scope`: `minecraft_only`, `selected_namespaces`, or `all`; +- `region_size`: `tiny`, `small`, `average`, `large`, or `huge`, corresponding + to 128, 256, 512, 1024, or 2048 block regions; +- `coverage`: the proportion of eligible regions touched by the palette; +- `fallback_weight`: source-biome weight in augment mode; +- namespace include/exclude lists and weighted output biome entries. + +Biome entries may restrict temperature/downfall and list similar source biomes. +`similar_biomes` is optional compatibility: missing IDs are ignored. +`required_similar_biomes` is strict: if one is absent, the output is disabled +and OreSpawn warns once while baking. + +## Provider JSON Example + +```json +{ + "schema_version": 4, + "provider_modid": "cakeworld", + "provider_revision": 1, + "biome_palettes": { + "cakeworld:overworld": { + "dimension": "minecraft:overworld", + "enabled": true, + "mode": "replace", + "scope": "minecraft_only", + "region_size": "large", + "coverage": 1.0, + "fallback_weight": 0.0, + "include_namespaces": [], + "exclude_namespaces": [], + "biomes": { + "cakeworld:candy_plains": { + "enabled": true, + "weight": 3.0, + "similar_biomes": ["minecraft:plains"], + "required_similar_biomes": [], + "min_temperature": 0.2, + "max_temperature": 1.2, + "min_downfall": 0.0, + "max_downfall": 0.8, + "surface": { + "top_block": "cakeworld:icing", + "filler_block": "cakeworld:chocolate_sponge", + "underwater_block": "cakeworld:biscuit_sand", + "filler_depth": 3 + } + } + } + } + }, + "dimension_materials": { + "cakeworld:overworld": { + "dimension": "minecraft:overworld", + "enabled": true, + "default_fluid": "cakeworld:lemonade", + "snow_block": "cakeworld:icing", + "ice_block": "cakeworld:frozen_lemonade" + } + } +} +``` + +Provider-owned rule IDs use the provider namespace. Output biomes and blocks +must be installed registry IDs. Fluid material IDs must resolve to blocks whose +default states contain real fluids. + +## Registration Helper + +Forge 13 predates `DeferredRegister`. Declare one OreSpawn registrar during mod +construction, then use `copyAndRegister` to copy a known biome before applying +small changes: + +```java +private static final OreSpawnBiomes.BiomeRegistrar BIOMES = + OreSpawnBiomes.registrar("examplemod"); + +private static final OreSpawnBiomes.BiomeReference CANDY_PLAINS = + OreSpawnBiomes.copyAndRegister( + BIOMES, "candy_plains", + () -> ForgeRegistries.BIOMES.getValue(new ResourceLocation("minecraft", "plains")), + builder -> builder.temperature(0.8F).downfall(0.4F)); +``` + +`blankAndRegister` starts from an empty builder and is intended for advanced +providers that deliberately supply every required climate, effects, spawn, and +generation field. Both helpers return a supplier-compatible handle, reject +duplicate or late declarations, and only register content; placement belongs +in the provider declaration. + +## Surfaces And Materials + +Biome surfaces support: + +- `top_block`: exposed ground; +- `filler_block`: material below the top; +- `underwater_block`: exposed ground below sea level; +- `ceiling_block`: optional underside material; +- `filler_depth`: 0-16 blocks. + +Provider surfaces run during `LOCAL_MODIFICATIONS`: after Minecraft has built +base surfaces and lakes, but before structures and vegetation. That ordering +lets OreSpawn replace the actual exposed ground while preserving later trees, +plants, authored structures, and block entities. In ceiling dimensions, +`ceiling_block` applies to the roof underside and does not replace the roof top. + +Surface correction is generation-only. Installing or updating OreSpawn does +not rewrite already generated chunks; travel into new terrain to see a changed +provider surface definition. + +Provider-declared `terrain_dimensions.host_blocks` are resolved by the single +terrain scan at the start of Forge 1.11's early generation coordinator, +immediately before provider surfaces. Matching natural blocks already present +in base terrain are eligible for geology; matching blocks authored later by +structures or vegetation are not. Air, liquids, bedrock, and block-entity +states remain protected even if a provider mistakenly lists their block IDs. + +Dimension materials support the ordinary aquifer fluid and replacements for +vanilla snow and ice. Minecraft 1.11.2 has one exposed generator-fluid field, +so `default_fluid` is fully supported. Later-format `deep_aquifer_fluid` and +`deep_aquifer_max_y` values remain readable and are preserved in saved profiles, +but this branch disables their editor controls, warns when a distinct deep +fluid was requested, and uses the ordinary fluid for generation. OreSpawn converts +weather products in loaded chunks and around players; it does not replace every +water or lava block after generation. + +## Templates And Total Conversions + +A total-conversion mod may bundle an automatic template: + +```json +"templates": { + "cakeworld:cake_world": { + "required_mods": ["cakeworld"], + "auto_select": true, + "auto_select_priority": 100, + "profile": { + "selected_template": "cakeworld:cake_world" + } + } +} +``` + +Automatic selection occurs only for fresh worlds when no explicit global +`default_template` exists. Existing world profiles never change automatically. +If several providers request automatic selection, the highest priority wins, +then lexical template ID order. + +## World-Creation Editor + +**Biomes & World Materials** is visible even when rock strata are disabled. +It lists palettes and materials by dimension, uses installed-registry pickers, +and validates IDs before world creation. The editor is creation-only in 4.0.0; +existing worlds remain editable through their self-contained server profile. + +## Performance Boundaries + +OreSpawn resolves dimensions, biomes, blocks, fluids, namespace filters, +climate ranges, and surfaces while the profile is baked. Runtime biome +selection uses the delegated source result, integer region hashing, primitive +weights, and pre-baked biome instances. It performs no provider callback, JSON access, +registry lookup, tag lookup, logging, or per-column allocation. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 00000000..69fd610e --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,246 @@ +# Configuration Reference + +OreSpawn uses three JSON contracts: + +| File | Schema | Purpose | +|---|---:|---| +| `config/orespawn-worldgen.json` | 6 | Installed-pack defaults for new worlds | +| `/serverconfig/orespawn-worldgen.json` | 5 | Self-contained snapshot for one world | +| `config/-orespawn.json` | 4 | Optional authoritative provider override | + +A provider may package schema 4 at `assets//orespawn/provider.json`. +Legacy provider schemas 1-3 remain accepted. Fluid deposits require schema 3; +biome palettes and dimension materials require schema 4. + +The profile for a new world is merged in this order: passive OreSpawn defaults, +packaged or API providers, provider override files, the global configuration, +the selected template, and Create World edits. The result is saved with the +world. Restart after editing JSON by hand. + +## Top-Level Fields + +| Field | Values | Meaning | +|---|---|---| +| `schema_version` | Contract-specific integer | Global 6, world 5, provider 4 | +| `geology_mode` | `geome`, `legacy` | Sky/geome engine or Cyano legacy engine | +| `place_fluid_deposits` | boolean | Master switch for configured fluid-deposit rules | +| `manage_vanilla_ores` | boolean | Lets OreSpawn suppress and replace claimed vanilla ore features | +| `suppress_all_ore_features` | boolean | Suppresses all standard Forge ore features; use only in complete packs | +| `default_template` | registry ID or empty string | Template selected for newly created server worlds | +| `formations` | object | Shape controls used only when terrain strata are active | +| `rocks` | object keyed by rule ID | Eligible rock definitions | +| `geomes` | object keyed by geome ID | Geological province weights | +| `biomes` | object keyed by biome ID | Explicit biome-to-geome weights | +| `biome_dictionary` | object keyed by Forge biome type | Fallback biome-to-geome weights | +| `terrain_dimensions` | object keyed by dimension ID | Dimensions and hosts eligible for terrain replacement | +| `biome_palettes` | object keyed by provider-owned rule ID | Optional native-biome overlays and surfaces | +| `dimension_materials` | object keyed by provider-owned rule ID | Aquifer fluid, snow, and ice substitutions | +| `ores` | object keyed by rule ID | Ore outputs and per-dimension placement | +| `fluid_deposits` | object keyed by rule ID | Provider-owned fluids and per-dimension placement | +| `retrogen` | object | Bounded ore retrogen controls | +| `flat_bedrock` | object | Opt-in flat bedrock controls | +| `worldgen_aliases` | ID-to-ID object | Replacement output aliases resolved while baking | + +## Enumerated Values + +| Setting | Accepted values | +|---|---| +| Formation algorithm | `stable_layers`, `sky_v1` | +| Formation preset | `tiny`, `small`, `average`, `large`, `huge`, `custom` | +| Rock family | `sedimentary`, `metamorphic`, `igneous_intrusive`, `igneous_volcanic` | +| Ore pattern | `default`, `vein`, `normal_cloud`, `precision`, `clusters`, `underfluids` | +| Legacy pattern aliases | `cluster`, `cloud` | +| Height distribution | `uniform`, `triangle`, `bottom_triangle`, `uniform_bottom_triangle` | +| Biome placement mode | `augment`, `replace` | +| Biome replacement scope | `all`, `minecraft_only`, `selected_namespaces` | +| Biome region size | `tiny`, `small`, `average`, `large`, `huge` | + +`sky_v1` exists to preserve migrated worlds. Use `stable_layers` for new packs. + +## Formations + +Each of `horizontal_size`, `vertical_thickness`, `waviness`, +`edge_irregularity`, and `formation_continuity` accepts any formation preset. +When a control is `custom`, its value comes from `formations.custom`: + +| Custom field | Range | Purpose | +|---|---:|---| +| `stratum_wavelength` | 16-8192 | Horizontal persistence of formations | +| `family_region_wavelength` | 16-8192 | Scale of broad family provinces | +| `vertical_thickness` | 1-255 | Typical layer thickness | +| `waviness_wavelength` | 32-2048 | Horizontal distance over which layers bend | +| `waviness_amplitude` | 0-512 | Maximum broad vertical displacement | +| `edge_wavelength` | 8-512 | Scale of boundary detail | +| `edge_amplitude` | 0-256 | Strength of boundary detail | +| `edge_octaves` | 1-8 | Number of boundary-detail scales | +| `continuity` | 0-1 | Proportion of formations retaining global identity | + +For Stable Layers, the Edge Detail presets use these +`wavelength / amplitude / octaves` values: + +| Preset | Edge detail | +|---|---:| +| Tiny | `48 / 4 / 1` | +| Small | `64 / 12 / 2` | +| Average | `96 / 24 / 3` | +| Large | `128 / 48 / 4` | +| Huge | `192 / 96 / 5` | + +Average is calibrated to retain visible variation at later layer contacts. +Custom profiles keep their explicit values; these numbers are only used by the +named presets and as defaults for new Custom settings. + +Cyano settings use `cyano.geome_size` (4-32767), +`cyano.rock_layer_noise` (1-32767), and `cyano.rock_layer_thickness` (1-255). +Migrated Mineralogy worlds also store `cyano.enabled`, the exact ordered +`cyano.igneous_rocks`, `cyano.metamorphic_rocks`, and +`cyano.sedimentary_rocks` arrays, plus `cyano.realistic_coal_layers` for the +Mineralogy 1.10 lineage. Native Mineralogy 1.12 did not have realistic coal; +its `PLACE_MINERALOGY_ROCK=false` is preserved as `cyano.enabled=false`. +These values and the old family white/blacklists are snapshotted per world and +are ignored by Sky. + +The resulting values and missing registry IDs are recorded in +`/serverconfig/orespawn-upgrade-report.txt`. OS3 rule and global-switch +imports are summarized in `config/orespawn-upgrade-report.txt` and retained in +machine-readable form at `config/orespawn-os3-migration-report.json`. + +## Rocks And Geomes + +A rock requires `enabled`, `family`, `depth_peak`, `depth_spread`, `min_y`, +`max_y`, `weight`, and `ore_replaceable`. Provider definitions should also set +`block`; global/world entries may use the rule ID as the block ID when `block` +is omitted. `dimensions` limits membership, and `geomes` multiplies selection +weight by province. A weight of zero prevents selection in that context. + +`min_y` and `max_y` are inclusive actual-world height limits. Stable Layers +may shift a layer vertically to preserve its formation, family, and lithology +identity, but that shifted coordinate never makes an out-of-range world block +eligible or rejects an otherwise legal world height. + +Geomes contain a non-negative `base` weight and non-negative weights for each +rock family. Keys may retain the legacy unnamespaced form or use a provider +resource ID such as `examplemod:crystal_basin`; the creation editor preserves +both forms. Biome and biome-dictionary maps multiply those geome weights. +Missing optional-mod biome IDs are ignored during baking. + +Terrain dimensions require `enabled`, `host_blocks`, and `host_tags`. +`biome_ids` and `biome_namespaces` can narrow a custom dimension. The Overworld +is conventional but not automatic; Nether and End terrain remain untouched +unless a profile explicitly opts them in. + +## Biome Palettes And World Materials + +Biome palettes are independent of rock strata. Each palette names a +`dimension`, placement `mode`, replacement `scope`, `region_size`, `coverage`, +`fallback_weight`, optional namespace filters, and one or more weighted biome +entries. Region presets are 128, 256, 512, 1024, and 2048 blocks. + +`augment` keeps the source biome as a weighted fallback. `replace` selects only +eligible palette biomes. Scope controls which source namespaces may be changed: +`minecraft_only` protects modded biomes by default, `selected_namespaces` +requires `include_namespaces`, and `all` permits every namespace except those +in `exclude_namespaces`. + +Each biome entry may set `similar_biomes`, `required_similar_biomes`, +temperature/downfall ranges, and a surface object. Optional similar biomes are +ignored when absent. If a required similar biome is absent, that output entry +is disabled with one setup warning. Surface fields are `top_block`, +`filler_block`, `underwater_block`, `ceiling_block`, and `filler_depth`. + +Dimension-material rules may set `default_fluid`, `deep_aquifer_fluid`, +`deep_aquifer_max_y`, `snow_block`, and `ice_block`. Fluid IDs must resolve to +blocks with non-empty fluid states. These substitutions are opt-in; a dimension +with no matching rule retains its native generator and weather materials. +Minecraft 1.11.2 exposes one generator fluid, so this branch applies +`default_fluid` only. It retains `deep_aquifer_fluid` and +`deep_aquifer_max_y` in provider and world profiles for cross-version +portability, but the editor keeps those controls disabled and generation does +not use a distinct deep fluid. +See `BIOMES.md` for complete examples and practical guidance. + +## Ore Fields + +An ore has `enabled`, one output `block` or weighted `outputs`, and at least one +entry in `dimensions` or `dimension_selectors`. Optional fields include `native_generation`, +`suppress_vanilla`, `retrogen`, `deep_output`, and `deep_output_max_y`. + +Each enabled ore dimension uses: + +| Field | Range/default | Meaning | +|---|---|---| +| `min_y`, `max_y` | 0-255 in the 1.11.2 editor | Inclusive placement range; metadata block states remain separate from height | +| `frequency` | 0-64 | Expected attempts per chunk | +| `quantity` | 1-64 | Fixed block budget for each attempt | +| `min_quantity`, `max_quantity` | 1-64 | Inclusive random block-budget range; both fields are required | +| `pattern` | pattern name or codec object | Deposit shape | +| `height_distribution` | one of four values | Vertical probability curve | +| `discard_chance_on_air_exposure` | 0-1 | Chance to omit candidates touching cave air | +| `spread` | 0-64 | Horizontal pattern reach | +| `vertical_spread` | 0-64 | Vertical pattern reach | +| `node_size` | 1-32 | Cluster node size | +| `length` | 1-64 | Pattern path length where supported | +| `fluid` | registry ID | Fluid used by `underfluids` | + +At least one of `host_families`, `host_blocks`, or `host_tags` must be present. +Hosts may be plain registry IDs or weighted objects such as +`{"tag":"forge:stone","weight":0.75}`. Optional +`geomes`, biome include/exclude IDs, and biome-dictionary include/exclude arrays +further narrow placement. + +On Minecraft 1.11.2, a plain block ID accepts every metadata state belonging to +that block. To accept ordinary stone only, use +`{"block":"minecraft:stone","metadata":0}` in `host_blocks`; omitting +`metadata` also permits granite, diorite, and andesite states stored under the +same `minecraft:stone` block ID. + +`frequency` is expected attempts per chunk: the integer part is guaranteed and +the fractional part is the chance of one additional attempt. A fixed +`quantity` or sampled quantity range is a placement budget, not a promise that +every candidate finds a valid host. A complete range overrides `quantity` if +both are present. + +The selector `orespawn:all_except_nether_end` covers every dimension except +the vanilla Nether and End. Explicit rules in `dimensions` override selector +rules for the same ore and dimension, including explicit disabled rules. + +## Fluid Deposits, Retrogen, And Bedrock + +Each `fluid_deposits` entry has a stable provider-namespaced rule ID, an +`enabled` flag, one output fluid `block`, and one or more `dimensions`. The +output must resolve to a non-air block whose default state has a non-empty +fluid state. OreSpawn does not provide a default water, lava, or oil rule. +Players who enable standalone rock strata can create a world-owned rule from +the **Fluid Deposits** screen by choosing any installed fluid block. The UI +starts it as a covered Overworld deposit and keeps every value editable. + +An enabled dimension supports `min_y`, `max_y`, `frequency`, `min_radius`, +`max_radius`, `min_vertical_radius`, `max_vertical_radius`, `max_lobes`, and +`min_solid_cover`. `min_solid_shell` defaults to `1` and requires that many +solid blocks around the sides and underside of each generated lobe; the larger +of it and `min_solid_cover` is used above the lobe. A candidate that intersects +cave air is rejected before any fluid is written. The rule also requires at +least one `host_families`, `host_blocks`, +or `host_tags` entry. Optional `biome_ids`, `excluded_biome_ids`, +`biome_dictionary`, `excluded_biome_dictionary`, and `geomes` narrow the rule. +`frequency` uses the same expected-attempts-per-chunk meaning as ores. + +`retrogen.enabled` is off by default. `revision` is a non-negative marker, +`force` deliberately revisits marked chunks, and `chunks_per_tick` is 1-16. +Only ore rules with `retrogen:true` participate. Terrain strata are never +retro-generated. + +`flat_bedrock.enabled` and `flat_bedrock.retrogen` are off by default. +`layers` is 1-5 and `dimensions` is an array of full dimension IDs. + +## Validation And Server Copying + +Registry IDs use `namespace:path`, for example `minecraft:granite`. Validate +files with the schemas in `schemas/` and compare them with `examples/`. +Enabled fluid outputs must additionally resolve to real non-air fluid blocks, +and each enabled fluid dimension must have a valid host rule. Missing blocks, +ordinary solid blocks, and hostless rules are rejected before generation. + +Copying a world's `serverconfig/orespawn-worldgen.json` to the same location in +a dedicated server world reproduces its choices when the same referenced mods, +blocks, biomes, dimensions, and tags are installed. diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 00000000..b7e6e7c9 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,232 @@ +# OreSpawn Developer Guide + +## Decide Which Integration You Need + +| Goal | Recommended integration | +|---|---| +| Add ores to vanilla stone | Ore-only provider with explicit host tags | +| Let a modpack tune another mod's rules | `config/-orespawn.json` | +| Ship rocks, geomes, or custom terrain | Full packaged provider | +| Construct definitions in Java | API provider sent through Forge IMC | +| Offer an optional world style | Named template in a provider | +| Add covered underground oil or another fluid | Provider schema 3 fluid deposit | +| Add or place biomes without a framework dependency | Provider schema 4 biome palette | +| Replace surfaces, aquifers, snow, or ice | Provider schema 4 dimension materials | +| Inspect active geology at runtime | `GeologyProfileView` and `GeologySampler` | + +Strata are optional. If no enabled terrain dimension has eligible rocks, +OreSpawn skips terrain replacement and all formation/geome settings are inert. +An ore-only provider needs only ore output blocks, dimensions, and valid host +blocks or tags. + +## Provider JSON Quick Start + +Put a schema-4 file in your mod jar at: + +```text +src/main/resources/assets/examplemod/orespawn/provider.json +``` + +The rule IDs must use your mod namespace, but output and host blocks may belong +to any installed mod. This minimal provider places tin in normal Overworld +stone without enabling strata: + +```json +{ + "schema_version": 4, + "provider_modid": "examplemod", + "provider_revision": 1, + "ores": { + "examplemod:ore/tin": { + "block": "examplemod:tin_ore", + "enabled": true, + "source_mod": "examplemod", + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "min_y": 0, + "max_y": 96, + "frequency": 6.0, + "min_quantity": 4, + "max_quantity": 11, + "pattern": "vein", + "height_distribution": "triangle", + "host_tags": ["forge:stone"] + } + } + } + } +} +``` + +The complete example at `examples/examplemod-orespawn.json` adds a rock, a +weighted ore output, a provider-owned fluid deposit, a geome, a biome influence, +a custom dimension, a biome palette, world materials, and a selectable template. + +## Java API Quick Start + +Declare OreSpawn as a mandatory dependency on the Forge 1.11 mod annotation: + +```java +@Mod(modid = "examplemod", name = "Example Mod", version = "1.0.0", + dependencies = "required-after:orespawn@[4.0.6,5.0.0)") +``` + +Submit immutable definitions during Forge's initialization event: + +```java +import zone.moddev.mc.orespawn.api.GeologyFamily; +import zone.moddev.mc.orespawn.api.OreHeightDistribution; +import zone.moddev.mc.orespawn.api.OreDimensionSelector; +import zone.moddev.mc.orespawn.api.OrePattern; +import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.orespawn.api.WorldgenProvider; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; + +@Mod.EventHandler +public void init(FMLInitializationEvent event) { + ResourceLocation tin = new ResourceLocation("examplemod", "tin_ore"); + WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) + .ore(tin, ore -> ore + .retrogen(false) + .dimensionSelector(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END, + placement -> placement + .yRange(0, 96) + .attempts(6.0) + .quantityRange(4, 11) + .pattern(OrePattern.VEIN) + .heightDistribution(OreHeightDistribution.TRIANGLE) + .biome(new ResourceLocation("minecraft", "plains")) + .biomeDictionary("FOREST") + .excludeBiome(new ResourceLocation("minecraft", "roofed_forest")) + .excludeBiomeDictionary("SPOOKY") + .hostTag(new ResourceLocation("forge", "stone")))) + .build(); + + OreSpawnApi.enqueue(provider); +} +``` + +Only `zone.moddev.mc.orespawn.api` is stable. Do not call classes in +`worldgen`, `integration`, `client`, or other implementation packages. + +Use `.quantity(8)` when every attempt should have a fixed budget. The selector +above preserves old OS3 behavior in every ordinary dimension except Nether and +End. Add an explicit `.dimension(overworld, ...)` as well when the Overworld +needs different settings; the explicit rule overrides the selector there. +Ore dimension builders support the same exact-ID and biome-dictionary include +and exclude filters as provider JSON and fluid-deposit builders. + +## Pack Override Quick Start + +Copy `examples/examplemod-orespawn.json` to: + +```text +config/examplemod-orespawn.json +``` + +Change `provider_modid` to the exact mod ID and keep every owned rule ID in +that namespace. A present pack override is authoritative. If it is malformed, +OreSpawn marks that provider inactive rather than falling back to packaged or +API values. This fail-safe lets the provider retain native generation. + +## Ownership And Takeover + +A provider that normally generates its own ores should keep doing so until: + +```java +OreSpawnApi.isOreTakeoverActive("examplemod") +``` + +returns `true`. `PENDING` means provider discovery has not frozen. `INACTIVE` +means the provider file, registry blocks, hosts, dimensions, or ownership rules +did not validate. Never disable native generation for either state. + +## Configuration Values At A Glance + +- Geology modes: `geome` (Sky) and `legacy` (Cyano). +- Formation algorithms: `stable_layers` and migration-only `sky_v1`. +- Presets: `tiny`, `small`, `average`, `large`, `huge`, `custom`. +- Families: `sedimentary`, `metamorphic`, `igneous_intrusive`, + `igneous_volcanic`. +- Patterns: `default`, `vein`, `normal_cloud`, `precision`, `clusters`, + `underfluids`; legacy aliases `cluster` and `cloud` are accepted. +- Height distributions: `uniform`, `triangle`, `bottom_triangle`, + `uniform_bottom_triangle`. +- `frequency`: expected attempts per chunk from 0 to 64. The integer part is + guaranteed and the fraction is the chance of one extra attempt. +- `quantity`: fixed block-placement budget per attempt from 1 to 64. +- `min_quantity` and `max_quantity`: paired inclusive random budget; a complete + range takes precedence over a fixed quantity. +- `dimension_selectors.orespawn:all_except_nether_end`: OS3-compatible fallback + for ordinary dimensions; explicit dimension rules override it. +- Air-exposure discard: 0 keeps exposed candidates; 1 rejects all candidates + touching cave air. +- Biome placement: `augment` or `replace`; scope is `all`, `minecraft_only`, or + `selected_namespaces`; region sizes are 128-2048 block presets. + +See `CONFIGURATION.md` and the JSON Schemas for every field and numeric range. + +## Runtime And Performance Rules + +Provider files and API definitions freeze before generation. OreSpawn resolves +registry IDs, tags, dimensions, geomes, aliases, and block states while baking. +The generation loop must not contain provider callbacks, config reads, registry +lookups, strings, logging, reflection, or avoidable allocation. + +Biome filters retain their exact registry IDs. Minecraft 1.11.2 uses a static +Forge-backed biome registry, so generation carries those stable IDs alongside +the selected biome instances. Fluid deposits perform one keyed surface-biome +lookup per chunk invocation and no registry lookup in the placement loop. + +Biome palettes wrap the dimension's already-selected biome provider and bake +static-registry biomes, climate ranges, namespace filters, weights, surfaces, +and world materials at server activation. No TerraBlender API is called. A +dimension without a palette or material rule keeps the original generator path. + +Definitions normally change after a restart. `/orespawn reload` is intended for +operator-controlled profile reloads. Existing chunks are unchanged unless +bounded ore or bedrock retrogen is enabled. + +## Distribution Checklist + +1. Validate the provider file against `schemas/orespawn-provider.schema.json`. +2. Test without OreSpawn if your mod declares it optional; otherwise declare a + mandatory dependency. +3. Keep native ore generation enabled until takeover status is active. +4. Test every configured dimension and host tag. +5. For biome providers, test required/optional similar-biome behavior both with + and without compatibility mods. +6. Confirm the provider appears in `/orespawn status`. +7. Test a new world; profile edits do not rewrite already generated terrain. + +OreSpawn's own standard `check` lifecycle includes a consumer-style surface +integration test. A separate test provider creates independently marked +Grass/Dirt, underwater, filler, and roof columns in open and ceiling +normal-noise dimensions. The gate verifies biome and chunk edges, late tree, +vegetation, structure and chest sentinels, the roof underside, and exact save +and reload behavior. On Forge 13 it also exercises the registered spring +wrapper with a non-Forge-stone provider rock and registers an external ore +pattern beside every built-in type. It also verifies OreSpawn's Forge 13 biome +registrar rejects duplicate and late declarations. +Run `gradlew check` (or `gradlew build`, which includes it) +before publishing any change to biome registration, palettes, surfaces, +feature ordering, height handling, or profile persistence. + +Before publishing a Forge 1.11.2 jar, also run the packaged-runtime gate with +the official Minecraft 1.11.2 dedicated-server jar and the libraries installed +for Forge 13.20.1.2588: + +```text +gradlew packagedForgeRuntimeTest --offline --no-daemon \ + -PpackagedMinecraftServerJar= \ + -PpackagedForgeLibrariesRoot= +``` + +Unlike ForgeGradle's development launches, this gate places the reobfuscated +OreSpawn jar in `mods`, starts Forge's real `ServerLaunchWrapper`, generates a +fresh provider world, and reopens the same save. It deliberately excludes +mapped Forge jars, `sourceSets.main`, `MOD_CLASSES`, and LegacyDev so packaging +metadata such as `FMLAT` is tested exactly as players receive it. diff --git a/docs/DIMENSIONS.md b/docs/DIMENSIONS.md new file mode 100644 index 00000000..613fa383 --- /dev/null +++ b/docs/DIMENSIONS.md @@ -0,0 +1,44 @@ +# Terrain Dimensions + +Standalone OreSpawn has no enabled terrain-replacement dimension. The +Overworld is the conventional geology target used by full providers such as +Mineralogy. Nether and End remain untouched unless a global/world profile +explicitly enables them. Providers may automatically opt in only dimensions +in their own namespace. + +Biome palettes and dimension-material rules are separate from terrain +replacement. They may target `minecraft:overworld`, `minecraft:the_nether`, +`minecraft:the_end`, or a provider dimension without adding rock strata. +Unconfigured dimensions keep their original biome source, aquifers, snow, and +ice. OreSpawn wraps the source already installed by the dimension, so +TerraBlender is supported when present but is never required. + +Each `terrain_dimensions` entry supplies replacement host blocks or tags and +may restrict generation to explicit biome IDs or biome namespaces. With no +biome restriction, all biomes in that dimension are eligible. + +Rock entries may include a `dimensions` array. For backward compatibility, an +entry without this field belongs only to `minecraft:overworld`. A custom +dimension is disabled during baking if no valid eligible rocks resolve. + +Ore rules may instead use the built-in +`dimension_selectors.orespawn:all_except_nether_end` policy. It covers the +Overworld and ordinary custom dimensions, but never the vanilla Nether or End. +An explicit ore rule for a dimension takes precedence over the selector, even +when the explicit rule is disabled. + +Example: + +```json +"examplemod:crystal_caverns": { + "enabled": true, + "biome_namespaces": ["examplemod"], + "biome_ids": [], + "host_blocks": ["examplemod:base_rock"], + "host_tags": [] +} +``` + +OreSpawn resolves dimensions, hosts, tags, rocks, and biome filters before +chunk generation. An unconfigured dimension performs one table lookup and +immediately skips. diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 00000000..da8603a8 --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,66 @@ +# Ore Patterns And Runtime Features + +Every ore dimension selects one pattern. Legacy string values remain accepted; +the namespaced form is preferred. + +| Pattern | ID | Shape | +|---|---|---| +| Compact | `orespawn:default` | One dense, face-connected deposit | +| Vein | `orespawn:vein` | Wandering chain of connected nodes | +| Normal cloud | `orespawn:normal_cloud` | Diffuse bounded cloud | +| Precision | `orespawn:precision` | Deterministic compact fill | +| Clusters | `orespawn:clusters` | Multiple nearby face-connected nodes | +| Under fluids | `orespawn:underfluids` | Connected deposit beneath configured fluid | + +Built-in settings are `spread` (0-64), `vertical_spread` (0-64), `node_size` +(1-32), `length` (1-64), and a fluid registry ID. The legacy flat fields and +the codec object below are equivalent: + +```json +"pattern": { + "type": "orespawn:clusters", + "settings": { + "spread": 12, + "vertical_spread": 5, + "node_size": 3, + "length": 20, + "fluid": "minecraft:water" + } +} +``` + +Other mods may register `OrePatternType` values in the Forge registry named by +`OreSpawnPatternRegistry.REGISTRY_NAME`. Each type supplies a Mojang `Codec` +and compiles decoded settings into a `CompiledOrePattern`. Compilation occurs +during profile baking. The generation loop invokes only the compiled object. +Third-party codec settings are preserved and shown read-only in OreSpawn's UI. +On Forge 13, register the named type through `OreSpawnPatternRegistry.registry()` +during pre-initialization. The target does not replay a registry event for a +custom registry created during OreSpawn pre-initialization. + +Height selection supports `uniform`, centre-peaked `triangle`, deep-biased +`bottom_triangle`, and a half-uniform `uniform_bottom_triangle`. `frequency` +is expected attempts per chunk: the integer part is guaranteed and the +fractional part is the chance of one additional attempt. `quantity` is a fixed +placement budget; `min_quantity` and `max_quantity` define an inclusive random +budget. `orespawn:all_except_nether_end` preserves OS3-style ordinary custom +dimension coverage, with explicit dimensions taking precedence. +`discard_chance_on_air_exposure` is a +number from 0 to 1; selected placements touching air are rejected with that +probability. It can reproduce buried ore behavior without reducing deposits +that remain enclosed in rock. Exposure inspection is limited to the active +chunk; an unavailable neighbouring block is not read or treated as cave air. + +Compact nodes use one of 48 pre-baked orientations. Every prefix from 1 to 64 +blocks is face-connected when the host material is continuous. Forge 1.11 +initial generation and retrogen both keep reads and writes inside the active +chunk. A shape reaching an edge is clipped there, and the neighbouring chunk +receives its own independently sampled attempts when it generates. + +Retrogen records a deterministic profile revision in chunk NBT under +`OreSpawn`. Only ore rules with `retrogen:true` participate. Processing is +bounded by `chunks_per_tick`; no terrain strata retrogen exists. + +Flat bedrock is disabled by default. When enabled it flattens the configured +number of bottom layers and, in the Nether, the ceiling. It uses normal Forge +features and chunk events, with no reflection. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md new file mode 100644 index 00000000..e7aba6d0 --- /dev/null +++ b/docs/MIGRATION.md @@ -0,0 +1,155 @@ +# Migration + +## OreSpawn 3 Compatibility On Minecraft 1.11.2 + +This branch ships a deprecated compatibility bridge for existing OreSpawn 3 +consumer jars. It preserves the compatible public descriptors from OreSpawn +3.2.2 and 3.3.1, including `com.mcmoddev.orespawn`, `OreSpawn.API`, plugin +annotations, builders, feature hooks, replacements, and programmatic +registration handles. Existing binaries are discovered without recompilation. + +The bridge scans `@OreSpawnPlugin` metadata and packaged +`assets//` definitions, translates resource and Java +registrations into OreSpawn 4 providers, and schedules custom legacy features +through one compatibility coordinator. The original OreSpawn 3 generator is +never registered alongside its translated OreSpawn 4 rule. + +Migration is atomic and idempotent. Source files are never edited. Before an +OreSpawn 4 target is written, OreSpawn retains a backup and writes through a +temporary file. The deterministic report records mappings, clamping, +unsupported entries, ambiguities, and any required user action. A clean second +startup does not rewrite the result or repeat registration, retrogen, or +generation. + +Minecraft 1.11 metadata states are preserved as block ID plus metadata; the +migrator does not invent post-flattening block IDs. Legacy Base Metals rules +use stable provider identities when their outputs match uniquely. Mineralogy +3 replacement rocks are accepted as ore hosts, but Mineralogy remains the +authoritative geology engine when that legacy stack is installed. + +## Mineralogy 1.10, 1.11, And 1.12 Geology Handoff + +An existing world must not silently switch geology engines when Mineralogy is +updated to integrate with OreSpawn 4. If a generated world has no OreSpawn +world profile and its saved Forge mod list records Mineralogy 3 or earlier, +OreSpawn creates the first world profile in `legacy` mode before generating +new chunks. + +Mineralogy 1.10, 1.11, and 1.12 used similar configuration files but not identical +contracts. OreSpawn therefore selects a lineage from the Mineralogy version in +`level.dat` (or the recoverable `level.dat_old`) and then consumes: + +- `GEOME_SIZE`, `ROCK_LAYER_NOISE`, and `ROCK_LAYER_THICKNESS` in both lines; +- every family whitelist and blacklist with the exact historical rock order; +- `REALISTIC_COAL_LAYERS` only for the 1.10 lineage; +- `PLACE_MINERALOGY_ROCK` only for native 1.12, including preserving `false`. + +Native Mineralogy 1.11.2 3.3.0 uses defaults `geome_size=100`, +`rock_layer_noise=32`, `rock_layer_thickness=8`, geology enabled, and realistic +coal disabled. Its exact rock order is Andesite, Basalt, Diorite, Granite, +Rhyolite, Pegmatite, Pumice; Shale, Conglomerate, Dolomite, Limestone, Marble, +Sandstone, Chert, Gypsum; Slate, Schist, Gneiss, Phyllite, Amphibolite. Known +saved-version and configuration signals select the matching lineage. A +genuinely ambiguous file on this target uses native 1.11 and records that choice +as a warning in the deterministic report. + +A carried 1.10 file can become hybrid after Mineralogy 1.12 normalizes its own +Forge configuration. In that case the saved world version takes precedence: +1.10 retains its realistic-coal behavior and does not invent a later enable +flag, while 1.12 respects its native enable flag and does not enable realistic +coal. If the config was not copied, the published defaults for the selected +lineage are recorded instead. An existing OS4 profile is never overwritten. +Fresh worlds do not select legacy mode merely because a legacy config is still +installed. + +The snapshot is stored at `/serverconfig/orespawn-worldgen.json` and a +human-readable explanation is written to +`/serverconfig/orespawn-upgrade-report.txt`. The report has no timestamp +and is byte-stable across reload. OreSpawn does not rewrite the source +Mineralogy config or generated chunks during this handoff; the installed +Mineralogy version may independently normalize its own Forge config. Moving an +upgraded world to Sky later remains possible as an explicit choice, with an +expected old/new chunk seam. + +Legacy OreSpawn conversion also writes `config/orespawn-upgrade-report.txt`. +It summarizes consumed resources, translated providers, preserved global +flags, and warnings; `config/orespawn-os3-migration-report.json` remains the +deterministic machine-readable detail. + +## Provider-Aware OS3 Imports + +When an OreSpawn 2/3 file is named for an installed provider, the migrator now +compares each converted primary output with that provider's ore declarations. +A unique output match is written under the provider's stable rule ID, allowing +provider merging to retain the migrated user values without adding a duplicate +default. Ambiguous and unmatched outputs keep their `orespawn:legacy/...` IDs +and are called out in `orespawn-migration/migration-report.txt` for manual +review. Files owned by mods without an active provider retain the original +legacy-ID behaviour. + +Migration is non-destructive. OreSpawn writes `config/orespawn-worldgen.json` +only when that target does not already exist and retains every source file. + +When `config/mineralogy-geomes.json` exists, OreSpawn imports the Mineralogy 6 +profile directly, updates its schema marker, and records `migrated_from`. + +Otherwise it scans `config/orespawn3/*.json` and `config/orespawn/*.json` for +legacy OreSpawn `version: "2.0"` spawn files. It converts: + +- default, vein, normal cloud, precision, cluster, and under-fluid patterns; +- weighted output blocks and replacement blocks; +- numeric Overworld, Nether, and End dimensions; +- biome ID and biome-dictionary include/exclude rules; +- frequency, size, height, spread, node, fluid, and retrogen settings. + +The OS3 default generator is migrated precisely: `size - variation` through +`size + variation - 1` becomes an inclusive `min_quantity`/`max_quantity` +range, while `variation: 0` remains a fixed `quantity`. Old `maxHeight` was +exclusive, so it becomes modern inclusive `max_y: maxHeight - 1`; a missing +maximum uses the old default 256 and becomes 255. Empty legacy dimension lists +become `orespawn:all_except_nether_end`, preserving their meaning of every +ordinary dimension while excluding the vanilla Nether and End. Quantities are +clamped to the supported 1-64 budget with a migration-report warning, and empty +height ranges are skipped and reported. Variation for other legacy pattern +types retains the documented approximation used by the existing importer. + +Compatible flags are also read from `config/orespawn.cfg`: vanilla/all ore +replacement, retrogen, forced retrogen, flat bedrock, and bedrock thickness. +Unknown numeric dimensions and obsolete block states are reported instead of +guessed. Review `config/orespawn-migration/migration-report.txt` after import. + +Global schemas 1-5 and world schemas 1-4 are upgraded in memory and persisted +where safe. A schema-1 world held only mode/oil/formation choices and is +overlaid on the effective installed-pack profile. A schema-2 world is already +a full snapshot and preserves its geology. Existing terrain is not rewritten. + +Provider schemas 1-3 remain readable. Schema 4 adds `biome_palettes`, +`dimension_materials`, and automatic fresh-world template metadata. Older +profiles receive empty biome/material sections, so migration cannot change +their terrain or biome output. Auto-selected templates are never applied to an +existing world profile during migration. + +The old `place_crude_oil` and singleton `oil` fields migrate to +`place_fluid_deposits` and a provider-owned rule. A valid block becomes +`:fluid_deposit/`; `minecraft:air` creates no rule. Mineralogy +profiles converge on `mineralogy:fluid_deposit/crude_oil` without duplicates. +One-time backups are written before persisted global or world migration. + +Unqualified built-in geome names remain accepted and normalize to +`orespawn:` internally. + +Managed vanilla-ore default revisions update only rules that still exactly +match an older OreSpawn default signature. Changed frequencies, ranges, +quantities, or patterns are treated as pack/player choices and are never +rewritten. + +World profiles are backed up beside the active file before an ore-default +revision is written. Revision 8 folds temporary Mineralogy-owned vanilla ore +IDs into OreSpawn's canonical IDs, preserving edited rules and removing +duplicate placement. Revision 9 recognizes the later-port deep-biased defaults +so a profile copied from a newer OreSpawn branch can still be migrated safely. + +On Minecraft 1.11.2, revision 10 converts only untouched managed-ore signatures +to the target's native height range, frequency, quantity, pattern, and exposure +behavior. It also restores the separate Badlands gold rule. Hand-edited rules +and explicitly stored Custom values are preserved. diff --git a/docs/PLAYER_GUIDE.md b/docs/PLAYER_GUIDE.md new file mode 100644 index 00000000..bd27be72 --- /dev/null +++ b/docs/PLAYER_GUIDE.md @@ -0,0 +1,110 @@ +# OreSpawn Player And Server Guide + +## The Short Version + +OreSpawn is an engine used by other mods. On its own it changes nothing. Mods +such as Mineralogy give it rocks, ores, and sensible default settings. + +For a normal game: + +1. Open **OreSpawn...** while creating the world. +2. Choose **Recommended Defaults** unless you want to customise geology. +3. Open **Help & Guide** for a plain-language tour of the controls. +4. Press **Done**, then create the world normally. + +Hover over unfamiliar controls for a short explanation. The same explanations +are collected in **Help & Guide**, so a setting can be learned either while +editing it or one topic at a time. + +There is no requirement to use rock strata. Ore-only mods can use OreSpawn to +place ores in ordinary vanilla stone while every geology control remains idle. +When no provider supplies rocks, **Configure Rock Strata...** starts with a +balanced editable set of vanilla stone, granite, diorite, and andesite. Other +vanilla terrain blocks keep their normal placement unless a player deliberately +adds them. You can remove the starter rocks or add blocks from installed mods +before creating the world. + +Mods can also offer new biomes and world materials without enabling strata. +Use **Biomes & World Materials** to inspect installed dimension palettes, +surface blocks, aquifer fluids, snow, and ice. The picker only accepts real +installed registry entries. Missing optional compatibility biomes are skipped +safely instead of breaking world creation. + +## What The Main Controls Mean + +- **Template** selects a complete setup supplied by OreSpawn, a mod, or a pack. +- **Sky** creates broad rock layers and geological regions called geomes. The + surface biome influences a geome without forcing identical borders. +- **Cyano (Legacy)** uses the older classic Mineralogy layer engine. +- **Formation Reach** controls how far rock formations extend sideways. +- **Layer Thickness** controls their vertical thickness. +- **Waviness** bends layers; **Edge Detail** roughens their boundaries. +- **Continuity** controls how often a formation keeps its identity across a + region. +- **Manage Vanilla Ores** lets OreSpawn replace vanilla ore features with the + configured OreSpawn rules. Leave it off to keep normal Minecraft placement. +- **Fluid Deposits** appears after strata are enabled or when a mod or pack + supplies a rule. Press **Add** to choose water, lava, or a fluid block from an + installed mod. These are covered underground deposits, not exposed vanilla + lakes. **Solid Cover** controls the roof thickness, while **Solid Shell** + prevents a deposit from opening into a cave at its sides or underside. +- **Biomes & World Materials** controls broad biome regions and what their + surfaces, underground water, snow, and ice are made from. **Augment** mixes + new biomes into the existing source; **Replace** creates a complete provider + style. Namespace scope protects other biome mods unless a pack deliberately + opts them in. + +**World Materials** applies across an entire dimension. **Aquifer Fluid** +changes the normal below-sea-level fluid. Minecraft 1.11.2 exposes only that +single generator fluid, so the later-format **Deep Aquifer** values remain +stored but their controls are disabled on this branch. Snow and ordinary ice +can also be replaced. Use **Fluid Deposits**, not World Materials, for +occasional underground lakes or pockets. + +## Rocks, Ores, And Other Mods + +The material picker lists blocks from installed mods by full registry ID, for +example `minecraft:granite` or `examplemod:slate`. **Safe Only** hides doors, +machines, and other blocks that are poor choices for underground terrain. + +Ore richness changes attempts per chunk. Each richness step halves or doubles +the installed default while preserving depth and deposit shape. Patterns decide +whether a deposit is compact, vein-like, clustered, cloud-like, or below a +fluid. Hosts decide which blocks, tags, or configured rock families it may +replace. + +Removing a rock from generation does not unregister its block or recipes. It +only prevents that rock appearing in newly generated terrain. + +## Existing Worlds And Servers + +Each world stores its final choices in: + +```text +/serverconfig/orespawn-worldgen.json +``` + +Changes normally affect only chunks generated afterward. Existing terrain is +not rewritten. Ore and flat-bedrock retrogen must be enabled deliberately; +rock strata are never retro-generated. + +For an existing Mineralogy 1.10, 1.11, or 1.12 world, OreSpawn automatically keeps +the matching **Cyano (Legacy)** layout when it creates that world's first OS4 +profile. The old layer sizes, rock lists, enabled state, and applicable coal +setting are copied into the world before new chunks generate. A carried 1.10 +config and native 1.11/1.12 configs are handled separately, even if an upgrade has +left both generations of keys in the file. Fresh worlds still use the current +recommended engine. + +After upgrading, review `config/orespawn-upgrade-report.txt` and +`/serverconfig/orespawn-upgrade-report.txt`. They explain which legacy +files were consumed, which settings were preserved, and any registry names or +rules needing attention. Changing an upgraded world to Sky is deliberate and +can make newly generated chunks look different from old ones. + +For a dedicated server, copy the whole world including that file and install +the same mods. Alternatively, place a prepared global profile at +`config/orespawn-worldgen.json` before creating a new server world. + +The server console commands `/orespawn status`, `/orespawn reload`, and +`/orespawn dump-biomes` help pack authors diagnose active providers and IDs. diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md new file mode 100644 index 00000000..f8422f56 --- /dev/null +++ b/docs/PROVIDERS.md @@ -0,0 +1,118 @@ +# Worldgen Providers + +Provider mods may contribute through Forge IMC, a packaged resource at +`assets//orespawn/provider.json`, or a pack override at +`config/-orespawn.json`. A valid override is authoritative. A +present malformed override leaves that provider inactive instead of silently +falling back. + +Provider schema 4 supports `profile_defaults`, `rocks`, `ores`, +`fluid_deposits`, `geomes`, `biome_rules`, `terrain_dimensions`, and +`templates`, plus `biome_palettes` and `dimension_materials`. Each file requires a +matching `provider_modid`, a positive `provider_revision`, and at least one +contribution. Legacy schemas 1-3 remain accepted; schema 3 introduced fluid +deposits and schema 4 introduces biome and world-material controls. + +An ore-only provider does not need rocks, geomes, or terrain dimensions. Give +each ore explicit host blocks or tags and OreSpawn will leave vanilla terrain, +including vanilla granite/diorite/andesite features, untouched. Formation +controls remain inert until a profile contains eligible rocks and an enabled +terrain-replacement dimension. + +Rule IDs in `rocks` and `ores` must use the provider namespace. They are stable +ownership keys, not necessarily block IDs. Set `block` for one output or +`outputs` for a weighted list: + +```json +"examplemod:ore/tin": { + "block": "examplemod:tin_ore", + "outputs": [ + { "block": "examplemod:tin_ore", "weight": 90 }, + { "block": "examplemod:rich_tin_ore", "weight": 10 } + ], + "enabled": true, + "dimension_selectors": { + "orespawn:all_except_nether_end": { + "enabled": true, + "min_y": 0, + "max_y": 95, + "frequency": 5.0, + "min_quantity": 4, + "max_quantity": 11, + "pattern": "default", + "host_tags": ["forge:stone"] + } + } +} +``` + +Fluid-deposit IDs also use the provider namespace. Their `block` may belong to +any installed mod, but it must be a real fluid block. Every enabled dimension +needs hosts and can independently set depth, attempts, lobe geometry, cover, +biome filters, and geome weights: + +```json +"examplemod:fluid_deposit/brine": { + "block": "examplemod:brine", + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "min_y": 0, + "max_y": 32, + "frequency": 0.05, + "min_radius": 4, + "max_radius": 10, + "min_vertical_radius": 2, + "max_vertical_radius": 4, + "max_lobes": 3, + "min_solid_cover": 2, + "min_solid_shell": 1, + "host_tags": ["forge:stone"] + } + } +} +``` + +An enabled ore dimension requires a Y range, expected attempts per chunk in +`frequency`, and at least one host family, host block, or host tag. Use +`quantity` for a fixed block budget, or use both `min_quantity` and +`max_quantity` for an inclusive random budget from 1 through 64. If a fixed +quantity and a complete range are both present, the range is authoritative; a +lone range bound is invalid. Host arrays accept either registry-ID strings or weighted +objects such as `{ "block": "minecraft:stone", "weight": 1.0 }` and +`{ "tag": "forge:stone", "weight": 0.5 }`. Biome include/exclude IDs and +Forge biome-dictionary names may further restrict a rule. + +For OS3-compatible placement in ordinary modded dimensions, put a rule under +`dimension_selectors.orespawn:all_except_nether_end`. It applies to every +dimension except `minecraft:the_nether` and `minecraft:the_end`. An explicit +entry in `dimensions` overrides the selector for that ore in the named +dimension, including an explicit disabled rule. This prevents duplicate +generation while allowing one dimension to use different height, quantity, or +host settings. + +Use `height_distribution` to select `uniform`, `triangle`, +`bottom_triangle`, or `uniform_bottom_triangle`. Set +`discard_chance_on_air_exposure` from 0 to 1 when some or all of an ore should +remain buried instead of appearing on cave walls. + +Only suppress a provider mod's native ore generation when +`OreSpawnApi.isOreTakeoverActive(modid)` returns true. `PENDING` means discovery +has not frozen. `INACTIVE` is the fail-safe and native generation must remain. + +Existing worlds merge newly introduced provider rule IDs but do not overwrite +world edits. Disabled and unassigned rules remain tombstones; removed provider +rules remain in the self-contained snapshot. + +Biome providers can add Forge biomes normally, then declare where those biomes +belong through `biome_palettes`. The overlay wraps the dimension's existing +biome source, so it composes after vanilla or another Forge 1.11 biome provider +instead of taking a compile-time dependency on it. Use +`minecraft_only` scope when the provider should leave other mods' biomes alone. +Use `required_similar_biomes` only when an output truly cannot work without a +referenced biome; ordinary compatibility hints belong in `similar_biomes`. + +See `examples/examplemod-orespawn.json` for rocks, weighted ore output, a +fluid deposit, a custom dimension, biome palette, world materials, and a +selectable template. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..e0e46029 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +# OreSpawn 4 Documentation + +OreSpawn is a required Forge mod and a declarative world-generation engine. +The normal jar is both the compile-time and runtime dependency; there is no +shaded or embeddable engine artifact. + +Choose the guide that matches what you are doing: + +- [Player and server guide](PLAYER_GUIDE.md) +- [Developer quick start and complete integration map](DEVELOPER_GUIDE.md) +- [Configuration field reference](CONFIGURATION.md) +- [Provider JSON files](PROVIDERS.md) +- [Java API and custom patterns](API.md) +- [Ore patterns and runtime tools](FEATURES.md) +- [Templates](TEMPLATES.md) +- [Biomes and world materials](BIOMES.md) +- [Dimensions](DIMENSIONS.md) +- [Migration](MIGRATION.md) +- [Troubleshooting](TROUBLESHOOTING.md) +- [Versioning and release identities](VERSIONS.md) +- [Compact instructions for coding agents](AGENTS.md) + +Validated examples are in `examples/`; JSON Schemas are in `schemas/`. +The provider and migration guides include OS3-compatible ranged quantities and +the `orespawn:all_except_nether_end` dimension selector. + +On first load OreSpawn copies this bundle to `config/orespawn-guide/`. Existing +exported files are never overwritten. Delete an exported file if you want the +current jar to restore that file on the next launch. diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md new file mode 100644 index 00000000..0eb7104b --- /dev/null +++ b/docs/TEMPLATES.md @@ -0,0 +1,26 @@ +# Geology Templates + +Templates are named profile overlays supplied by providers. They may set +formation presets, rocks, geomes, biome rules, ores, fluid deposits, suppression, +retrogen, flat bedrock, and terrain dimensions. A template may reference any +installed registry ID and list `required_mods`. + +Templates normally activate only when players select one in OreSpawn's Create +World screen. A total-conversion provider may mark a template +`auto_select:true` and set `auto_select_priority`. That template becomes the +default only for a fresh world when the global profile does not already name +`default_template`. The highest priority wins; equal priorities use lexical +template ID order and produce a setup warning. An explicit pack/server default +always wins. + +Template application happens after the global pack configuration and before +world-creation edits. The selected result is copied into the world profile. +Provider template changes never rewrite an existing world. + +The editor always applies template changes to the merged pack/provider baseline, +not on top of the previous template. Switching templates therefore cannot leave +stale rocks, biomes, fluids, or materials behind. Existing world snapshots never +auto-switch after installation or provider updates. + +Use namespaced template IDs such as `examplemod:ancient_sea`. Translation keys +for the selector belong to the provider resource pack. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 00000000..69fa71a8 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,65 @@ +# Troubleshooting + +## Provider remains inactive + +Check that the provider mod is loaded, file name and `provider_modid` match, +`provider_revision` is positive, provider rule IDs use its namespace, all +output blocks resolve, all pattern codecs decode, and every enabled ore or +terrain dimension has hosts. A malformed override deliberately prevents +fallback to packaged or API declarations. + +## OreSpawn installed alone does nothing + +That is the intended passive default. Install a provider such as Mineralogy, +add a provider file, or explicitly configure rocks/ores and terrain dimensions. + +## Custom terrain does not appear + +Confirm the dimension is enabled in `terrain_dimensions`, its hosts resolve, +at least one enabled rock includes that dimension, and biome restrictions +match. OreSpawn does not replace Nether or End terrain by default. + +## A custom biome does not appear + +Confirm its registry ID resolves, its palette targets the intended dimension, +the palette scope includes the source biome namespace, and the placement's +temperature/downfall ranges match. `similar_biomes` are optional comparisons; +missing IDs are ignored. Every ID in `required_similar_biomes` must exist or +OreSpawn deliberately skips that placement and logs one setup warning. + +If no palette matches, OreSpawn leaves the biome supplied by the wrapped +vanilla or modded biome source unchanged. Use `/orespawn dump-biomes` to inspect +the IDs available in the current installation. + +## Aquifer or surface materials do not change + +Confirm an enabled `dimension_materials` rule targets the exact dimension and +uses registered fluid-block IDs for aquifers. Surface substitutions are tied +to enabled palette entries and affect newly generated terrain only. Features +or structures that explicitly place blocks after terrain generation are not +global material replacements. + +## Changes do not affect terrain + +Restart after editing JSON or changing provider/API declarations. Travel to +new chunks. Ore retrogen and flat-bedrock retrogen must be explicitly enabled; +geological strata are never retrogened. + +## Server differs from a client test world + +Copy `/serverconfig/orespawn-worldgen.json`, not merely the global +client config. Install the same provider mods and blocks on the server. + +## Native ores duplicate + +Provider mods suppress native generation only after +`OreSpawnApi.isOreTakeoverActive(modid)` is true. Keep native generation for +`PENDING` and `INACTIVE`. For pack-wide vanilla or modded suppression, review +`manage_vanilla_ores` and `suppress_all_ore_features` carefully. + +## Operator diagnostics + +- `/orespawn status` shows active mode, rule/provider counts, and retrogen queue. +- `/orespawn reload` reloads providers and the active world profile. +- `/orespawn retrogen [radius]` queues currently loaded chunks only. +- `/orespawn dump-biomes` writes `config/orespawn-biomes.txt`. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md new file mode 100644 index 00000000..60931dbd --- /dev/null +++ b/docs/VERSIONS.md @@ -0,0 +1,215 @@ +# Mod Versioning Policy + +This document defines how versions are assigned to MMD mods and how an exact +Minecraft and loader target is encoded in a release version. + +## Version format + +Mod versions use four numeric components: + +```text +Major.Minor.Bug.Target +``` + +The first three components describe the functional release. For example, +OreSpawn `4.0.6` means major version 4, minor version 0, and bug revision 6. + +The fourth component identifies the Minecraft and loader target. A complete +release version such as `4.0.6.120061` therefore identifies both the OreSpawn +4.0.6 feature set and its Minecraft 1.20.6 Forge build. + +This is an expanded, Maven-compatible versioning convention. It is not strict +Semantic Versioning 2.0, which defines exactly three numeric core components. + +When the Major or Minor component increases, the functional components to its +right reset to zero. The Target component is then appended for the build being +released. For example: + +```text +4.0.6.120061 -> 4.1.0.120061 +4.1.3.120061 -> 5.0.0.120061 +``` + +## Target component + +The Target component is deterministic and is not another feature or bug +sequence number. + +To calculate it: + +1. Normalize the Minecraft version to `major.minor.patch`, using zero when the + patch component is omitted. +2. Concatenate the Minecraft major number without padding, the minor number as + two digits, the patch number as two digits, and the one-digit loader code. +3. Use loader code `1` for Forge and `2` for NeoForge. + +The component can be decoded from right to left: one loader digit, two patch +digits, two minor digits, and all remaining digits for the Minecraft major +version. + +Examples: + +| Minecraft | Loader | Target | Example full OreSpawn version | +| --- | --- | ---: | --- | +| 1.10.2 | Forge | `110021` | `4.0.6.110021` | +| 1.11.2 | Forge | `111021` | `4.0.16.111021` | +| 1.12.2 | Forge | `112021` | `4.0.16.112021` | +| 1.13.2 | Forge | `113021` | `4.0.6.113021` | +| 1.20.6 | Forge | `120061` | `4.0.6.120061` | +| 1.21.11 | Forge | `121111` | `4.0.6.121111` | +| 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | +| 26.2 | Forge | `2602001` | `4.0.6.2602001` | +| 26.2 | NeoForge | `2602002` | `4.0.6.2602002` | + +Historical MMD releases may also have four numeric components but may have used +the fourth component differently. This policy applies prospectively; it does +not reinterpret an old release number. + +## Major version + +Increase the **Major** number for a large-scale change, paradigm shift, or +breaking change that moves the mod forward in a fundamental way. + +Examples include: + +- Mineralogy 6 no longer containing its own world generation engine, unlike + Mineralogy 5. +- OreSpawn 4 gaining a complete terrain generation engine, including strata, + unlike OreSpawn 3. + +Compatibility adaptations required to support another Minecraft or loader +version do not by themselves require a major version increase when the mod's +supported behaviour and public contracts remain equivalent. + +## Minor version + +Increase the **Minor** number for a new feature or a significant change to +existing behaviour that does not justify a new major generation. + +Examples include: + +- adding a new player-usable block or other substantial feature; +- substantially overhauling a world-generation engine; +- making a significant fix or adjustment that materially changes how a major + part of the mod behaves. + +## Bug version + +Increase the **Bug** number for a bug fix or a very small feature that does not +materially change the mod's design. + +Examples include: + +- correcting a generation defect; +- fixing a user interface or compatibility problem; +- adding or correcting a language file translation; +- making a small documentation or configuration improvement that warrants a + release. + +This component is sometimes called the patch number in other versioning +systems. MMD uses the name **Bug** to make its intended purpose explicit. + +## Ports to new Minecraft versions + +Porting a mod to a new Minecraft version does not automatically change the +functional `Major.Minor.Bug` version. Functionally equivalent ports share those +first three components, while their complete versions have different Target +components. + +For example: + +```text +Minecraft 26.1.2 / Forge / OreSpawn 4.0.6.2601021 +Minecraft 26.2 / Forge / OreSpawn 4.0.6.2602001 +Minecraft 26.2 / NeoForge / OreSpawn 4.0.6.2602002 +``` + +Target-specific implementation details may differ internally where Minecraft +or its mod loader requires them. Those adaptations do not change the functional +version when users and integrations receive the same supported behaviour. + +If a port also introduces a feature or fix that changes the functional release, +the first three components must be assessed using the Major, Minor, and Bug +rules above. The Target component always identifies the build's actual +Minecraft and loader target. + +## Branch-specific fixes and skipped numbers + +Functional version numbers are allocated across the mod as a whole and must +not be reused for unrelated change sets on different Minecraft branches. The +same `Major.Minor.Bug` may be shared by functionally equivalent ports. + +If a released branch receives a bug fix that other branches do not require, +only the affected branch's Bug number is incremented. For example, Forge +1.12.2 moved from `4.0.6.112021` to `4.0.7.112021` for its target-only packaged +runtime repair while unaffected branches remained on their target-qualified +4.0.6 versions. + +If a different branch later receives a separate fix, it uses the next unused +Bug number, such as `4.0.8`, even if the `4.0.7` fix was not applicable to it. +Forge 1.11.2 and 1.12.2 then advance through 4.0.9 for safe natural-host +replacement and 4.0.10 for Stable Layers actual-height eligibility. Both use a +static, two-dimensional biome system, so the dynamic-registry and vertical sampler +repairs released as 4.0.11 and 4.0.12 are not applicable; they advance directly +to 4.0.13 for provider biome-filter parity and namespaced geome support, then to +4.0.14 so exposed one-layer Snow is included in configured weather-material +conversion. Forge 1.11 and 1.12 have no Y-sensitive biome-cell attribution or +server-side GameTest harness, so 4.0.15 and the GameTest lifecycle portion of 4.0.16 are not +applicable; it adopts the shared 4.0.16 identity while retaining ordinary +benchmark auto-stop. A branch may therefore legitimately skip functional version +numbers. + +This provides three useful guarantees: + +1. A functional version is not used to describe two unrelated change sets. +2. A higher functional version identifies a later change in the mod's release + history. +3. The Target component identifies the exact Minecraft and loader build without + overloading the functional version. + +A higher functional version on another Minecraft branch does **not** +necessarily mean it contains every lower-numbered branch-specific fix. Some +fixes are relevant only to a particular Minecraft or loader implementation. + +## Dependency ranges + +Dependencies should normally express the compatible functional release range. +For example, Maven-style range `[4.0.6,5.0.0)` deliberately accepts all +target-qualified OreSpawn 4.0.6 builds while excluding OreSpawn 5. + +Consumers must still declare their supported Minecraft version and loader in +their own metadata. The Target component makes that compatibility visible; it +does not replace loader-level compatibility checks. + +## Release and pull-request documentation + +Because maintained branches can legitimately contain different fixes, the +version number alone is not a substitute for release notes. + +Every release and pull request should state: + +- the Minecraft version and loader it targets; +- the complete four-component version and its functional `Major.Minor.Bug`; +- the features and fixes actually included; +- any fixes from nearby versions that are not applicable to that branch; +- whether the change is functionally equivalent to another maintained branch; +- any migration, compatibility, or configuration considerations for users. + +## Decision summary + +When assigning a version, ask the following questions in order: + +1. Is this a fundamental or breaking new generation of the mod? Increase + **Major**. +2. Is this a substantial feature or significant behavioural overhaul? Increase + **Minor**. +3. Is this a bug fix or very small feature? Increase **Bug**, using the next + unused number across the mod. +4. Is this only a functionally equivalent Minecraft or loader port? Keep the + existing `Major.Minor.Bug`. +5. Calculate and append the Target component for the exact Minecraft and loader + build. + +The objective is to make versions useful to players, pack developers, mod +integrators, release automation, and support teams while allowing each +maintained Minecraft branch to receive only the changes it actually needs. diff --git a/docs/examples/examplemod-orespawn.json b/docs/examples/examplemod-orespawn.json new file mode 100644 index 00000000..d850d2ef --- /dev/null +++ b/docs/examples/examplemod-orespawn.json @@ -0,0 +1,182 @@ +{ + "schema_version": 4, + "provider_modid": "examplemod", + "provider_revision": 1, + "rocks": { + "examplemod:rock/slate": { + "block": "examplemod:slate", + "enabled": true, + "family": "metamorphic", + "depth_peak": 20, + "depth_spread": 36, + "min_y": 0, + "max_y": 255, + "weight": 1.2, + "ore_replaceable": true, + "dimensions": ["minecraft:overworld"], + "geomes": { "orespawn:mountain_belt": 2.5 } + }, + "examplemod:rock/crystal_rock": { + "block": "examplemod:crystal_rock", + "enabled": true, + "family": "igneous_intrusive", + "depth_peak": 32, + "depth_spread": 64, + "min_y": 0, + "max_y": 255, + "weight": 1.0, + "ore_replaceable": true, + "dimensions": ["examplemod:crystal_caverns"] + } + }, + "ores": { + "examplemod:ore/tin": { + "block": "examplemod:tin_ore", + "outputs": [ + { "block": "examplemod:tin_ore", "weight": 90 }, + { "block": "examplemod:rich_tin_ore", "weight": 10, "min_y": 0, "max_y": 24 } + ], + "enabled": true, + "source_mod": "examplemod", + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "min_y": 0, + "max_y": 96, + "frequency": 6.0, + "quantity": 8, + "pattern": { + "type": "orespawn:clusters", + "settings": { + "spread": 8, + "vertical_spread": 4, + "node_size": 4, + "length": 16, + "fluid": "minecraft:water" + } + }, + "height_distribution": "triangle", + "discard_chance_on_air_exposure": 0.5, + "spread": 8, + "vertical_spread": 4, + "node_size": 4, + "host_families": ["sedimentary", "metamorphic", "igneous_intrusive"], + "host_blocks": [ + { "block": "minecraft:stone", "weight": 0.75 } + ] + } + } + } + }, + "fluid_deposits": { + "examplemod:fluid_deposit/brine": { + "enabled": true, + "block": "examplemod:brine", + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "min_y": 0, + "max_y": 32, + "frequency": 0.05, + "min_radius": 6, + "max_radius": 14, + "min_vertical_radius": 2, + "max_vertical_radius": 6, + "max_lobes": 4, + "min_solid_cover": 2, + "min_solid_shell": 1, + "host_families": ["sedimentary"], + "host_blocks": [], + "host_tags": ["forge:stone"], + "biome_ids": [], + "excluded_biome_ids": [], + "biome_dictionary": ["OCEAN"], + "excluded_biome_dictionary": [], + "geomes": {} + } + } + } + }, + "geomes": { + "examplemod:crystal_province": { + "base": 0.5, + "families": { + "sedimentary": 0.2, + "metamorphic": 1.0, + "igneous_intrusive": 3.0, + "igneous_volcanic": 0.5 + } + } + }, + "biome_rules": { + "minecraft:mountains": { + "examplemod:crystal_province": 2.5, + "orespawn:mountain_belt": 2.0 + } + }, + "terrain_dimensions": { + "examplemod:crystal_caverns": { + "enabled": true, + "biome_ids": [], + "biome_namespaces": ["examplemod"], + "host_blocks": ["examplemod:base_rock"], + "host_tags": [] + } + }, + "biome_palettes": { + "examplemod:crystal_caverns": { + "dimension": "examplemod:crystal_caverns", + "enabled": true, + "mode": "replace", + "scope": "all", + "region_size": "large", + "coverage": 1.0, + "fallback_weight": 0.0, + "include_namespaces": [], + "exclude_namespaces": [], + "biomes": { + "examplemod:crystal_forest": { + "enabled": true, + "weight": 3.0, + "similar_biomes": ["minecraft:forest"], + "required_similar_biomes": [], + "min_temperature": 0.2, + "max_temperature": 1.0, + "min_downfall": 0.4, + "max_downfall": 1.0, + "surface": { + "top_block": "examplemod:crystal_grass", + "filler_block": "examplemod:crystal_soil", + "underwater_block": "examplemod:crystal_sand", + "filler_depth": 3 + } + } + } + } + }, + "dimension_materials": { + "examplemod:crystal_caverns": { + "dimension": "examplemod:crystal_caverns", + "enabled": true, + "default_fluid": "examplemod:liquid_crystal", + "snow_block": "examplemod:crystal_snow", + "ice_block": "examplemod:crystal_ice" + } + }, + "templates": { + "examplemod:crystal_world": { + "name_key": "template.examplemod.crystal_world", + "description_key": "template.examplemod.crystal_world.description", + "required_mods": ["examplemod"], + "auto_select": false, + "auto_select_priority": 0, + "profile": { + "formations": { + "horizontal_size": "large", + "formation_continuity": "large" + }, + "manage_vanilla_ores": true + } + } + } +} diff --git a/docs/examples/orespawn-global.json b/docs/examples/orespawn-global.json new file mode 100644 index 00000000..cabcfee1 --- /dev/null +++ b/docs/examples/orespawn-global.json @@ -0,0 +1,51 @@ +{ + "schema_version": 6, + "geology_mode": "geome", + "place_fluid_deposits": true, + "manage_vanilla_ores": false, + "suppress_all_ore_features": false, + "default_template": "", + "formations": { + "algorithm": "stable_layers", + "horizontal_size": "average", + "vertical_thickness": "average", + "waviness": "average", + "edge_irregularity": "average", + "formation_continuity": "average", + "custom": {} + }, + "geomes": { + "stable_craton": { + "base": 1.0, + "families": { + "sedimentary": 1.0, + "metamorphic": 1.0, + "igneous_intrusive": 1.4, + "igneous_volcanic": 0.25 + } + } + }, + "biomes": {}, + "biome_dictionary": {}, + "rocks": {}, + "terrain_dimensions": {}, + "biome_palettes": {}, + "dimension_materials": {}, + "ores": {}, + "fluid_deposits": {}, + "retrogen": { + "enabled": false, + "force": false, + "revision": 0, + "chunks_per_tick": 1 + }, + "flat_bedrock": { + "enabled": false, + "retrogen": false, + "layers": 1, + "dimensions": ["minecraft:overworld", "minecraft:the_nether"] + }, + "worldgen_aliases": {}, + "providers": {}, + "ore_providers": {} +} diff --git a/docs/examples/orespawn-world.json b/docs/examples/orespawn-world.json new file mode 100644 index 00000000..93f51e30 --- /dev/null +++ b/docs/examples/orespawn-world.json @@ -0,0 +1,89 @@ +{ + "schema_version": 5, + "geology_mode": "geome", + "place_fluid_deposits": true, + "manage_vanilla_ores": false, + "suppress_all_ore_features": false, + "formations": { + "algorithm": "stable_layers", + "horizontal_size": "large", + "vertical_thickness": "average", + "waviness": "average", + "edge_irregularity": "average", + "formation_continuity": "large", + "custom": {} + }, + "geomes": { + "stable_craton": { + "base": 1.0, + "families": { + "sedimentary": 1.0, + "metamorphic": 1.0, + "igneous_intrusive": 1.4, + "igneous_volcanic": 0.25 + } + } + }, + "biomes": {}, + "biome_dictionary": {}, + "rocks": { + "minecraft:sandstone": { + "enabled": true, + "family": "sedimentary", + "depth_peak": 64, + "depth_spread": 40, + "min_y": 0, + "max_y": 255, + "weight": 1.0, + "ore_replaceable": true + }, + "minecraft:diorite": { + "enabled": true, + "family": "igneous_intrusive", + "depth_peak": 20, + "depth_spread": 36, + "min_y": 0, + "max_y": 255, + "weight": 1.0, + "ore_replaceable": true + }, + "minecraft:granite": { + "enabled": true, + "family": "igneous_intrusive", + "depth_peak": 12, + "depth_spread": 44, + "min_y": 0, + "max_y": 255, + "weight": 1.0, + "ore_replaceable": true + } + }, + "terrain_dimensions": { + "minecraft:overworld": { + "enabled": true, + "biome_ids": [], + "biome_namespaces": [], + "host_blocks": ["minecraft:stone"], + "host_tags": [] + } + }, + "biome_palettes": {}, + "dimension_materials": {}, + "ores": {}, + "fluid_deposits": {}, + "retrogen": { + "enabled": false, + "force": false, + "revision": 0, + "chunks_per_tick": 1 + }, + "flat_bedrock": { + "enabled": false, + "retrogen": false, + "layers": 1, + "dimensions": ["minecraft:overworld", "minecraft:the_nether"] + }, + "worldgen_aliases": {}, + "providers": {}, + "ore_providers": {} +} diff --git a/docs/schemas/orespawn-global.schema.json b/docs/schemas/orespawn-global.schema.json new file mode 100644 index 00000000..e20e5774 --- /dev/null +++ b/docs/schemas/orespawn-global.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mcmoddev.com/orespawn/schemas/orespawn-global.schema.json", + "allOf": [ + { "$ref": "profile-common.schema.json" }, + { + "type": "object", + "required": ["schema_version"], + "properties": { "schema_version": { "const": 6 } } + } + ] +} diff --git a/docs/schemas/orespawn-provider.schema.json b/docs/schemas/orespawn-provider.schema.json new file mode 100644 index 00000000..83f67f7b --- /dev/null +++ b/docs/schemas/orespawn-provider.schema.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mcmoddev.com/orespawn/schemas/orespawn-provider.schema.json", + "type": "object", + "required": ["schema_version", "provider_modid", "provider_revision"], + "properties": { + "schema_version": { "enum": [1, 2, 3, 4] }, + "provider_modid": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]+$" }, + "provider_revision": { "type": "integer", "minimum": 1 }, + "rocks": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/rock" } }, + "ores": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/ore" } }, + "fluid_deposits": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/fluidDeposit" } }, + "geomes": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/geome" } }, + "biome_rules": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/weights" } }, + "terrain_dimensions": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/terrainDimension" } }, + "biome_palettes": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/biomePalette" } }, + "dimension_materials": { "type": "object", "additionalProperties": { "$ref": "profile-common.schema.json#/$defs/dimensionMaterials" } }, + "profile_defaults": { "type": "object" }, + "templates": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["profile"], + "properties": { + "name_key": { "type": "string" }, + "description_key": { "type": "string" }, + "required_mods": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, + "auto_select": { "type": "boolean" }, + "auto_select_priority": { "type": "integer" }, + "profile": { "type": "object" } + } + } + } + }, + "allOf": [ + { + "if": { "properties": { "schema_version": { "const": 1 } } }, + "then": { + "required": ["ores"], + "properties": { + "ores": { "minProperties": 1 }, + "rocks": false, + "geomes": false, + "biome_rules": false, + "terrain_dimensions": false, + "fluid_deposits": false, + "biome_palettes": false, + "dimension_materials": false, + "templates": false, + "profile_defaults": false + } + } + }, + { + "if": { "properties": { "schema_version": { "enum": [1, 2] } } }, + "then": { "properties": { "fluid_deposits": false } } + }, + { + "if": { "properties": { "schema_version": { "enum": [1, 2, 3] } } }, + "then": { + "properties": { + "biome_palettes": false, + "dimension_materials": false + } + } + } + ], + "anyOf": [ + { "required": ["rocks"], "properties": { "rocks": { "minProperties": 1 } } }, + { "required": ["ores"], "properties": { "ores": { "minProperties": 1 } } }, + { "required": ["geomes"], "properties": { "geomes": { "minProperties": 1 } } }, + { "required": ["biome_rules"], "properties": { "biome_rules": { "minProperties": 1 } } }, + { "required": ["terrain_dimensions"], "properties": { "terrain_dimensions": { "minProperties": 1 } } }, + { "required": ["fluid_deposits"], "properties": { "fluid_deposits": { "minProperties": 1 } } }, + { "required": ["biome_palettes"], "properties": { "biome_palettes": { "minProperties": 1 } } }, + { "required": ["dimension_materials"], "properties": { "dimension_materials": { "minProperties": 1 } } }, + { "required": ["templates"], "properties": { "templates": { "minProperties": 1 } } } + ], + "additionalProperties": false +} diff --git a/docs/schemas/orespawn-world.schema.json b/docs/schemas/orespawn-world.schema.json new file mode 100644 index 00000000..f23b7970 --- /dev/null +++ b/docs/schemas/orespawn-world.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mcmoddev.com/orespawn/schemas/orespawn-world.schema.json", + "allOf": [ + { "$ref": "profile-common.schema.json" }, + { + "type": "object", + "required": ["schema_version"], + "properties": { "schema_version": { "const": 5 } } + } + ] +} diff --git a/docs/schemas/profile-common.schema.json b/docs/schemas/profile-common.schema.json new file mode 100644 index 00000000..87cefe57 --- /dev/null +++ b/docs/schemas/profile-common.schema.json @@ -0,0 +1,351 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mcmoddev.com/orespawn/schemas/profile-common.schema.json", + "type": "object", + "required": ["formations", "rocks", "geomes", "biomes", "terrain_dimensions", "biome_palettes", "dimension_materials", "ores", "fluid_deposits"], + "properties": { + "geology_mode": { "enum": ["geome", "legacy"] }, + "place_fluid_deposits": { "type": "boolean" }, + "manage_vanilla_ores": { "type": "boolean" }, + "suppress_all_ore_features": { "type": "boolean" }, + "default_template": { "$ref": "#/$defs/optionalRegistryId" }, + "selected_template": { "$ref": "#/$defs/registryId" }, + "formations": { "$ref": "#/$defs/formations" }, + "rocks": { "type": "object", "additionalProperties": { "$ref": "#/$defs/rock" } }, + "geomes": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/geome" } }, + "biomes": { "type": "object", "additionalProperties": { "$ref": "#/$defs/weights" } }, + "biome_dictionary": { "type": "object", "additionalProperties": { "$ref": "#/$defs/weights" } }, + "terrain_dimensions": { "type": "object", "additionalProperties": { "$ref": "#/$defs/terrainDimension" } }, + "biome_palettes": { "type": "object", "additionalProperties": { "$ref": "#/$defs/biomePalette" } }, + "dimension_materials": { "type": "object", "additionalProperties": { "$ref": "#/$defs/dimensionMaterials" } }, + "ores": { "type": "object", "additionalProperties": { "$ref": "#/$defs/ore" } }, + "fluid_deposits": { "type": "object", "additionalProperties": { "$ref": "#/$defs/fluidDeposit" } }, + "worldgen_aliases": { "type": "object", "additionalProperties": { "$ref": "#/$defs/registryId" } }, + "providers": { "type": "object" }, + "ore_providers": { "type": "object" } + ,"retrogen": { "$ref": "#/$defs/retrogen" } + ,"flat_bedrock": { "$ref": "#/$defs/flatBedrock" } + }, + "additionalProperties": true, + "$defs": { + "registryId": { "type": "string", "pattern": "^[a-z0-9_.-]+:[a-z0-9_./-]+$" }, + "optionalRegistryId": { "type": "string", "pattern": "^(|[a-z0-9_.-]+:[a-z0-9_./-]+)$" }, + "registryIds": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/registryId" } }, + "family": { "enum": ["sedimentary", "metamorphic", "igneous_intrusive", "igneous_volcanic"] }, + "preset": { "enum": ["tiny", "small", "average", "large", "huge", "custom"] }, + "weights": { "type": "object", "additionalProperties": { "type": "number", "minimum": 0 } }, + "metadata": { "type": "integer", "minimum": 0, "maximum": 15 }, + "weightedBlock": { + "type": "object", + "required": ["block"], + "properties": { + "block": { "$ref": "#/$defs/registryId" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "weight": { "type": "number", "minimum": 0, "maximum": 1 } + } + }, + "weightedTag": { + "type": "object", + "required": ["tag"], + "properties": { + "tag": { "$ref": "#/$defs/registryId" }, + "weight": { "type": "number", "minimum": 0, "maximum": 1 } + } + }, + "hostBlocks": { + "type": "array", + "items": { "anyOf": [ + { "$ref": "#/$defs/registryId" }, + { "$ref": "#/$defs/weightedBlock" } + ] } + }, + "hostTags": { + "type": "array", + "items": { "anyOf": [ + { "$ref": "#/$defs/registryId" }, + { "$ref": "#/$defs/weightedTag" } + ] } + }, + "formations": { + "type": "object", + "required": ["algorithm", "horizontal_size", "vertical_thickness", "waviness", "edge_irregularity", "formation_continuity", "custom"], + "properties": { + "algorithm": { "enum": ["stable_layers", "sky_v1"] }, + "horizontal_size": { "$ref": "#/$defs/preset" }, + "vertical_thickness": { "$ref": "#/$defs/preset" }, + "waviness": { "$ref": "#/$defs/preset" }, + "edge_irregularity": { "$ref": "#/$defs/preset" }, + "formation_continuity": { "$ref": "#/$defs/preset" }, + "custom": { "type": "object", "additionalProperties": { "type": "number" } } + } + }, + "rock": { + "type": "object", + "required": ["enabled", "family", "depth_peak", "depth_spread", "min_y", "max_y", "weight", "ore_replaceable"], + "properties": { + "block": { "$ref": "#/$defs/registryId" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "enabled": { "type": "boolean" }, + "family": { "$ref": "#/$defs/family" }, + "depth_peak": { "type": "integer", "minimum": -2048, "maximum": 2048 }, + "depth_spread": { "type": "integer", "minimum": 1 }, + "min_y": { "type": "integer", "minimum": -2048, "maximum": 2048 }, + "max_y": { "type": "integer", "minimum": -2048, "maximum": 2048 }, + "weight": { "type": "number", "minimum": 0 }, + "ore_replaceable": { "type": "boolean" }, + "geomes": { "$ref": "#/$defs/weights" }, + "dimensions": { "$ref": "#/$defs/registryIds" } + }, + "additionalProperties": true + }, + "geome": { + "type": "object", + "required": ["base", "families"], + "properties": { + "base": { "type": "number", "minimum": 0 }, + "families": { "type": "object", "additionalProperties": { "type": "number", "minimum": 0 } } + }, + "additionalProperties": true + }, + "terrainDimension": { + "type": "object", + "required": ["enabled", "host_blocks", "host_tags"], + "properties": { + "enabled": { "type": "boolean" }, + "biome_ids": { "$ref": "#/$defs/registryIds" }, + "biome_namespaces": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]+$" } }, + "host_blocks": { "$ref": "#/$defs/hostBlocks" }, + "host_tags": { "$ref": "#/$defs/hostTags" } + } + }, + "oreDimension": { + "type": "object", + "required": ["enabled", "min_y", "max_y", "frequency"], + "properties": { + "enabled": { "type": "boolean" }, + "min_y": { "type": "integer", "minimum": -2048, "maximum": 2048 }, + "max_y": { "type": "integer", "minimum": -2048, "maximum": 2048 }, + "frequency": { "type": "number", "minimum": 0, "maximum": 64 }, + "quantity": { "type": "integer", "minimum": 1, "maximum": 64 }, + "min_quantity": { "type": "integer", "minimum": 1, "maximum": 64 }, + "max_quantity": { "type": "integer", "minimum": 1, "maximum": 64 }, + "pattern": { "anyOf": [ + { "enum": ["default", "vein", "normal_cloud", "precision", "clusters", "underfluids", "cluster", "cloud"] }, + { + "type": "object", + "required": ["type", "settings"], + "properties": { + "type": { "$ref": "#/$defs/registryId" }, + "settings": { "type": "object" } + } + } + ] }, + "height_distribution": { + "enum": ["uniform", "triangle", "bottom_triangle", "uniform_bottom_triangle"] + }, + "discard_chance_on_air_exposure": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "spread": { "type": "integer", "minimum": 0, "maximum": 64 }, + "vertical_spread": { "type": "integer", "minimum": 0, "maximum": 64 }, + "node_size": { "type": "integer", "minimum": 1, "maximum": 32 }, + "length": { "type": "integer", "minimum": 1, "maximum": 64 }, + "fluid": { "$ref": "#/$defs/registryId" }, + "host_families": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/family" } }, + "host_blocks": { "$ref": "#/$defs/hostBlocks" }, + "host_tags": { "$ref": "#/$defs/hostTags" }, + "geomes": { "$ref": "#/$defs/weights" }, + "biome_ids": { "$ref": "#/$defs/registryIds" }, + "excluded_biome_ids": { "$ref": "#/$defs/registryIds" }, + "biome_dictionary": { "type": "array", "items": { "type": "string" } }, + "excluded_biome_dictionary": { "type": "array", "items": { "type": "string" } } + }, + "allOf": [ + { "anyOf": [ + { "required": ["quantity"] }, + { "required": ["min_quantity", "max_quantity"] } + ] }, + { "anyOf": [ + { "required": ["host_families"] }, + { "required": ["host_blocks"] }, + { "required": ["host_tags"] } + ] } + ], + "dependentRequired": { + "min_quantity": ["max_quantity"], + "max_quantity": ["min_quantity"] + }, + "additionalProperties": true + }, + "ore": { + "type": "object", + "required": ["enabled"], + "properties": { + "block": { "$ref": "#/$defs/registryId" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "enabled": { "type": "boolean" }, + "source_mod": { "type": "string" }, + "native_generation": { "type": "boolean" }, + "suppress_vanilla": { "type": "boolean" }, + "retrogen": { "type": "boolean" }, + "outputs": { + "type": "array", + "items": { + "type": "object", + "required": ["block", "weight"], + "properties": { + "block": { "$ref": "#/$defs/registryId" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "weight": { "type": "number", "minimum": 0 }, + "min_y": { "type": "integer" }, + "max_y": { "type": "integer" } + } + } + }, + "deep_output": { "$ref": "#/$defs/registryId" }, + "deep_output_metadata": { "$ref": "#/$defs/metadata" }, + "deep_output_max_y": { "type": "integer" }, + "dimensions": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/oreDimension" } }, + "dimension_selectors": { + "type": "object", + "minProperties": 1, + "properties": { + "orespawn:all_except_nether_end": { "$ref": "#/$defs/oreDimension" } + }, + "additionalProperties": false + } + }, + "anyOf": [ + { "required": ["dimensions"] }, + { "required": ["dimension_selectors"] } + ], + "additionalProperties": true + }, + "fluidDepositDimension": { + "type": "object", + "required": ["enabled", "min_y", "max_y", "frequency", "min_radius", "max_radius", "min_vertical_radius", "max_vertical_radius", "max_lobes", "min_solid_cover"], + "properties": { + "enabled": { "type": "boolean" }, + "min_y": { "type": "integer" }, "max_y": { "type": "integer" }, + "frequency": { "type": "number", "minimum": 0, "maximum": 64 }, + "min_radius": { "type": "integer", "minimum": 1, "maximum": 64 }, "max_radius": { "type": "integer", "minimum": 1, "maximum": 64 }, + "min_vertical_radius": { "type": "integer", "minimum": 1, "maximum": 64 }, "max_vertical_radius": { "type": "integer", "minimum": 1, "maximum": 64 }, + "max_lobes": { "type": "integer", "minimum": 1, "maximum": 16 }, "min_solid_cover": { "type": "integer", "minimum": 0, "maximum": 64 }, + "min_solid_shell": { "type": "integer", "minimum": 0, "maximum": 64, "default": 1 }, + "host_families": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/family" } }, + "host_blocks": { "$ref": "#/$defs/hostBlocks" }, + "host_tags": { "$ref": "#/$defs/hostTags" }, + "geomes": { "$ref": "#/$defs/weights" }, + "biome_ids": { "$ref": "#/$defs/registryIds" }, + "excluded_biome_ids": { "$ref": "#/$defs/registryIds" }, + "biome_dictionary": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "excluded_biome_dictionary": { "type": "array", "items": { "type": "string", "minLength": 1 } } + }, + "anyOf": [ + { "required": ["host_families"] }, + { "required": ["host_blocks"] }, + { "required": ["host_tags"] } + ], + "additionalProperties": true + }, + "fluidDeposit": { + "type": "object", + "required": ["enabled", "block", "dimensions"], + "properties": { + "enabled": { "type": "boolean" }, + "block": { "$ref": "#/$defs/registryId" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "dimensions": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/fluidDepositDimension" } } + }, + "additionalProperties": true + }, + "retrogen": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "force": { "type": "boolean" }, + "revision": { "type": "integer", "minimum": 0 }, + "chunks_per_tick": { "type": "integer", "minimum": 1, "maximum": 16 } + } + }, + "biomeSurface": { + "type": "object", + "properties": { + "top_block": { "$ref": "#/$defs/registryId" }, + "top_block_metadata": { "$ref": "#/$defs/metadata" }, + "filler_block": { "$ref": "#/$defs/registryId" }, + "filler_block_metadata": { "$ref": "#/$defs/metadata" }, + "underwater_block": { "$ref": "#/$defs/registryId" }, + "underwater_block_metadata": { "$ref": "#/$defs/metadata" }, + "ceiling_block": { "$ref": "#/$defs/registryId" }, + "ceiling_block_metadata": { "$ref": "#/$defs/metadata" }, + "filler_depth": { "type": "integer", "minimum": 0, "maximum": 16 } + }, + "additionalProperties": true + }, + "biomePlacement": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "weight": { "type": "number", "minimum": 0 }, + "similar_biomes": { "$ref": "#/$defs/registryIds" }, + "required_similar_biomes": { "$ref": "#/$defs/registryIds" }, + "min_temperature": { "type": "number", "minimum": -2, "maximum": 2 }, + "max_temperature": { "type": "number", "minimum": -2, "maximum": 2 }, + "min_downfall": { "type": "number", "minimum": 0, "maximum": 1 }, + "max_downfall": { "type": "number", "minimum": 0, "maximum": 1 }, + "surface": { "$ref": "#/$defs/biomeSurface" } + }, + "additionalProperties": true + }, + "biomePalette": { + "type": "object", + "required": ["dimension", "enabled", "mode", "scope", "region_size", "coverage", "fallback_weight", "biomes"], + "properties": { + "dimension": { "$ref": "#/$defs/registryId" }, + "enabled": { "type": "boolean" }, + "mode": { "enum": ["augment", "replace"] }, + "scope": { "enum": ["all", "minecraft_only", "selected_namespaces"] }, + "region_size": { "enum": ["tiny", "small", "average", "large", "huge"] }, + "coverage": { "type": "number", "minimum": 0, "maximum": 1 }, + "fallback_weight": { "type": "number", "minimum": 0 }, + "include_namespaces": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]+$" } }, + "exclude_namespaces": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]+$" } }, + "biomes": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/biomePlacement" } } + }, + "additionalProperties": true + }, + "dimensionMaterials": { + "type": "object", + "required": ["dimension", "enabled"], + "properties": { + "dimension": { "$ref": "#/$defs/registryId" }, + "enabled": { "type": "boolean" }, + "default_fluid": { "$ref": "#/$defs/registryId" }, + "deep_aquifer_fluid": { "$ref": "#/$defs/registryId" }, + "deep_aquifer_max_y": { "type": "integer", "minimum": -2048, "maximum": 2048 }, + "snow_block": { "$ref": "#/$defs/registryId" }, + "ice_block": { "$ref": "#/$defs/registryId" } + }, + "anyOf": [ + { "properties": { "enabled": { "const": false } }, "required": ["enabled"] }, + { "required": ["default_fluid"] }, + { "required": ["deep_aquifer_fluid"] }, + { "required": ["snow_block"] }, + { "required": ["ice_block"] } + ], + "additionalProperties": true + }, + "flatBedrock": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "retrogen": { "type": "boolean" }, + "layers": { "type": "integer", "minimum": 1, "maximum": 5 }, + "dimensions": { "$ref": "#/$defs/registryIds" } + } + } + } +} diff --git a/gradle.properties b/gradle.properties index 22b71b3c..05a8f5dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,22 +1,32 @@ -mc_version=1.12 -top_mc_version=1.11 -ccl_version=2.4.3.165 -ccc_version=2.3.5.93 -nei_version=2.1.2.175 -//jei_version=3.10.0.268 -//jei_version=3.12.8.328 -//jei_version=3.13.6.389 -jei_version=+ -mantle_version=1.1.3.199 -tconstruct_version=2.6.1.464 -mcmp_version=1.2.1 -//mcmp_version=1.3.0 -mcmp_experimental_version=2.0.0_88 -top_version=1.3.3-46 -orespawn_version=3.1.0+ -tesla_version=1.2.1.50 -bme_version=2.5.0-beta1 -mme_version=0.11.0 -fme_version=0.11.0 -bmi_version=0.11.0 -pa_version=2.3.0 \ No newline at end of file +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +# This is required to provide enough memory for the Minecraft decompilation process. +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false +org.gradle.configuration-cache=false +org.gradle.caching=true +org.gradle.parallel=false +net.minecraftforge.gradle.merge-source-sets=false + +minecraft_version=1.11.2 +minecraft_version_range=[1.11.2] +forge_version=13.20.1.2588 +forge_version_range=[13.20.1.2588,) +loader_version_range=[13,) +mapping_channel=stable +mapping_version=32-1.11 + +# Release metadata consumed by the generic dispatcher. +loader_name=forge +loader_code=1 +java_version=8 +java_toolchain_version=8.0.502+7 +gradle_java_version=17 +curseforge_project_id=245586 + +mod_id=orespawn +mod_name=MMD OreSpawn +mod_license=LGPL-2.1 +mod_version=4.0.16.111021 +mod_group=zone.moddev.mc.orespawn +mod_authors=SkyBlade1978, dshadowwolf, the MMD Team +mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a95009c3..2c68b418 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index e95643d6..f9553162 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e7080f2e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +plugins { + id('org.gradle.toolchains.foojay-resolver-convention') version '1.0.0' +} + +rootProject.name = 'OreSpawn' diff --git a/src/baseMetals111Fixture/java/com/mcmoddev/orespawn/BaseMetalsOreSpawn.java b/src/baseMetals111Fixture/java/com/mcmoddev/orespawn/BaseMetalsOreSpawn.java new file mode 100644 index 00000000..a71cdd01 --- /dev/null +++ b/src/baseMetals111Fixture/java/com/mcmoddev/orespawn/BaseMetalsOreSpawn.java @@ -0,0 +1,14 @@ +package com.mcmoddev.orespawn; + +import com.mcmoddev.orespawn.api.os3.OS3API; +import com.mcmoddev.orespawn.api.plugin.IOreSpawnPlugin; +import com.mcmoddev.orespawn.api.plugin.OreSpawnPlugin; + +/** Exact Forge 1.11 Base Metals OreSpawn plugin contract from commit f6ceb967. */ +@OreSpawnPlugin(modid = "basemetals", resourcePath = "orespawn") +public class BaseMetalsOreSpawn implements IOreSpawnPlugin { + @Override + public void register(OS3API apiInterface) { + // The historical provider is declarative and is read from the jar. + } +} diff --git a/src/baseMetals111Fixture/java/zone/moddev/mc/orespawn/basemetals111/BaseMetals111Fixture.java b/src/baseMetals111Fixture/java/zone/moddev/mc/orespawn/basemetals111/BaseMetals111Fixture.java new file mode 100644 index 00000000..d255641d --- /dev/null +++ b/src/baseMetals111Fixture/java/zone/moddev/mc/orespawn/basemetals111/BaseMetals111Fixture.java @@ -0,0 +1,30 @@ +package zone.moddev.mc.orespawn.basemetals111; + +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** Minimal block registry around the exact historical Base Metals provider. */ +@Mod(modid = BaseMetals111Fixture.MODID, name = "Base Metals 1.11 Provider Fixture", + version = "2.5.0-beta", acceptableRemoteVersions = "*") +@Mod.EventBusSubscriber(modid = BaseMetals111Fixture.MODID) +public final class BaseMetals111Fixture { + public static final String MODID = "basemetals"; + private static final String[] ORES = { + "coldiron_ore", "adamantine_ore", "starsteel_ore", "copper_ore", + "silver_ore", "tin_ore", "lead_ore", "zinc_ore", "mercury_ore", + "nickel_ore", "platinum_ore" + }; + + public BaseMetals111Fixture() { } + + @SubscribeEvent + public static void registerBlocks(RegistryEvent.Register event) { + for (String name : ORES) { + event.getRegistry().register(new Block(Material.ROCK) + .setRegistryName(MODID, name).setUnlocalizedName(MODID + "." + name)); + } + } +} diff --git a/src/baseMetals111Fixture/resources/assets/basemetals/orespawn/basemetals.json b/src/baseMetals111Fixture/resources/assets/basemetals/orespawn/basemetals.json new file mode 100644 index 00000000..00cdce6b --- /dev/null +++ b/src/baseMetals111Fixture/resources/assets/basemetals/orespawn/basemetals.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "dimensions": [ + { + "dimension": -1, + "ores": [ + {"block":"basemetals:coldiron_ore","parameters":{"size":8,"variation":4,"frequency":5.0,"minHeight":0,"maxHeight":128},"feature":"default","replace_block":"default"}, + {"block":"basemetals:adamantine_ore","parameters":{"size":8,"variation":4,"frequency":2.0,"minHeight":0,"maxHeight":128},"feature":"default","replace_block":"default"} + ] + }, + { + "dimension": 1, + "ores": [ + {"block":"basemetals:starsteel_ore","parameters":{"size":8,"variation":4,"frequency":5.0,"minHeight":0,"maxHeight":255},"feature":"default","replace_block":"default"} + ] + }, + { + "ores": [ + {"block":"basemetals:copper_ore","parameters":{"size":8,"variation":4,"frequency":10.0,"minHeight":0,"maxHeight":96},"feature":"default","replace_block":"default"}, + {"block":"basemetals:silver_ore","parameters":{"size":8,"variation":4,"frequency":4.0,"minHeight":0,"maxHeight":32},"feature":"default","replace_block":"default"}, + {"block":"basemetals:tin_ore","parameters":{"size":8,"variation":4,"frequency":10.0,"minHeight":0,"maxHeight":128},"feature":"default","replace_block":"default"}, + {"block":"basemetals:lead_ore","parameters":{"size":8,"variation":4,"frequency":5.0,"minHeight":0,"maxHeight":64},"feature":"default","replace_block":"default"}, + {"block":"basemetals:zinc_ore","parameters":{"size":8,"variation":4,"frequency":5.0,"minHeight":0,"maxHeight":96},"feature":"default","replace_block":"default"}, + {"block":"basemetals:mercury_ore","parameters":{"size":8,"variation":4,"frequency":3.0,"minHeight":0,"maxHeight":32},"feature":"default","replace_block":"default"}, + {"block":"basemetals:nickel_ore","parameters":{"size":8,"variation":4,"frequency":1.0,"minHeight":32,"maxHeight":96},"feature":"default","replace_block":"default"}, + {"block":"basemetals:platinum_ore","parameters":{"size":8,"variation":4,"frequency":0.125,"minHeight":1,"maxHeight":32},"feature":"default","replace_block":"default"} + ] + } + ] +} diff --git a/src/baseMetals111Fixture/resources/mcmod.info b/src/baseMetals111Fixture/resources/mcmod.info new file mode 100644 index 00000000..3e07d126 --- /dev/null +++ b/src/baseMetals111Fixture/resources/mcmod.info @@ -0,0 +1 @@ +[{"modid":"basemetals","name":"Base Metals 1.11 Provider Fixture","description":"Build-only historical provider fixture","version":"2.5.0-beta","mcversion":"1.11.2"}] diff --git a/src/baseMetals111Fixture/resources/pack.mcmeta b/src/baseMetals111Fixture/resources/pack.mcmeta new file mode 100644 index 00000000..d13bcb63 --- /dev/null +++ b/src/baseMetals111Fixture/resources/pack.mcmeta @@ -0,0 +1 @@ +{"pack":{"description":"Base Metals 1.11 provider fixture","pack_format":2}} diff --git a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java new file mode 100644 index 00000000..33bc23d3 --- /dev/null +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -0,0 +1,910 @@ +package zone.moddev.mc.orespawn.testmod; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.Set; +import java.util.LinkedHashSet; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import zone.moddev.mc.orespawn.api.BiomePlacementMode; +import zone.moddev.mc.orespawn.api.BiomeRegionSize; +import zone.moddev.mc.orespawn.api.BiomeReplacementScope; +import zone.moddev.mc.orespawn.api.GeologyFamily; +import zone.moddev.mc.orespawn.api.OrePatternType; +import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.orespawn.api.OreSpawnBiomes; +import zone.moddev.mc.orespawn.api.OreSpawnBiomes.BiomeReference; +import zone.moddev.mc.orespawn.api.OreSpawnBiomes.BiomeRegistrar; +import zone.moddev.mc.orespawn.api.OreSpawnPatternRegistry; +import zone.moddev.mc.orespawn.api.ProviderStatus; +import zone.moddev.mc.orespawn.api.StandardPatternSettings; +import zone.moddev.mc.orespawn.api.WorldgenProvider; +import zone.moddev.mc.orespawn.api.WorldgenProvider.BiomeSurfaceDefinition; +import zone.moddev.mc.orespawn.api.WorldgenProvider.TerrainDimensionDefinition; +import zone.moddev.mc.orespawn.init.OreSpawnPatterns; +import zone.moddev.mc.orespawn.worldgen.SurfaceProbeSpringBridge; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockDynamicLiquid; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tileentity.TileEntityChest; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeDecorator; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.gen.ChunkProviderFlat; +import net.minecraft.world.chunk.IChunkGenerator; +import net.minecraftforge.common.BiomeDictionary; +import net.minecraftforge.common.DimensionManager; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.event.terraingen.DecorateBiomeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.FMLServerAboutToStartEvent; +import net.minecraftforge.fml.common.event.FMLServerStartedEvent; +import net.minecraftforge.fml.common.eventhandler.EventPriority; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import net.minecraftforge.fml.common.registry.GameRegistry; +import net.minecraftforge.fml.common.IWorldGenerator; +import net.minecraftforge.fml.common.FMLCommonHandler; + +/** Independent Forge 1.11 provider-surface and exact-biome regression fixture. */ +@Mod(modid = SurfaceProbeTestMod.MODID, name = "OreSpawn Surface Probe", + version = "1.0.0", acceptedMinecraftVersions = "[1.11.2]", + dependencies = "required-after:orespawn@[4.0.6,5.0.0)") +public final class SurfaceProbeTestMod { + static final String MODID = "surfaceprobe"; + private static final Logger LOGGER = LogManager.getLogger(); + private static final ResourceLocation END = new ResourceLocation("minecraft", "the_end"); + private static final ResourceLocation NETHER = new ResourceLocation("minecraft", "the_nether"); + private static final ResourceLocation OVERWORLD = new ResourceLocation("minecraft", "overworld"); + private static final ResourceLocation BIOME_A = new ResourceLocation(MODID, "surface_a"); + private static final ResourceLocation BIOME_B = new ResourceLocation(MODID, "surface_b"); + private static final ResourceLocation PROBE_GEOME = new ResourceLocation(MODID, "exact_biome"); + private static final ResourceLocation SPRING_ROCK = new ResourceLocation(MODID, "rock/spring_host"); + private static final ProbeLiquid DEPOSIT_FLUID = new ProbeLiquid(); + private static final BlockPos SPRING_POS = new BlockPos(1128, 32, 1128); + private static final IBlockState[] NATURAL_SOURCES = { + Blocks.DIRT.getStateFromMeta(0), Blocks.GRASS.getDefaultState(), + Blocks.DIRT.getStateFromMeta(1), Blocks.DIRT.getStateFromMeta(2), + Blocks.GRAVEL.getDefaultState(), Blocks.SAND.getStateFromMeta(0), + Blocks.SAND.getStateFromMeta(1), Blocks.CLAY.getDefaultState(), + Blocks.HARDENED_CLAY.getDefaultState(), + Blocks.STAINED_HARDENED_CLAY.getStateFromMeta(0), + Blocks.STAINED_HARDENED_CLAY.getStateFromMeta(1), + Blocks.STAINED_HARDENED_CLAY.getStateFromMeta(14) + }; + private static final int MIN_CHUNK = 63; + private static final int MAX_CHUNK = 65; + private static final int COLUMNS = 9 * 16 * 16; + private static final int FILLER = COLUMNS * 3; + private static final int NATURAL_SOURCE_COUNT = 9 * NATURAL_SOURCES.length; + private static final int GROUND_Y = 200; + private static final int MARKER_Y = GROUND_Y - 5; + private static final int ROOF_UNDERSIDE_Y = 220; + private static final int ROOF_TOP_Y = 222; + private static final int GEOLOGY_MIN_Y = 20; + private static final int GEOLOGY_MAX_Y = 22; + private static final String PHASE_PROPERTY = "surfaceprobe.integrationPhase"; + private static final String MARKER_NAME = "surfaceprobe-integration.properties"; + private static final String CHEST_ITEM_NAME = "surfaceprobe sentinel"; + private static final String RAW_CHEST_ITEM_NAME = "surfaceprobe raw block entity sentinel"; + private static final Block WEATHER_SNOW_REPLACEMENT = Blocks.WOOL; + private static final Block WEATHER_ICE_REPLACEMENT = Blocks.PACKED_ICE; + private static final ResourceLocation[] BUILT_IN_GEOMES = { + new ResourceLocation("orespawn", "stable_craton"), + new ResourceLocation("orespawn", "mountain_belt"), + new ResourceLocation("orespawn", "volcanic_arc"), + new ResourceLocation("orespawn", "sedimentary_basin"), + new ResourceLocation("orespawn", "coastal_shelf"), + new ResourceLocation("orespawn", "arid_basin"), + new ResourceLocation("orespawn", "wetland_basin"), + new ResourceLocation("orespawn", "glacial_highland") + }; + + private static OrePatternType externalPattern; + + private final BiomeRegistrar registrar = OreSpawnBiomes.registrar(MODID); + private final BiomeReference surfaceA = OreSpawnBiomes.blankAndRegister(registrar, + "surface_a", properties -> configure(properties, 1.35F, 0.15F)); + private final BiomeReference surfaceB = OreSpawnBiomes.blankAndRegister(registrar, + "surface_b", properties -> configure(properties, 0.7F, 0.8F)); + private final Set preparedTerrain = new LinkedHashSet<>(); + + public SurfaceProbeTestMod() { + MinecraftForge.EVENT_BUS.register(this); + } + + private static void configure(Biome.BiomeProperties properties, + float temperature, float rainfall) { + properties.setBaseHeight(0.1F).setHeightVariation(0.2F) + .setTemperature(temperature).setRainfall(rainfall) + .setWaterColor(4159204); + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void registerBiomes(RegistryEvent.Register event) { + surfaceA.get().decorator = new ProbeDecorator(); + surfaceB.get().decorator = new ProbeDecorator(); + BiomeDictionary.addTypes(surfaceA.get(), BiomeDictionary.Type.HOT, BiomeDictionary.Type.DRY); + BiomeDictionary.addTypes(surfaceB.get(), BiomeDictionary.Type.HOT, BiomeDictionary.Type.WET); + } + + @SubscribeEvent + public void registerBlocks(RegistryEvent.Register event) { + event.getRegistry().register(DEPOSIT_FLUID); + } + + @EventHandler + public void preInit(FMLPreInitializationEvent event) { + // Forge 13 creates OreSpawn's custom registry during OreSpawn pre-init and + // does not emit a later Register event for it. Construct and register the + // external fixture type only after that registry exists. + externalPattern = OrePatternType.create( + StandardPatternSettings.CODEC, settings -> context -> false) + .setRegistryName(MODID, "external_probe"); + OreSpawnPatterns.registry().register(externalPattern); + GameRegistry.registerWorldGenerator(new ProbeGenerator(), 1000); + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void placeControlledTerrain(DecorateBiomeEvent.Pre event) { + World world = event.getWorld(); + int chunkX = event.getPos().getX() >> 4; + int chunkZ = event.getPos().getZ() >> 4; + if ((world.provider.getDimension() != 1 && world.provider.getDimension() != -1) + || chunkX < MIN_CHUNK || chunkX > MAX_CHUNK + || chunkZ < MIN_CHUNK || chunkZ > MAX_CHUNK) return; + String key = world.provider.getDimension() + ":" + chunkX + ":" + chunkZ; + if (!preparedTerrain.add(key)) return; + Chunk chunk = world.getChunkProvider().provideChunk(chunkX, chunkZ); + ProbeGenerator.placeTerrain(chunk, chunkX << 4, chunkZ << 4, + world.provider.getDimension() == -1); + if (world.provider.getDimension() == 1) { + placeRawNaturalSources(world, chunk, chunkX << 4, chunkZ << 4); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void verifySurfaceStage(DecorateBiomeEvent.Pre event) { + World world = event.getWorld(); + int chunkX = event.getPos().getX() >> 4; + int chunkZ = event.getPos().getZ() >> 4; + if ((world.provider.getDimension() != 1 && world.provider.getDimension() != -1) + || chunkX < MIN_CHUNK || chunkX > MAX_CHUNK + || chunkZ < MIN_CHUNK || chunkZ > MAX_CHUNK) return; + BlockPos pos = new BlockPos(chunkX << 4, GROUND_Y, chunkZ << 4); + if (world.getBlockState(pos).getBlock() == Blocks.GRASS) { + throw new IllegalStateException("OreSpawn surface did not run within the native decoration stage at " + + pos + " in dimension " + world.provider.getDimension()); + } + } + + @EventHandler + public void init(FMLInitializationEvent event) { + WorldgenProvider.Builder provider = WorldgenProvider.builder(MODID, 1); + addGeology(provider); + provider.fluidDeposit(new ResourceLocation(MODID, "fluid_deposit/dynamic_tick_probe"), + id(DEPOSIT_FLUID), deposit -> deposit.dimension(OVERWORLD, + dimension -> dimension.hostBlock(id(Blocks.STONE)))); + // This stable palette id makes seed zero select both fixture biomes on + // opposite sides of the 1,024-block Tiny-region boundary. + addPalette(provider, "end_palette_1", END, false); + addPalette(provider, "nether_palette_1", NETHER, true); + provider.dimensionMaterials(new ResourceLocation(MODID, "materials/end"), END, + materials -> materials.snowBlock(id(WEATHER_SNOW_REPLACEMENT)) + .iceBlock(id(WEATHER_ICE_REPLACEMENT))); + if (!OreSpawnApi.enqueue(provider.build())) { + throw new IllegalStateException("Could not enqueue the surfaceprobe provider"); + } + } + + private static void addGeology(WorldgenProvider.Builder provider) { + provider.geome(PROBE_GEOME, geome -> geome.baseWeight(0.0D) + .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); + provider.rock(new ResourceLocation(MODID, "rock/exact_biome"), id(Blocks.PRISMARINE), + GeologyFamily.SEDIMENTARY, rock -> { + rock.dimension(END).yRange(0, 255).geomeWeight(PROBE_GEOME, 1.0D); + for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); + }); + provider.rock(new ResourceLocation(MODID, "rock/fallback"), id(Blocks.NETHERRACK), + GeologyFamily.SEDIMENTARY, rock -> { + rock.dimension(END).yRange(0, 255).geomeWeight(PROBE_GEOME, 0.0D); + for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 1.0D); + }); + provider.rock(SPRING_ROCK, id(Blocks.PURPUR_BLOCK), + GeologyFamily.IGNEOUS_INTRUSIVE, rock -> { + rock.dimension(OVERWORLD).yRange(0, 0).weight(0.000001D); + for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 1.0D); + }); + provider.biome(BIOME_A, Collections.singletonMap(PROBE_GEOME, 100.0D)); + provider.biome(BIOME_B, Collections.singletonMap(PROBE_GEOME, 100.0D)); + TerrainDimensionDefinition.Builder terrain = TerrainDimensionDefinition.builder(END) + .biomeNamespace(MODID).hostBlock(id(Blocks.END_STONE)); + for (Block block : Arrays.asList(Blocks.DIRT, Blocks.GRASS, Blocks.GRAVEL, + Blocks.SAND, Blocks.CLAY, Blocks.HARDENED_CLAY, + Blocks.STAINED_HARDENED_CLAY)) { + terrain.hostBlock(id(block)); + } + provider.terrainDimension(terrain.build()); + } + + @EventHandler + public void serverAboutToStart(FMLServerAboutToStartEvent event) { + Path profile = worldRoot(event.getServer()).resolve("serverconfig") + .resolve("orespawn-worldgen.json"); + JsonObject root; + try (BufferedReader reader = Files.newBufferedReader(profile)) { + root = new JsonParser().parse(reader).getAsJsonObject(); + } catch (IOException | RuntimeException exception) { + throw new IllegalStateException("Could not read the test-owned End geology profile", exception); + } + try { + root.addProperty("place_fluid_deposits", true); + JsonObject terrain = root.getAsJsonObject("terrain_dimensions"); + JsonObject end = terrain.getAsJsonObject(END.toString()); + JsonArray hosts = end.getAsJsonArray("host_blocks"); + for (Block block : Arrays.asList(Blocks.AIR, Blocks.WATER, Blocks.BEDROCK, Blocks.CHEST)) { + zone.moddev.mc.orespawn.util.JsonCopies.add(hosts, id(block).toString()); + } + try (BufferedWriter writer = Files.newBufferedWriter(profile)) { + new GsonBuilder().setPrettyPrinting().create().toJson(root, writer); + } + } catch (IOException | RuntimeException exception) { + throw new IllegalStateException("Could not write the test-owned End geology profile", exception); + } + if (!WorldGeologyProfileManager.reloadActiveProfile()) { + throw new IllegalStateException("Could not reload the test-owned End geology profile"); + } + } + + private static void addPalette(WorldgenProvider.Builder provider, String name, + ResourceLocation dimension, boolean ceiling) { + BiomeSurfaceDefinition a = surface(Blocks.EMERALD_BLOCK, Blocks.QUARTZ_BLOCK, + Blocks.LAPIS_BLOCK, ceiling ? Blocks.IRON_BLOCK : null); + BiomeSurfaceDefinition b = surface(Blocks.DIAMOND_BLOCK, Blocks.REDSTONE_BLOCK, + Blocks.COAL_BLOCK, ceiling ? Blocks.GOLD_BLOCK : null); + provider.biomePalette(new ResourceLocation(MODID, name), dimension, + palette -> palette.mode(BiomePlacementMode.REPLACE) + .scope(BiomeReplacementScope.MINECRAFT_ONLY) + .regionSize(BiomeRegionSize.TINY).coverage(1.0D).fallbackWeight(0.0D) + .biome(BIOME_A, biome -> biome.weight(1.0D) + .temperature(-2.0D, 2.0D).downfall(0.0D, 1.0D).surface(a)) + .biome(BIOME_B, biome -> biome.weight(1.0D) + .temperature(-2.0D, 2.0D).downfall(0.0D, 1.0D).surface(b))); + } + + private static BiomeSurfaceDefinition surface(Block top, Block filler, + Block underwater, Block ceiling) { + BiomeSurfaceDefinition.Builder builder = BiomeSurfaceDefinition.builder() + .topBlock(id(top)).fillerBlock(id(filler)).underwaterBlock(id(underwater)) + .fillerDepth(3); + if (ceiling != null) builder.ceilingBlock(id(ceiling)); + return builder.build(); + } + + private static ResourceLocation id(Block block) { + ResourceLocation id = ForgeRegistries.BLOCKS.getKey(block); + if (id == null) throw new IllegalStateException("Unregistered fixture block " + block); + return id; + } + + @EventHandler + public void serverStarted(FMLServerStartedEvent event) { + String phase = System.getProperty(PHASE_PROPERTY, "").trim(); + if (!"fresh".equals(phase) && !"reload".equals(phase)) { + throw new IllegalStateException("Missing or invalid " + PHASE_PROPERTY + ": " + phase); + } + if (OreSpawnApi.getProviderStatus(MODID) != ProviderStatus.ACTIVE) { + throw new IllegalStateException("surfaceprobe provider is not active"); + } + verifyPatterns(); + MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance(); + WorldServer overworld = server.getWorld(0); + if (overworld == null || overworld.getSeed() != 0L) { + throw new IllegalStateException("surfaceprobe requires seed token zsjpxah (hash zero)"); + } + Path marker = worldRoot(server).resolve(MARKER_NAME); + Properties previous = "reload".equals(phase) ? read(marker) : null; + if ("fresh".equals(phase) && Files.exists(marker)) { + throw new IllegalStateException("Fresh surfaceprobe retained an old marker"); + } + Map results = new LinkedHashMap<>(); + results.put("end", audit(requireWorld(server, 1), false)); + results.put("nether", audit(requireWorld(server, -1), true)); + ResourceLocation spring = auditSpring(overworld, phase); + int dynamicFluidPlacements = DEPOSIT_FLUID.placements(); + if ("fresh".equals(phase) && dynamicFluidPlacements <= 0) { + throw new IllegalStateException("OreSpawn did not place the dynamic fluid-deposit probe"); + } + if ("reload".equals(phase) && dynamicFluidPlacements != 0) { + throw new IllegalStateException("Reload generated new dynamic fluid deposits: " + + dynamicFluidPlacements); + } + Properties current = properties(overworld.getSeed(), results, spring); + current.setProperty("dynamic_fluid_placements", "fresh".equals(phase) + ? Integer.toString(dynamicFluidPlacements) + : previous.getProperty("dynamic_fluid_placements")); + if (previous == null) { + write(marker, current); + } else { + for (String key : current.stringPropertyNames()) { + if (!current.getProperty(key).equals(previous.getProperty(key))) { + throw new IllegalStateException("Reload changed " + key + ": expected " + + previous.getProperty(key) + " but found " + current.getProperty(key)); + } + } + previous.setProperty("reload_verified", "true"); + write(marker, previous); + } + LOGGER.info("SURFACEPROBE PASS phase={} end={} nether={}", + phase, results.get("end"), results.get("nether")); + server.initiateShutdown(); + } + + private static WorldServer requireWorld(MinecraftServer server, int dimension) { + WorldServer world = server.getWorld(dimension); + if (world == null) { + DimensionManager.initDimension(dimension); + world = DimensionManager.getWorld(dimension); + } + if (world == null) throw new IllegalStateException("Missing dimension " + dimension); + if (world.getChunkProvider().chunkGenerator instanceof ChunkProviderFlat) { + throw new IllegalStateException("surfaceprobe requires normal-noise dimension " + dimension); + } + return world; + } + + private static Audit audit(WorldServer world, boolean roofed) { + long dry = 0, wet = 0, filler = 0, geology = 0, ceiling = 0, roof = 0; + long rawNatural = 0, structureNatural = 0, vegetationNatural = 0; + long cavePockets = 0, underwaterPockets = 0, rawBedrock = 0, rawBlockEntities = 0; + long exposedSnow = 0, surfaceIce = 0, buriedSnow = 0, buriedIce = 0; + long unconfiguredSnow = 0, unconfiguredIce = 0; + int biomeA = 0, biomeB = 0, edges = 0, sentinels = 0; + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + loadPopulationBorder(world); + for (int chunkZ = MIN_CHUNK; chunkZ <= MAX_CHUNK; chunkZ++) { + for (int chunkX = MIN_CHUNK; chunkX <= MAX_CHUNK; chunkX++) { + Chunk chunk = world.getChunkProvider().provideChunk(chunkX, chunkZ); + if (!chunk.isTerrainPopulated()) { + throw new IllegalStateException("Normal chunk population did not complete at " + + chunkX + "," + chunkZ); + } + int minX = chunkX << 4, minZ = chunkZ << 4; + zone.moddev.mc.orespawn.worldgen.WorldMaterialWeather.onChunkLoad( + new net.minecraftforge.event.world.ChunkEvent.Load(chunk)); + for (int localZ = 0; localZ < 16; localZ++) { + for (int localX = 0; localX < 16; localX++) { + int x = minX + localX, z = minZ + localZ; + int ground = markedGround(chunk, cursor, x, z); + Biome biome = world.getBiome(cursor.setPos(x, ground, z)); + ResourceLocation biomeId = biome.getRegistryName(); + Material material = material(biomeId, roofed); + float temperature = BIOME_A.equals(biomeId) ? 1.35F : 0.7F; + float rainfall = BIOME_A.equals(biomeId) ? 0.15F : 0.8F; + if (Float.compare(biome.getTemperature(), temperature) != 0 + || Float.compare(biome.getRainfall(), rainfall) != 0) { + throw new IllegalStateException("Climate mismatch for " + biomeId + " at " + cursor); + } + if (BIOME_A.equals(biomeId)) biomeA++; else biomeB++; + if (x > (MIN_CHUNK << 4) + && !biomeId.equals(world.getBiome(cursor.setPos(x - 1, ground, z)).getRegistryName())) edges++; + if (z > (MIN_CHUNK << 4) + && !biomeId.equals(world.getBiome(cursor.setPos(x, ground, z - 1)).getRegistryName())) edges++; + boolean underwater = localX == 1 && localZ == 1; + Block expectedTop = underwater ? material.underwater : material.top; + assertBlock(chunk, cursor, x, ground, z, + expectedTop, "surface top"); + if (underwater) wet++; else dry++; + for (int depth = 1; depth <= 3; depth++) { + assertBlock(chunk, cursor, x, ground - depth, z, material.filler, + "surface filler " + depth); + filler++; + } + if (!roofed) { + for (int y = GEOLOGY_MIN_Y; y <= GEOLOGY_MAX_Y; y++) { + assertBlock(chunk, cursor, x, y, z, Blocks.PRISMARINE, "exact-biome geology"); + geology++; + } + } else { + assertBlock(chunk, cursor, x, ROOF_UNDERSIDE_Y, z, material.ceiling, "roof underside"); + assertBlock(chunk, cursor, x, ROOF_TOP_Y, z, Blocks.STONE, "roof top"); + ceiling++; roof++; + } + } + } + sentinels += auditSentinels(world, minX, minZ); + if (!roofed) { + NaturalSourceAudit natural = auditNaturalSources(world, chunk, cursor, minX, minZ); + rawNatural += natural.rawConverted; + structureNatural += natural.structurePreserved; + vegetationNatural += natural.vegetationPreserved; + cavePockets += natural.cavePreserved; + underwaterPockets += natural.underwaterPreserved; + rawBedrock += natural.bedrockPreserved; + rawBlockEntities += natural.blockEntityPreserved; + } + WeatherMaterialAudit weather = auditWeatherMaterials(chunk, cursor, minX, minZ, roofed); + exposedSnow += weather.exposedSnowConverted; + surfaceIce += weather.surfaceIceConverted; + buriedSnow += weather.buriedSnowPreserved; + buriedIce += weather.buriedIcePreserved; + unconfiguredSnow += weather.unconfiguredSnowPreserved; + unconfiguredIce += weather.unconfiguredIcePreserved; + } + } + if (dry != COLUMNS - 9 || wet != 9 || filler != FILLER || biomeA == 0 || biomeB == 0 + || edges == 0 || sentinels != 36 || geology != (roofed ? 0 : FILLER) + || (roofed && (ceiling != COLUMNS || roof != COLUMNS + || unconfiguredSnow != 9 || unconfiguredIce != 9 + || exposedSnow != 0 || surfaceIce != 0 || buriedSnow != 0 || buriedIce != 0)) + || (!roofed && (rawNatural != NATURAL_SOURCE_COUNT + || structureNatural != NATURAL_SOURCE_COUNT + || vegetationNatural != NATURAL_SOURCE_COUNT + || cavePockets != NATURAL_SOURCE_COUNT / 2 + || underwaterPockets != NATURAL_SOURCE_COUNT / 2 + || rawBedrock != 9 || rawBlockEntities != 9 + || exposedSnow != 9 || surfaceIce != 9 + || buriedSnow != 9 || buriedIce != 9 + || unconfiguredSnow != 0 || unconfiguredIce != 0))) { + throw new IllegalStateException("Incomplete surface audit: dry=" + dry + ", wet=" + wet + + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + + ", edges=" + edges + ", sentinels=" + sentinels + ", geology=" + geology + + ", ceiling=" + ceiling + ", roof=" + roof + + ", rawNatural=" + rawNatural + + ", structureNatural=" + structureNatural + + ", vegetationNatural=" + vegetationNatural + + ", cavePockets=" + cavePockets + + ", underwaterPockets=" + underwaterPockets + + ", rawBedrock=" + rawBedrock + + ", rawBlockEntities=" + rawBlockEntities + + ", exposedSnow=" + exposedSnow + ", surfaceIce=" + surfaceIce + + ", buriedSnow=" + buriedSnow + ", buriedIce=" + buriedIce + + ", unconfiguredSnow=" + unconfiguredSnow + + ", unconfiguredIce=" + unconfiguredIce); + } + return new Audit(dry, wet, filler, geology, ceiling, roof, biomeA, biomeB, edges, sentinels, + rawNatural, structureNatural, vegetationNatural, cavePockets, + underwaterPockets, rawBedrock, rawBlockEntities, + exposedSnow, surfaceIce, buriedSnow, buriedIce, unconfiguredSnow, unconfiguredIce); + } + + private static WeatherMaterialAudit auditWeatherMaterials(Chunk chunk, + BlockPos.MutableBlockPos cursor, int minX, int minZ, boolean roofed) { + if (roofed) { + long snow = chunk.getBlockState(cursor.setPos(minX + 2, GROUND_Y + 11, minZ + 2)) + .getBlock() == Blocks.SNOW_LAYER ? 1 : 0; + long ice = chunk.getBlockState(cursor.setPos(minX + 3, GROUND_Y + 11, minZ + 2)) + .getBlock() == Blocks.ICE ? 1 : 0; + return new WeatherMaterialAudit(0, 0, 0, 0, snow, ice); + } + long snow = chunk.getBlockState(cursor.setPos(minX + 2, GROUND_Y + 1, minZ + 2)) + .getBlock() == WEATHER_SNOW_REPLACEMENT ? 1 : 0; + long ice = chunk.getBlockState(cursor.setPos(minX + 3, GROUND_Y + 1, minZ + 2)) + .getBlock() == WEATHER_ICE_REPLACEMENT ? 1 : 0; + long buriedSnow = chunk.getBlockState(cursor.setPos(minX + 2, GROUND_Y - 24, minZ + 3)) + .getBlock() == Blocks.SNOW ? 1 : 0; + long buriedIce = chunk.getBlockState(cursor.setPos(minX + 3, GROUND_Y - 24, minZ + 3)) + .getBlock() == Blocks.ICE ? 1 : 0; + return new WeatherMaterialAudit(snow, ice, buriedSnow, buriedIce, 0, 0); + } + + private static NaturalSourceAudit auditNaturalSources(WorldServer world, Chunk chunk, + BlockPos.MutableBlockPos cursor, int minX, int minZ) { + long raw = 0, structure = 0, vegetation = 0, cave = 0, underwater = 0; + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index), z = naturalZ(minZ, index); + if (chunk.getBlockState(cursor.setPos(x, GROUND_Y - 12, z)).getBlock() == Blocks.PRISMARINE) raw++; + IBlockState pocket = chunk.getBlockState(cursor.setPos(x, GROUND_Y - 11, z)); + if (index < NATURAL_SOURCES.length / 2) { + if (pocket.getBlock() == Blocks.AIR) cave++; + } else if (pocket.getBlock() == Blocks.WATER) { + underwater++; + } + if (NATURAL_SOURCES[index].equals(chunk.getBlockState( + cursor.setPos(x, GROUND_Y - 16, z)))) structure++; + if (NATURAL_SOURCES[index].equals(chunk.getBlockState( + cursor.setPos(x, GROUND_Y - 20, z)))) vegetation++; + } + long bedrock = chunk.getBlockState(cursor.setPos(minX + 11, GROUND_Y - 24, minZ + 12)) + .getBlock() == Blocks.BEDROCK ? 1 : 0; + BlockPos chestPos = new BlockPos(minX + 12, GROUND_Y - 24, minZ + 12); + long blockEntity = 0; + if (chunk.getBlockState(chestPos).getBlock() == Blocks.CHEST + && world.getTileEntity(chestPos) instanceof TileEntityChest) { + ItemStack stack = ((TileEntityChest) world.getTileEntity(chestPos)).getStackInSlot(0); + if (stack.getItem() == Items.EMERALD && RAW_CHEST_ITEM_NAME.equals(stack.getDisplayName())) { + blockEntity = 1; + } + } + return new NaturalSourceAudit(raw, structure, vegetation, cave, underwater, bedrock, blockEntity); + } + + private static void loadPopulationBorder(WorldServer world) { + for (int chunkZ = MIN_CHUNK - 1; chunkZ <= MAX_CHUNK + 1; chunkZ++) { + for (int chunkX = MIN_CHUNK - 1; chunkX <= MAX_CHUNK + 1; chunkX++) { + world.getChunkProvider().provideChunk(chunkX, chunkZ); + } + } + } + + private static int auditSentinels(WorldServer world, int minX, int minZ) { + assertBlock(world, minX + 4, GROUND_Y + 1, minZ + 4, Blocks.LOG, "tree log"); + assertBlock(world, minX + 4, GROUND_Y + 4, minZ + 4, Blocks.LEAVES, "tree leaves"); + assertBlock(world, minX + 6, GROUND_Y + 1, minZ + 6, Blocks.DIRT, "vegetation substrate"); + assertBlock(world, minX + 6, GROUND_Y + 2, minZ + 6, Blocks.SAPLING, "vegetation"); + assertBlock(world, minX + 8, GROUND_Y + 1, minZ + 8, Blocks.BRICK_BLOCK, "structure"); + BlockPos chestPos = new BlockPos(minX + 10, GROUND_Y + 1, minZ + 10); + assertBlock(world, chestPos.getX(), chestPos.getY(), chestPos.getZ(), Blocks.CHEST, "chest"); + if (!(world.getTileEntity(chestPos) instanceof TileEntityChest)) { + throw new IllegalStateException("Chest block entity missing at " + chestPos); + } + ItemStack stack = ((TileEntityChest) world.getTileEntity(chestPos)).getStackInSlot(0); + if (stack.getItem() != Items.DIAMOND || !CHEST_ITEM_NAME.equals(stack.getDisplayName())) { + throw new IllegalStateException("Chest inventory changed at " + chestPos); + } + return 4; + } + + private static ResourceLocation auditSpring(WorldServer world, String phase) { + if (!SurfaceProbeSpringBridge.recognizesProviderRock(Blocks.PURPUR_BLOCK)) { + throw new IllegalStateException("Provider rock is not accepted by the spring compatibility path"); + } + world.getChunkProvider().provideChunk(SPRING_POS.getX() >> 4, SPRING_POS.getZ() >> 4); + if ("fresh".equals(phase)) { + for (BlockPos rock : Arrays.asList(SPRING_POS.up(), SPRING_POS.down(), + SPRING_POS.west(), SPRING_POS.east(), SPRING_POS.north())) { + world.setBlockState(rock, Blocks.PURPUR_BLOCK.getDefaultState(), 2); + } + world.setBlockToAir(SPRING_POS); + world.setBlockToAir(SPRING_POS.south()); + if (!SurfaceProbeSpringBridge.placeWater(world, SPRING_POS)) { + throw new IllegalStateException("Provider-rock spring was rejected"); + } + } + Block block = world.getBlockState(SPRING_POS).getBlock(); + if (block != Blocks.FLOWING_WATER && block != Blocks.WATER) { + throw new IllegalStateException("Provider-rock spring changed across " + phase + ": " + block); + } + return id(block); + } + + private static void verifyPatterns() { + for (String name : Arrays.asList("default", "vein", "normal_cloud", "precision", + "clusters", "underfluids")) { + if (OreSpawnPatternRegistry.registry().getValue(new ResourceLocation("orespawn", name)) == null) { + throw new IllegalStateException("Missing built-in ore pattern " + name); + } + } + if (OreSpawnPatternRegistry.registry().getValue( + new ResourceLocation(MODID, "external_probe")) != externalPattern) { + throw new IllegalStateException("Fixture external ore pattern did not register"); + } + } + + private static Material material(ResourceLocation biome, boolean roofed) { + if (BIOME_A.equals(biome)) return new Material(Blocks.EMERALD_BLOCK, Blocks.QUARTZ_BLOCK, + Blocks.LAPIS_BLOCK, roofed ? Blocks.IRON_BLOCK : null); + if (BIOME_B.equals(biome)) return new Material(Blocks.DIAMOND_BLOCK, Blocks.REDSTONE_BLOCK, + Blocks.COAL_BLOCK, roofed ? Blocks.GOLD_BLOCK : null); + throw new IllegalStateException("Unexpected provider biome " + biome); + } + + private static int markedGround(Chunk chunk, BlockPos.MutableBlockPos cursor, int x, int z) { + for (int y = 255; y >= 0; y--) { + if (chunk.getBlockState(cursor.setPos(x, y, z)).getBlock() == Blocks.OBSIDIAN) return y + 5; + } + throw new IllegalStateException("Surface marker missing at " + x + "," + z); + } + + private static void placeRawNaturalSources(World world, Chunk chunk, int minX, int minZ) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index), z = naturalZ(minZ, index); + chunk.setBlockState(pos.setPos(x, GROUND_Y - 12, z).toImmutable(), NATURAL_SOURCES[index]); + chunk.setBlockState(pos.setPos(x, GROUND_Y - 11, z).toImmutable(), + index < NATURAL_SOURCES.length / 2 + ? Blocks.AIR.getDefaultState() : Blocks.WATER.getDefaultState()); + } + chunk.setBlockState(pos.setPos(minX + 11, GROUND_Y - 24, minZ + 12).toImmutable(), + Blocks.BEDROCK.getDefaultState()); + BlockPos chestPos = new BlockPos(minX + 12, GROUND_Y - 24, minZ + 12); + world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); + if (world.getTileEntity(chestPos) instanceof TileEntityChest) { + ItemStack stack = new ItemStack(Items.EMERALD); + stack.setStackDisplayName(RAW_CHEST_ITEM_NAME); + ((TileEntityChest) world.getTileEntity(chestPos)).setInventorySlotContents(0, stack); + } + chunk.markDirty(); + } + + private static void placeAuthoredNaturalSources(World world, int minX, int minZ, int depth) { + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + world.setBlockState(new BlockPos(naturalX(minX, index), GROUND_Y - depth, + naturalZ(minZ, index)), NATURAL_SOURCES[index], 2); + } + } + + private static void placeWeatherMaterialSentinels(World world, int minX, int minZ) { + if (world.provider.getDimension() == -1) { + world.setBlockState(new BlockPos(minX + 2, GROUND_Y + 11, minZ + 2), + Blocks.SNOW_LAYER.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 3, GROUND_Y + 11, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + return; + } + if (world.provider.getDimension() != 1) return; + world.setBlockState(new BlockPos(minX + 2, GROUND_Y + 1, minZ + 2), + Blocks.SNOW_LAYER.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 3, GROUND_Y + 1, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 2, GROUND_Y - 24, minZ + 3), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 3, GROUND_Y - 24, minZ + 3), + Blocks.ICE.getDefaultState(), 2); + } + + private static int naturalX(int minX, int index) { + return minX + 12 + index % 4; + } + + private static int naturalZ(int minZ, int index) { + return minZ + 1 + index / 4; + } + + private static void assertBlock(Chunk chunk, BlockPos.MutableBlockPos cursor, + int x, int y, int z, Block expected, String purpose) { + Block actual = chunk.getBlockState(cursor.setPos(x, y, z)).getBlock(); + if (actual != expected) throw new IllegalStateException("Expected " + purpose + " " + expected + + " at " + cursor + " but found " + actual); + } + + private static void assertBlock(World world, int x, int y, int z, Block expected, String purpose) { + BlockPos pos = new BlockPos(x, y, z); + Block actual = world.getBlockState(pos).getBlock(); + if (actual != expected) throw new IllegalStateException("Expected " + purpose + " " + expected + + " at " + pos + " but found " + actual); + } + + private static Path worldRoot(MinecraftServer server) { + return server.getActiveAnvilConverter().getFile(server.getFolderName(), "level.dat") + .toPath().toAbsolutePath().normalize().getParent(); + } + + private static Properties properties(long seed, Map audits, ResourceLocation spring) { + Properties properties = new Properties(); + properties.setProperty("seed", Long.toString(seed)); + properties.setProperty("dimensions", Integer.toString(audits.size())); + properties.setProperty("columns_per_dimension", Integer.toString(COLUMNS)); + properties.setProperty("spring", spring.toString()); + for (Map.Entry entry : audits.entrySet()) entry.getValue().put(properties, entry.getKey()); + return properties; + } + + private static Properties read(Path path) { + Properties properties = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { properties.load(reader); } + catch (IOException exception) { throw new IllegalStateException("Could not read " + path, exception); } + return properties; + } + + private static void write(Path path, Properties properties) { + try { + Files.createDirectories(path.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + properties.store(writer, "OreSpawn Forge 1.12 surface integration"); + } + } catch (IOException exception) { + throw new IllegalStateException("Could not write " + path, exception); + } + } + + private static final class ProbeGenerator implements IWorldGenerator { + @Override + public void generate(Random random, int chunkX, int chunkZ, World world, + IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) { + if ((world.provider.getDimension() != 1 && world.provider.getDimension() != -1) + || chunkX < MIN_CHUNK || chunkX > MAX_CHUNK + || chunkZ < MIN_CHUNK || chunkZ > MAX_CHUNK) return; + placeSentinels(world, chunkX << 4, chunkZ << 4); + } + + private static void placeTerrain(Chunk chunk, int minX, int minZ, boolean roofed) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int localZ = 0; localZ < 16; localZ++) { + for (int localX = 0; localX < 16; localX++) { + int x = minX + localX, z = minZ + localZ; + for (int y = GROUND_Y + 1; y < 256; y++) + chunk.setBlockState(pos.setPos(x, y, z), Blocks.AIR.getDefaultState()); + chunk.setBlockState(pos.setPos(x, MARKER_Y, z), Blocks.OBSIDIAN.getDefaultState()); + if (!roofed) for (int y = GEOLOGY_MIN_Y; y <= GEOLOGY_MAX_Y; y++) + chunk.setBlockState(pos.setPos(x, y, z), Blocks.END_STONE.getDefaultState()); + chunk.setBlockState(pos.setPos(x, GROUND_Y - 4, z), Blocks.STONE.getDefaultState()); + for (int y = GROUND_Y - 3; y < GROUND_Y; y++) + chunk.setBlockState(pos.setPos(x, y, z), Blocks.DIRT.getDefaultState()); + chunk.setBlockState(pos.setPos(x, GROUND_Y, z), Blocks.GRASS.getDefaultState()); + if (localX == 1 && localZ == 1) + chunk.setBlockState(pos.setPos(x, GROUND_Y + 1, z), Blocks.WATER.getDefaultState()); + if (roofed) for (int y = ROOF_UNDERSIDE_Y; y <= ROOF_TOP_Y; y++) + chunk.setBlockState(pos.setPos(x, y, z), Blocks.STONE.getDefaultState()); + } + } + chunk.markDirty(); + } + + private static void placeSentinels(World world, int minX, int minZ) { + world.setBlockState(new BlockPos(minX + 4, GROUND_Y + 1, minZ + 4), Blocks.LOG.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 4, GROUND_Y + 2, minZ + 4), Blocks.LOG.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 4, GROUND_Y + 3, minZ + 4), Blocks.LOG.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 4, GROUND_Y + 4, minZ + 4), Blocks.LEAVES.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 6, GROUND_Y + 1, minZ + 6), Blocks.DIRT.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 6, GROUND_Y + 2, minZ + 6), Blocks.SAPLING.getDefaultState(), 2); + world.setBlockState(new BlockPos(minX + 8, GROUND_Y + 1, minZ + 8), Blocks.BRICK_BLOCK.getDefaultState(), 2); + BlockPos chestPos = new BlockPos(minX + 10, GROUND_Y + 1, minZ + 10); + world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); + if (!(world.getTileEntity(chestPos) instanceof TileEntityChest)) + throw new IllegalStateException("Fixture chest failed at " + chestPos); + ItemStack stack = new ItemStack(Items.DIAMOND); + stack.setStackDisplayName(CHEST_ITEM_NAME); + ((TileEntityChest) world.getTileEntity(chestPos)).setInventorySlotContents(0, stack); + if (world.provider.getDimension() == 1) { + placeAuthoredNaturalSources(world, minX, minZ, 16); + placeAuthoredNaturalSources(world, minX, minZ, 20); + } + placeWeatherMaterialSentinels(world, minX, minZ); + } + } + + private static final class Material { + final Block top, filler, underwater, ceiling; + Material(Block top, Block filler, Block underwater, Block ceiling) { + this.top = top; this.filler = filler; this.underwater = underwater; this.ceiling = ceiling; + } + } + + /** Distinguishable dynamic liquid exercising vanilla's scheduled-tick retention path. */ + private static final class ProbeLiquid extends BlockDynamicLiquid { + private int placements; + + ProbeLiquid() { + super(net.minecraft.block.material.Material.LAVA); + setRegistryName(MODID, "dynamic_tick_probe"); + setUnlocalizedName(MODID + ".dynamic_tick_probe"); + } + + @Override + public void onBlockAdded(World world, BlockPos pos, IBlockState state) { + placements++; + super.onBlockAdded(world, pos, state); + } + + int placements() { + return placements; + } + } + + private static final class ProbeDecorator extends BiomeDecorator { + @Override + public void decorate(World world, Random random, Biome biome, BlockPos pos) { + MinecraftForge.EVENT_BUS.post(new DecorateBiomeEvent.Pre(world, random, pos)); + MinecraftForge.EVENT_BUS.post(new DecorateBiomeEvent.Post(world, random, pos)); + } + } + + private static final class Audit { + final long dry, wet, filler, geology, ceiling, roof; + final int biomeA, biomeB, edges, sentinels; + final long rawNatural, structureNatural, vegetationNatural; + final long cavePockets, underwaterPockets, rawBedrock, rawBlockEntities; + final long exposedSnow, surfaceIce, buriedSnow, buriedIce; + final long unconfiguredSnow, unconfiguredIce; + Audit(long dry, long wet, long filler, long geology, long ceiling, long roof, + int biomeA, int biomeB, int edges, int sentinels, + long rawNatural, long structureNatural, long vegetationNatural, + long cavePockets, long underwaterPockets, long rawBedrock, long rawBlockEntities, + long exposedSnow, long surfaceIce, long buriedSnow, long buriedIce, + long unconfiguredSnow, long unconfiguredIce) { + this.dry = dry; this.wet = wet; this.filler = filler; this.geology = geology; + this.ceiling = ceiling; this.roof = roof; this.biomeA = biomeA; + this.biomeB = biomeB; this.edges = edges; this.sentinels = sentinels; + this.rawNatural = rawNatural; this.structureNatural = structureNatural; + this.vegetationNatural = vegetationNatural; this.cavePockets = cavePockets; + this.underwaterPockets = underwaterPockets; this.rawBedrock = rawBedrock; + this.rawBlockEntities = rawBlockEntities; + this.exposedSnow = exposedSnow; this.surfaceIce = surfaceIce; + this.buriedSnow = buriedSnow; this.buriedIce = buriedIce; + this.unconfiguredSnow = unconfiguredSnow; this.unconfiguredIce = unconfiguredIce; + } + void put(Properties properties, String prefix) { + properties.setProperty(prefix + ".dry", Long.toString(dry)); + properties.setProperty(prefix + ".wet", Long.toString(wet)); + properties.setProperty(prefix + ".filler", Long.toString(filler)); + properties.setProperty(prefix + ".geology", Long.toString(geology)); + properties.setProperty(prefix + ".ceiling", Long.toString(ceiling)); + properties.setProperty(prefix + ".roof", Long.toString(roof)); + properties.setProperty(prefix + ".biome_a", Integer.toString(biomeA)); + properties.setProperty(prefix + ".biome_b", Integer.toString(biomeB)); + properties.setProperty(prefix + ".edges", Integer.toString(edges)); + properties.setProperty(prefix + ".sentinels", Integer.toString(sentinels)); + properties.setProperty(prefix + ".raw_natural_sources", Long.toString(rawNatural)); + properties.setProperty(prefix + ".structure_natural_sources", Long.toString(structureNatural)); + properties.setProperty(prefix + ".vegetation_natural_sources", Long.toString(vegetationNatural)); + properties.setProperty(prefix + ".cave_pockets", Long.toString(cavePockets)); + properties.setProperty(prefix + ".underwater_pockets", Long.toString(underwaterPockets)); + properties.setProperty(prefix + ".raw_bedrock", Long.toString(rawBedrock)); + properties.setProperty(prefix + ".raw_block_entities", Long.toString(rawBlockEntities)); + properties.setProperty(prefix + ".exposed_snow_converted", Long.toString(exposedSnow)); + properties.setProperty(prefix + ".surface_ice_converted", Long.toString(surfaceIce)); + properties.setProperty(prefix + ".buried_snow_preserved", Long.toString(buriedSnow)); + properties.setProperty(prefix + ".buried_ice_preserved", Long.toString(buriedIce)); + properties.setProperty(prefix + ".unconfigured_snow_preserved", Long.toString(unconfiguredSnow)); + properties.setProperty(prefix + ".unconfigured_ice_preserved", Long.toString(unconfiguredIce)); + } + @Override public String toString() { + return "Audit{dry=" + dry + ", wet=" + wet + ", filler=" + filler + + ", geology=" + geology + ", ceiling=" + ceiling + ", sentinels=" + sentinels + "}"; + } + } + + private static final class WeatherMaterialAudit { + final long exposedSnowConverted, surfaceIceConverted; + final long buriedSnowPreserved, buriedIcePreserved; + final long unconfiguredSnowPreserved, unconfiguredIcePreserved; + WeatherMaterialAudit(long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { + this.exposedSnowConverted = exposedSnowConverted; + this.surfaceIceConverted = surfaceIceConverted; + this.buriedSnowPreserved = buriedSnowPreserved; + this.buriedIcePreserved = buriedIcePreserved; + this.unconfiguredSnowPreserved = unconfiguredSnowPreserved; + this.unconfiguredIcePreserved = unconfiguredIcePreserved; + } + } + + private static final class NaturalSourceAudit { + final long rawConverted, structurePreserved, vegetationPreserved; + final long cavePreserved, underwaterPreserved, bedrockPreserved, blockEntityPreserved; + NaturalSourceAudit(long rawConverted, long structurePreserved, + long vegetationPreserved, long cavePreserved, long underwaterPreserved, + long bedrockPreserved, long blockEntityPreserved) { + this.rawConverted = rawConverted; + this.structurePreserved = structurePreserved; + this.vegetationPreserved = vegetationPreserved; + this.cavePreserved = cavePreserved; + this.underwaterPreserved = underwaterPreserved; + this.bedrockPreserved = bedrockPreserved; + this.blockEntityPreserved = blockEntityPreserved; + } + } +} diff --git a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/worldgen/SurfaceProbeSpringBridge.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/worldgen/SurfaceProbeSpringBridge.java new file mode 100644 index 00000000..fbc709fb --- /dev/null +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/worldgen/SurfaceProbeSpringBridge.java @@ -0,0 +1,24 @@ +package zone.moddev.mc.orespawn.worldgen; + +import java.util.Random; + +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; + +/** Test-only package bridge for the Forge 1.12 spring compatibility path. */ +public final class SurfaceProbeSpringBridge { + private SurfaceProbeSpringBridge() { + } + + public static boolean recognizesProviderRock(Block block) { + return VanillaSpringCompatibility.isProviderRock(block); + } + + public static boolean placeWater(WorldServer world, BlockPos pos) { + return VanillaSpringCompatibility.generate(Blocks.FLOWING_WATER, + world, new Random(0L), pos); + } + +} diff --git a/src/biomeIntegrationTest/resources/mcmod.info b/src/biomeIntegrationTest/resources/mcmod.info new file mode 100644 index 00000000..74df810e --- /dev/null +++ b/src/biomeIntegrationTest/resources/mcmod.info @@ -0,0 +1,11 @@ +[ + { + "modid": "surfaceprobe", + "name": "OreSpawn Surface Integration Test", + "description": "Test-only provider mod for OreSpawn's surface replacement gate.", + "version": "1.0.0", + "mcversion": "1.11.2", + "authorList": ["OreSpawn test fixture"], + "dependencies": ["required-after:orespawn@[4.0.6,5.0.0)"] + } +] diff --git a/src/biomeIntegrationTest/resources/pack.mcmeta b/src/biomeIntegrationTest/resources/pack.mcmeta new file mode 100644 index 00000000..7a7d6cdf --- /dev/null +++ b/src/biomeIntegrationTest/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "OreSpawn provider-surface integration fixtures", + "pack_format": 2 + } +} diff --git a/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java b/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java new file mode 100644 index 00000000..711050d9 --- /dev/null +++ b/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java @@ -0,0 +1,352 @@ +package zone.moddev.mc.orespawn.client; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiCreateWorld; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.TextFormatting; +import net.minecraft.world.GameType; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.WorldType; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; + +/** Build-only client probe. It is compiled and packaged outside every release artifact. */ +@Mod(modid = ClientProbeTestMod.MODID, name = "OreSpawn Client Probe", version = "1", + acceptedMinecraftVersions = "[1.11.2]", dependencies = "required-after:orespawn") +public final class ClientProbeTestMod { + static final String MODID = "clientprobe"; + private static final String WORLD_DIRECTORY = "client-smoke-world"; + private final Set editorRoutes = new HashSet<>(); + private final Set attemptedButtons = new HashSet<>(); + private GuiButton worldSettingsButton; + private int state; + private int stateTicks; + private int firstWorldFrames; + private int reloadWorldFrames; + private int editorFrames; + private boolean worldSettingsOpened; + private boolean longEditorRoundTrip; + + @Mod.EventHandler + public void initialize(FMLInitializationEvent event) { + if (!Boolean.getBoolean("clientprobe.enabled")) return; + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void onScreenInitialized(GuiScreenEvent.InitGuiEvent.Post event) { + if (!(event.getGui() instanceof GuiCreateWorld)) return; + for (GuiButton button : event.getButtonList()) { + if (button.id == 0x4F53) worldSettingsButton = button; + } + } + + @SubscribeEvent + public void onScreenDrawn(GuiScreenEvent.DrawScreenEvent.Post event) { + if (event.getGui() instanceof OreSpawnScreen) editorFrames++; + } + + @SubscribeEvent + public void onWorldRendered(RenderWorldLastEvent event) { + if (state == 6) firstWorldFrames++; + if (state == 8) reloadWorldFrames++; + } + + @SubscribeEvent + public void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END || !Boolean.getBoolean("clientprobe.enabled")) return; + Minecraft minecraft = Minecraft.getMinecraft(); + if (++stateTicks > 3600) fail(minecraft, "Timed out in client probe state " + state); + try { + switch (state) { + case 0: + if (minecraft.currentScreen instanceof GuiMainMenu) { + minecraft.displayGuiScreen(new GuiCreateWorld(minecraft.currentScreen)); + nextState(1); + } + break; + case 1: + if (minecraft.currentScreen instanceof GuiCreateWorld && worldSettingsButton != null) { + GuiScreenEvent.ActionPerformedEvent.Pre press = + new GuiScreenEvent.ActionPerformedEvent.Pre(minecraft.currentScreen, + worldSettingsButton, java.util.Collections.singletonList(worldSettingsButton)); + if (!MinecraftForge.EVENT_BUS.post(press) || !press.isCanceled()) { + fail(minecraft, "OreSpawn world-settings action was not canceled"); + } + nextState(2); + } + break; + case 2: + if (minecraft.currentScreen instanceof OreSpawnWorldSettingsScreen && editorFrames >= 2) { + worldSettingsOpened = true; + validateCaptions((OreSpawnWorldSettingsScreen) minecraft.currentScreen); + validateLongEditorRoundTrip(minecraft, minecraft.currentScreen); + nextState(3); + } + break; + case 3: + if (minecraft.currentScreen instanceof OreSpawnWorldSettingsScreen) { + OreSpawnWorldSettingsScreen root = (OreSpawnWorldSettingsScreen) minecraft.currentScreen; + Button target = nextNavigationButton(root); + if (target == null) { + if (editorRoutes.size() < 5) fail(minecraft, + "Only exercised " + editorRoutes.size() + " editor routes: " + editorRoutes); + root.onClose(); + nextState(5); + } else { + GuiScreen before = minecraft.currentScreen; + target.press(); + if (minecraft.currentScreen != before && minecraft.currentScreen instanceof OreSpawnScreen) { + editorRoutes.add(minecraft.currentScreen.getClass().getSimpleName()); + editorFrames = 0; + nextState(4); + } + } + } + break; + case 4: + if (minecraft.currentScreen instanceof OreSpawnScreen && editorFrames >= 2) { + validateCaptions((OreSpawnScreen) minecraft.currentScreen); + ((OreSpawnScreen) minecraft.currentScreen).onClose(); + nextState(3); + } + break; + case 5: + if (minecraft.currentScreen instanceof GuiCreateWorld) { + minecraft.launchIntegratedServer(WORLD_DIRECTORY, "OreSpawn Client Smoke", + new WorldSettings(0L, GameType.CREATIVE, false, false, WorldType.DEFAULT)); + nextState(6); + } + break; + case 6: + if (minecraft.world != null && minecraft.player != null && firstWorldFrames >= 8 + && stateTicks >= 100) { + stopIntegratedServer(minecraft); + nextState(7); + } + break; + case 7: + if (minecraft.world == null && !minecraft.isIntegratedServerRunning() && stateTicks >= 20) { + minecraft.launchIntegratedServer(WORLD_DIRECTORY, "OreSpawn Client Smoke", + new WorldSettings(0L, GameType.CREATIVE, false, false, WorldType.DEFAULT)); + nextState(8); + } + break; + case 8: + if (minecraft.world != null && minecraft.player != null && reloadWorldFrames >= 8 + && stateTicks >= 100) { + writeMarker(); + // Let Minecraft's normal client shutdown own the final integrated-server + // disconnect. A second manual loadWorld(null) can leave already-scheduled + // 1.12 entity packets targeting a world that has just been removed. + minecraft.shutdown(); + nextState(10); + } + break; + default: + break; + } + } catch (RuntimeException | IOException failure) { + fail(minecraft, failure.toString()); + } + } + + private Button nextNavigationButton(OreSpawnWorldSettingsScreen root) { + for (GuiButton widget : root.buttons) { + if (!(widget instanceof Button) || widget instanceof CycleButton) continue; + Button button = (Button) widget; + String caption = TextFormatting.getTextWithoutFormattingCodes(button.getMessage()); + if (!attemptedButtons.add(caption)) continue; + String lower = caption.toLowerCase(java.util.Locale.ROOT); + if (lower.equals("done") || lower.equals("cancel") || lower.contains("recommended")) continue; + return button; + } + return null; + } + + private static void validateCaptions(OreSpawnScreen screen) { + for (GuiButton widget : screen.buttons) { + String caption = TextFormatting.getTextWithoutFormattingCodes(widget.displayString); + if (caption == null || caption.trim().isEmpty() + || caption.contains("options.generic_value") + || caption.startsWith("button.orespawn.") + || caption.startsWith("option.orespawn.")) { + throw new IllegalStateException("Invalid client caption: " + widget.displayString); + } + } + } + + private void validateLongEditorRoundTrip(Minecraft minecraft, GuiScreen parent) { + JsonObject root = WorldGeologyProfile.recommended(true).rootCopy(); + JsonObject ores = new JsonObject(); + JsonObject ore = new JsonObject(); + ore.addProperty("enabled", true); + ore.addProperty("block", "minecraft:diamond_ore"); + JsonObject oreDimensions = new JsonObject(); + JsonObject oreRule = new JsonObject(); + oreRule.addProperty("enabled", true); + oreRule.addProperty("min_y", 0); + oreRule.addProperty("max_y", 64); + oreRule.addProperty("frequency", 1.0D); + oreRule.addProperty("quantity", 8); + oreRule.addProperty("discard_chance_on_air_exposure", 0.0D); + oreRule.addProperty("pattern", "vein"); + oreRule.addProperty("height_distribution", "uniform"); + oreRule.addProperty("spread", 8); + oreRule.addProperty("vertical_spread", 4); + oreRule.addProperty("node_size", 4); + oreRule.add("host_families", new JsonArray()); + oreRule.add("host_blocks", values( + "example:ore_host_block_identifier_longer_than_thirty_two_characters")); + oreRule.add("host_tags", values( + "forge:ore_host_tag_identifier_longer_than_thirty_two_characters", + "forge:second_ore_host_tag_in_the_same_comma_separated_list")); + oreDimensions.add("minecraft:overworld", oreRule); + ore.add("dimensions", oreDimensions); + ores.add("example:long_editor_ore", ore); + root.add("ores", ores); + + JsonObject deposits = new JsonObject(); + JsonObject deposit = new JsonObject(); + deposit.addProperty("enabled", true); + deposit.addProperty("block", "minecraft:water"); + JsonObject fluidDimensions = new JsonObject(); + JsonObject fluidRule = new JsonObject(); + fluidRule.addProperty("enabled", true); + fluidRule.addProperty("min_y", 0); + fluidRule.addProperty("max_y", 48); + fluidRule.addProperty("frequency", 0.08D); + fluidRule.addProperty("min_radius", 5); + fluidRule.addProperty("max_radius", 12); + fluidRule.addProperty("min_vertical_radius", 2); + fluidRule.addProperty("max_vertical_radius", 5); + fluidRule.addProperty("max_lobes", 4); + fluidRule.addProperty("min_solid_cover", 2); + fluidRule.addProperty("min_solid_shell", 1); + fluidRule.add("host_families", new JsonArray()); + fluidRule.add("host_blocks", values( + "example:fluid_host_block_identifier_longer_than_thirty_two_characters")); + fluidRule.add("host_tags", values( + "forge:fluid_host_tag_identifier_longer_than_thirty_two_characters", + "forge:second_fluid_host_tag_in_the_same_comma_separated_list")); + fluidRule.add("biome_ids", values( + "example:included_biome_identifier_longer_than_thirty_two_characters")); + fluidRule.add("excluded_biome_ids", values( + "example:excluded_biome_identifier_longer_than_thirty_two_characters")); + fluidRule.add("biome_dictionary", values( + "INCLUDED_DICTIONARY_VALUE_LONGER_THAN_THIRTY_TWO_CHARACTERS", + "SECOND_INCLUDED_DICTIONARY_VALUE_IN_THE_COMMA_LIST")); + fluidRule.add("excluded_biome_dictionary", values( + "EXCLUDED_DICTIONARY_VALUE_LONGER_THAN_THIRTY_TWO_CHARACTERS")); + fluidRule.add("geomes", new JsonObject()); + fluidDimensions.add("minecraft:overworld", fluidRule); + deposit.add("dimensions", fluidDimensions); + deposits.add("example:long_editor_deposit", deposit); + root.add("fluid_deposits", deposits); + // Keep the synthetic profile in the editor's canonical shape so this + // assertion is about preservation of the eight long text fields rather + // than the session adding an unrelated optional empty section. + root.add("geomes", new JsonObject()); + + GeologyEditorSession session = new GeologyEditorSession( + WorldGeologyProfile.recommended(true).withRoot(root)); + String before = session.root().toString(); + + OreDimensionScreen oreScreen = new OreDimensionScreen(parent, session, + "example:long_editor_ore", "minecraft:overworld"); + ((GuiScreen) oreScreen).setWorldAndResolution(minecraft, 640, 480); + pressDone(oreScreen); + + FluidDepositDimensionScreen fluidScreen = new FluidDepositDimensionScreen(parent, session, + "example:long_editor_deposit", "minecraft:overworld"); + ((GuiScreen) fluidScreen).setWorldAndResolution(minecraft, 640, 480); + pressDone(fluidScreen); + + String after = session.root().toString(); + if (!before.equals(after)) { + throw new IllegalStateException("Opening and saving long editor values changed profile JSON\nBefore: " + + before + "\nAfter: " + after); + } + longEditorRoundTrip = true; + } + + private static JsonArray values(String... entries) { + JsonArray result = new JsonArray(); + for (String entry : entries) result.add(new JsonPrimitive(entry)); + return result; + } + + private static void pressDone(OreSpawnScreen screen) { + for (GuiButton widget : screen.buttons) { + if (!(widget instanceof Button)) continue; + String caption = TextFormatting.getTextWithoutFormattingCodes(((Button) widget).getMessage()); + if ("done".equalsIgnoreCase(caption)) { + ((Button) widget).press(); + return; + } + } + throw new IllegalStateException("Editor did not expose its Done action: " + + screen.getClass().getSimpleName()); + } + + private static void stopIntegratedServer(Minecraft minecraft) { + // Match GuiIngameMenu's target-native disconnect path. loadWorld(null) + // coordinates the integrated-server save/stop; installing the replacement + // screen in the same tick prevents EntityRenderer from seeing no world and + // no screen between frames. + if (minecraft.world != null) minecraft.world.sendQuittingDisconnectingPacket(); + minecraft.loadWorld(null); + minecraft.displayGuiScreen(new GuiMainMenu()); + } + + private void writeMarker() throws IOException { + Properties values = new Properties(); + values.setProperty("world_settings_opened", Boolean.toString(worldSettingsOpened)); + values.setProperty("long_editor_roundtrip", Boolean.toString(longEditorRoundTrip)); + values.setProperty("editor_routes", Integer.toString(editorRoutes.size())); + values.setProperty("editor_classes", editorRoutes.toString()); + values.setProperty("first_world_rendered", Boolean.toString(firstWorldFrames >= 8)); + values.setProperty("reload_rendered", Boolean.toString(reloadWorldFrames >= 8)); + values.setProperty("world_directory", WORLD_DIRECTORY); + try (FileOutputStream output = new FileOutputStream(new File("client-smoke-pass.properties"))) { + values.store(output, "OreSpawn Forge 1.11.2 client integration gate"); + } + } + + private void nextState(int next) { + state = next; + stateTicks = 0; + } + + private static void fail(Minecraft minecraft, String message) { + try { + Properties values = new Properties(); values.setProperty("failure", message); + try (FileOutputStream output = new FileOutputStream(new File("client-smoke-failure.properties"))) { + values.store(output, "OreSpawn client probe failure"); + } + } catch (IOException ignored) { + } + minecraft.shutdown(); + throw new IllegalStateException(message); + } +} diff --git a/src/clientIntegrationTest/resources/mcmod.info b/src/clientIntegrationTest/resources/mcmod.info new file mode 100644 index 00000000..519dc069 --- /dev/null +++ b/src/clientIntegrationTest/resources/mcmod.info @@ -0,0 +1 @@ +[{"modid":"clientprobe","name":"OreSpawn Client Probe","description":"Build-only OreSpawn client smoke fixture.","version":"1","mcversion":"1.11.2","authorList":["MMD"],"dependencies":["required-after:orespawn@[4.0.6,5.0.0)"]}] diff --git a/src/clientIntegrationTest/resources/pack.mcmeta b/src/clientIntegrationTest/resources/pack.mcmeta new file mode 100644 index 00000000..f41a7403 --- /dev/null +++ b/src/clientIntegrationTest/resources/pack.mcmeta @@ -0,0 +1 @@ +{"pack":{"description":"OreSpawn client smoke fixture","pack_format":2}} diff --git a/src/main/java/com/mcmoddev/orespawn/EventHandlers.java b/src/main/java/com/mcmoddev/orespawn/EventHandlers.java deleted file mode 100644 index e5be78d2..00000000 --- a/src/main/java/com/mcmoddev/orespawn/EventHandlers.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.mcmoddev.orespawn; - -import java.util.LinkedList; -import java.util.Arrays; -import java.util.Deque; -import java.util.List; -import java.util.Map.Entry; -import java.util.Random; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.stream.Collectors; - -import com.mcmoddev.orespawn.api.os3.BuilderLogic; -import com.mcmoddev.orespawn.api.os3.DimensionBuilder; -import com.mcmoddev.orespawn.api.os3.FeatureBuilder; -import com.mcmoddev.orespawn.api.os3.OreBuilder; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.data.Config; -import com.mcmoddev.orespawn.data.Constants; - -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.nbt.NBTTagString; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkGenerator; -import net.minecraft.world.gen.ChunkProviderServer; -import net.minecraftforge.event.terraingen.OreGenEvent; -import net.minecraftforge.event.terraingen.OreGenEvent.GenerateMinable.EventType; -import net.minecraftforge.event.world.ChunkDataEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent.Phase; -import net.minecraftforge.fml.common.gameevent.TickEvent.WorldTickEvent; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; -import net.minecraftforge.fml.common.eventhandler.Event; -import net.minecraftforge.fml.common.eventhandler.EventPriority; - -public class EventHandlers { - private Deque chunks; - private Deque retroChunks; - - EventHandlers() { - chunks = new ConcurrentLinkedDeque<>(); - retroChunks = new ConcurrentLinkedDeque<>(); - } - - private List vanillaEvents = Arrays.asList(EventType.ANDESITE, EventType.COAL, EventType.DIAMOND, EventType.DIORITE, EventType.DIRT, - EventType.EMERALD, EventType.GOLD, EventType.GRANITE, EventType.GRAVEL, EventType.IRON, EventType.LAPIS, EventType.REDSTONE, - EventType.QUARTZ, EventType.SILVERFISH); - - @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) - public void onGenerateMinable(OreGenEvent.GenerateMinable event) { - if (Config.getBoolean(Constants.REPLACE_VANILLA_OREGEN) && vanillaEvents.contains(event.getType())) { - event.setResult(Event.Result.DENY); - } - } - - @SubscribeEvent - public void onChunkSave(ChunkDataEvent.Save ev) { - NBTTagCompound dataTag = ev.getData().getCompoundTag(Constants.CHUNK_TAG_NAME); - NBTTagList ores = new NBTTagList(); - NBTTagList features = new NBTTagList(); - - features.appendTag(new NBTTagString("orespawn:default")); - - List spawns = OreSpawn.API.getSpawns().entrySet().stream() - .filter(ent -> ent.getValue().getAllDimensions().containsKey(ev.getWorld().provider.getDimension())) - .map(ent -> ent.getValue().getDimension(ev.getWorld().provider.getDimension())) - .collect(Collectors.toList()); - - if (ev.getWorld().provider.getDimension() > 0 && ev.getWorld().provider.getDimension() != 1) { - spawns.addAll(OreSpawn.API.getSpawns().entrySet().stream() - .filter(ent -> ent.getValue().getAllDimensions().containsKey(OreSpawn.API.dimensionWildcard())) - .map(ent -> ent.getValue().getDimension(OreSpawn.API.dimensionWildcard())) - .collect(Collectors.toList())); - } - - List spc = new LinkedList<>(); - List oreList = new LinkedList<>(); - spawns.stream().map(DimensionBuilder::getAllSpawns).forEach(spc::addAll); - spc.stream().map(SpawnBuilder::getOres).forEach(oreList::addAll); - - oreList.stream() - .map(oreEnt -> new NBTTagString(oreEnt.getOre().getBlock().getRegistryName().toString())) - .forEach(ores::appendTag); - - List featureList = new LinkedList<>(); - - spawns.forEach(sp -> featureList.addAll(sp.getAllSpawns().stream().map(SpawnBuilder::getFeatureGen).collect(Collectors.toList()))); - - featureList.stream() - .map(feat -> new NBTTagString(feat.getFeatureName())) - .forEach(features::appendTag); - - ChunkPos chunkCoords = new ChunkPos(ev.getChunk().x, ev.getChunk().z); - - if (!Config.getBoolean(Constants.RETROGEN_KEY) || chunks.contains(chunkCoords)) { - dataTag.setTag(Constants.ORE_TAG, ores); - dataTag.setTag(Constants.FEATURES_TAG, features); - } - - ev.getData().setTag(Constants.CHUNK_TAG_NAME, dataTag); - } - - @SubscribeEvent - public void onChunkLoad(ChunkDataEvent.Load ev) { - World world = ev.getWorld(); - ChunkPos chunkCoords = new ChunkPos(ev.getChunk().x, ev.getChunk().z); - - doBedrockRetrogen(chunkCoords); - - if (chunks.contains(chunkCoords)) { - return; - } - - if (Config.getBoolean(Constants.RETROGEN_KEY)) { - NBTTagCompound chunkTag = ev.getData().getCompoundTag(Constants.CHUNK_TAG_NAME); - - if (featuresAreDifferent(chunkTag, world.provider.getDimension()) || Config.getBoolean(Constants.FORCE_RETROGEN_KEY)) { - chunks.addLast(chunkCoords); - } - } - } - - - private boolean featuresAreDifferent(NBTTagCompound chunkTag, int dim) { - return ((countOres(dim) != chunkTag.getTagList(Constants.ORE_TAG, 8).tagCount()) || - compFeatures(chunkTag.getTagList(Constants.FEATURES_TAG, 8), dim)); - } - - private boolean compFeatures(NBTTagList tagList, int dim) { - List spawns = OreSpawn.API.getSpawns().entrySet().stream() - .filter(ent -> ent.getValue().getAllDimensions().containsKey(dim)) - .map(ent -> ent.getValue().getDimension(dim)) - .collect(Collectors.toList()); - - if (dim > 0 && dim != 1) { - spawns.addAll(OreSpawn.API.getSpawns().entrySet().stream() - .filter(ent -> ent.getValue().getAllDimensions().containsKey(OreSpawn.API.dimensionWildcard())) - .map(ent -> ent.getValue().getDimension(OreSpawn.API.dimensionWildcard())) - .collect(Collectors.toList())); - } - - List featureList = new LinkedList<>(); - - spawns.forEach(sp -> featureList.addAll(sp.getAllSpawns().stream().map(SpawnBuilder::getFeatureGen).collect(Collectors.toList()))); - - return featureList.size() == tagList.tagCount(); - } - - private void doBedrockRetrogen(ChunkPos chunkCoords) { - if (retroChunks.contains(chunkCoords)) { - return; - } - - if (Config.getBoolean(Constants.RETRO_BEDROCK)) { - retroChunks.addLast(chunkCoords); - } - } - - private int countOres(int dim) { - int acc = 0; - - for (Entry sL : OreSpawn.API.getSpawns().entrySet()) { - if (sL.getValue().getAllDimensions().containsKey(dim)) { - acc += sL.getValue().getAllDimensions().get(dim).getAllSpawns().size(); - } - - if (sL.getValue().getAllDimensions().containsKey(OreSpawn.API.dimensionWildcard())) { - acc += sL.getValue().getAllDimensions().get(OreSpawn.API.dimensionWildcard()).getAllSpawns().size(); - } - } - - return acc; - } - - @SubscribeEvent - public void worldTick(WorldTickEvent ev) { - if (ev.side != Side.SERVER) { - return; - } - - World world = ev.world; - - if (ev.phase == Phase.END) { - for (int c = 0; c < 5 && !chunks.isEmpty(); c++) { - ChunkPos p = chunks.pop(); - Random random = new Random(world.getSeed()); - // re-seed with something totally new :P - random.setSeed((((random.nextLong() >> 4 + 1) + p.x) + ((random.nextLong() >> 2 + 1) + p.z)) ^ world.getSeed()); - ChunkProviderServer chunkProvider = (ChunkProviderServer) world.getChunkProvider(); - IChunkGenerator chunkGenerator = ObfuscationReflectionHelper.getPrivateValue(ChunkProviderServer.class, chunkProvider, "field_186029_c", "chunkGenerator"); - OreSpawn.API.getGenerator().generate(random, p.x, p.z, world, chunkGenerator, chunkProvider); - } - - for (int c = 0; c < 5 && !retroChunks.isEmpty(); c++) { - ChunkPos p = retroChunks.pop(); - OreSpawn.flatBedrock.retrogen(world, p.x, p.z); - } - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/OreSpawn.java b/src/main/java/com/mcmoddev/orespawn/OreSpawn.java index b8695802..41570e1e 100644 --- a/src/main/java/com/mcmoddev/orespawn/OreSpawn.java +++ b/src/main/java/com/mcmoddev/orespawn/OreSpawn.java @@ -1,107 +1,38 @@ package com.mcmoddev.orespawn; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.data.FeatureRegistry; -import com.mcmoddev.orespawn.impl.os3.OS3APIImpl; -import com.mcmoddev.orespawn.json.OS3Reader; -import com.mcmoddev.orespawn.json.OS3Writer; -import com.mcmoddev.orespawn.commands.AddOreCommand; -import com.mcmoddev.orespawn.commands.ClearChunkCommand; -import com.mcmoddev.orespawn.commands.WriteConfigsCommand; -import com.mcmoddev.orespawn.commands.DumpBiomesCommand; -import com.mcmoddev.orespawn.data.Config; -import com.mcmoddev.orespawn.api.os3.OS3API; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.api.plugin.PluginLoader; -import com.mcmoddev.orespawn.worldgen.FlatBedrock; - -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import net.minecraftforge.common.MinecraftForge; -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.event.FMLFingerprintViolationEvent; +import com.mcmoddev.orespawn.api.os3.OS3API; +import com.mcmoddev.orespawn.compat.LegacyOs3Bridge; +import com.mcmoddev.orespawn.data.FeatureRegistry; +import com.mcmoddev.orespawn.json.OS3Writer; +import net.minecraftforge.fml.common.event.FMLFingerprintViolationEvent; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLServerStartingEvent; -import net.minecraftforge.fml.common.registry.GameRegistry; - -/** - * Main entry point for the mod, everything runs through this - * - * @author DShadowWolf <dshadowwolf@gmail.com> - */ - -@Mod(modid = Constants.MODID, - name = Constants.NAME, - version = Constants.VERSION, - acceptedMinecraftVersions = "[1.11.2,)", - certificateFingerprint = "@FINGERPRINT@") +/** Deprecated OS3 facade. OreSpawn 4 owns the actual mod lifecycle. */ +@Deprecated public class OreSpawn { - @Instance - public static OreSpawn instance; - - public static final Logger LOGGER = LogManager.getFormatterLogger(Constants.MODID); - public static final OS3API API = new OS3APIImpl(); + public static OreSpawn instance = new OreSpawn(); + public static final Logger LOGGER = LogManager.getLogger("OreSpawn-OS3-Bridge"); + public static final OS3API API = LegacyOs3Bridge.api(); public static final OS3Writer writer = new OS3Writer(); - static final EventHandlers eventHandlers = new EventHandlers(); - public static final FeatureRegistry FEATURES = new FeatureRegistry(); - protected static final Map> spawns = new HashMap<>(); - - static final FlatBedrock flatBedrock = new FlatBedrock(); - - public static Map> getSpawns() { - return spawns; - } - - @EventHandler - public void onFingerprintViolation(FMLFingerprintViolationEvent event) { - LOGGER.warn("Invalid fingerprint detected!"); - } - - @EventHandler - public void preInit(FMLPreInitializationEvent ev) { - Config.loadConfig(); - - PluginLoader.INSTANCE.load(ev); - - if (Config.getBoolean(Constants.FLAT_BEDROCK)) { - GameRegistry.registerWorldGenerator(flatBedrock, 100); - } - - if (Config.getBoolean(Constants.RETROGEN_KEY) || Config.getBoolean(Constants.REPLACE_VANILLA_OREGEN) || Config.getBoolean(Constants.RETRO_BEDROCK)) { - MinecraftForge.EVENT_BUS.register(eventHandlers); - MinecraftForge.ORE_GEN_BUS.register(eventHandlers); - } - } - - @EventHandler - public void init(FMLInitializationEvent ev) { - PluginLoader.INSTANCE.register(); - - OS3Reader.loadEntries(); - writer.writeSysconfIfNonexistent(); - API.registerSpawns(); - } - - @EventHandler - public void postInit(FMLPostInitializationEvent ev) { - Config.saveConfig(); - } - - @EventHandler - public void onServerStarting(FMLServerStartingEvent ev) { - ev.registerServerCommand(new ClearChunkCommand()); - ev.registerServerCommand(new DumpBiomesCommand()); - ev.registerServerCommand(new AddOreCommand()); - ev.registerServerCommand(new WriteConfigsCommand()); - } + public static final FeatureRegistry FEATURES = LegacyOs3Bridge.features(); + @SuppressWarnings("rawtypes") + protected static final Map spawns = Collections.emptyMap(); + public OreSpawn() { } + @SuppressWarnings("rawtypes") + public static Map getSpawns() { return spawns; } + public void onFingerprintViolation(FMLFingerprintViolationEvent event) { } + public void preInit(FMLPreInitializationEvent event) { LegacyOs3Bridge.initialize(event); } + public void init(FMLInitializationEvent event) { } + public void postInit(FMLPostInitializationEvent event) { } + public void onServerStarting(FMLServerStartingEvent event) { } } diff --git a/src/main/java/com/mcmoddev/orespawn/api/BiomeLocation.java b/src/main/java/com/mcmoddev/orespawn/api/BiomeLocation.java index 64022765..9e2ae586 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/BiomeLocation.java +++ b/src/main/java/com/mcmoddev/orespawn/api/BiomeLocation.java @@ -1,15 +1,25 @@ package com.mcmoddev.orespawn.api; import com.google.common.collect.ImmutableList; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; -import com.mcmoddev.orespawn.util.Collectors2; import net.minecraft.world.biome.Biome; import net.minecraftforge.fml.common.registry.ForgeRegistries; +/** Binary-compatible union of the OS3 3.2 and 3.3 biome contracts. */ public interface BiomeLocation { boolean matches(Biome biome); -default ImmutableList getBiomes() { - return ForgeRegistries.BIOMES.getValues().stream().filter(this::matches).collect(Collectors2.toImmutableList()); + default JsonElement serialize() { + return new JsonArray(); + } + + default ImmutableList getBiomes() { + ImmutableList.Builder result = ImmutableList.builder(); + for (Biome biome : ForgeRegistries.BIOMES.getValues()) { + if (matches(biome)) result.add(biome); + } + return result.build(); } } diff --git a/src/main/java/com/mcmoddev/orespawn/api/FeatureBase.java b/src/main/java/com/mcmoddev/orespawn/api/FeatureBase.java index fd273c46..0a95ca9a 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/FeatureBase.java +++ b/src/main/java/com/mcmoddev/orespawn/api/FeatureBase.java @@ -1,366 +1,107 @@ package com.mcmoddev.orespawn.api; -import com.google.common.collect.ImmutableSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Random; + +import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.impl.location.BiomeLocationComposition; +import com.google.gson.JsonParser; +import com.mcmoddev.orespawn.api.os3.ISpawnEntry; import com.mcmoddev.orespawn.util.OreList; + import net.minecraft.block.state.IBlockState; import net.minecraft.init.Blocks; import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.Vec3i; import net.minecraft.world.World; -import net.minecraft.world.biome.Biome; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; -import java.util.*; -import java.util.Map.Entry; - -public class FeatureBase { - private static final int MAX_CACHE_SIZE = 2048; - /** overflow cache so that ores that spawn at edge of chunk can - * appear in the neighboring chunk without triggering a chunk-load */ - private static final Map> overflowCache = new HashMap<>(MAX_CACHE_SIZE); - private static final Deque cacheOrder = new LinkedList<>(); +/** Allocation-light compatibility base retaining both published OS3 ABIs. */ +public class FeatureBase extends IForgeRegistryEntry.Impl { + private static final int GENERATION_WRITE_FLAGS = 2 | 16; protected Random random; + protected static final Vec3i[] offsets_small = offsets(2); + protected static final Vec3i[] offsets = offsets(4); + protected static final int[] offsetIndexRef = indexes(offsets.length); + protected static final int[] offsetIndexRef_small = indexes(offsets_small.length); - public FeatureBase(Random rand) { - this.random = rand; - } + public FeatureBase(Random random) { this.random = random; } - private boolean fullMatch(ImmutableSet locs, Biome biome) { - for (BiomeLocation b : locs) { - for (Biome bm : b.getBiomes()) { - if (bm.equals(biome)) { - return true; - } - } - } - - return false; + public boolean isValidBlock(IBlockState state) { + return state != null && state.getBlock() != Blocks.AIR; } - private boolean biomeMatch(Biome chunkBiome, BiomeLocation inp) { - if (inp.getBiomes().isEmpty()) { - return false; - } - - if (inp instanceof BiomeLocationComposition) { - BiomeLocationComposition loc = (BiomeLocationComposition) inp; - boolean exclMatch = fullMatch(loc.getExclusions(), chunkBiome); - boolean inclMatch = fullMatch(loc.getInclusions(), chunkBiome); + protected void runCache(int chunkX, int chunkZ, World world, ISpawnEntry spawn) { } + protected void runCache(int chunkX, int chunkZ, World world, List replacements) { } - if ((loc.getInclusions().isEmpty() || inclMatch) && !exclMatch) { - return false; - } - } else if (inp.matches(chunkBiome)) { + protected boolean spawn(IBlockState ore, World world, BlockPos pos, int dimension, + boolean cacheOverflow, ISpawnEntry spawn) { + if (!isValidBlock(ore) || spawn == null || !spawn.dimensionAllowed(dimension) + || !spawn.biomeAllowed(world.getBiome(pos)) || !spawn.getMatcher().test(world.getBlockState(pos))) { return false; } - - return true; + IBlockState output = spawn.getBlocks().getRandomBlock(random); + return output != null && world.setBlockState(pos, output, GENERATION_WRITE_FLAGS); } - protected void runCache(int chunkX, int chunkZ, World world, List blockReplace) { - Vec3i chunkCoord = new Vec3i(chunkX, chunkZ, world.provider.getDimension()); - Map cache = retrieveCache(chunkCoord); - - if (!cache.isEmpty()) { // if there is something in the cache, try to spawn it - for (Entry ent : cache.entrySet()) { - spawnNoCheck(cache.get(ent.getKey()), world, ent.getKey(), world.provider.getDimension(), blockReplace); - } - } + protected boolean spawn(IBlockState ore, World world, BlockPos pos, int dimension, + boolean cacheOverflow, List replacements, BiomeLocation biomes) { + if (!isValidBlock(ore) || (biomes != null && !biomes.matches(world.getBiome(pos)))) return false; + IBlockState current = world.getBlockState(pos); + if (replacements != null && !replacements.isEmpty() && !replacements.contains(current)) return false; + return world.setBlockState(pos, ore, GENERATION_WRITE_FLAGS); } - protected boolean spawn(IBlockState oreBlock, World world, BlockPos coord, int dimension, boolean cacheOverflow, - List blockReplace, BiomeLocation biomes) { - if (oreBlock == null) { - OreSpawn.LOGGER.fatal("FeatureBase.spawn() called with a null ore!"); - return false; + protected void scramble(int[] values, Random rand) { + for (int i = values.length - 1; i > 0; i--) { + int j = rand.nextInt(i + 1); int value = values[i]; values[i] = values[j]; values[j] = value; } - - Biome thisBiome = world.getBiome(coord); - - if (biomeMatch(thisBiome, biomes)) { - return false; - } - - BlockPos np = mungeFixYcoord(coord); - - if (coord.getY() >= world.getHeight()) { - OreSpawn.LOGGER.warn("Asked to spawn %s above build limit at %s", oreBlock, coord); - return false; - } - - return spawnOrCache(world, np, blockReplace, oreBlock, cacheOverflow, dimension); } - private BlockPos mungeFixYcoord(BlockPos coord) { - if (coord.getY() < 0) { - int newYCoord = coord.getY() * -1; - return new BlockPos(coord.getX(), newYCoord, coord.getZ()); - } else { - return new BlockPos(coord); + protected static void mergeDefaults(JsonObject target, JsonObject defaults) { + for (Entry entry : defaults.entrySet()) { + if (!target.has(entry.getKey())) target.add(entry.getKey(), new JsonParser().parse(entry.getValue().toString())); } } - private boolean spawnOrCache(World world, BlockPos coord, List blockReplace, IBlockState oreBlock, boolean cacheOverflow, int dimension) { - if (world.isBlockLoaded(coord)) { - IBlockState targetBlock = world.getBlockState(coord); - - if (canReplace(targetBlock, blockReplace)) { - world.setBlockState(coord, oreBlock); - return true; - } else { - return false; - } - } else if (cacheOverflow) { - cacheOverflowBlock(oreBlock, coord, dimension); - return true; - } - - return false; + protected int getPoint(int center, int radius, int spread) { + return center + random.nextInt(Math.max(1, spread * 2 + 1)) - spread; } - private void spawnNoCheck(IBlockState oreBlock, World world, BlockPos coord, int dimension, - List blockReplace) { - if (oreBlock == null) { - OreSpawn.LOGGER.fatal("FeatureBase.spawn() called with a null ore!"); - return; - } - - BlockPos np = mungeFixYcoord(coord); - - if (coord.getY() >= world.getHeight()) { - OreSpawn.LOGGER.warn("Asked to spawn %s above build limit at %s", oreBlock, coord); - return; - } - - spawnOrCache(world, np, blockReplace, oreBlock, false, dimension); + protected void spawnMungeInner(Random rand, int quantity, int dimension, Vec3i offset, + ISpawnEntry spawn, World world, BlockPos origin) { + for (int i = 0; i < quantity; i++) spawn(world.getBlockState(origin.add(offset)), world, + origin.add(offset), dimension, false, spawn); } - private void cacheOverflowBlock(IBlockState bs, BlockPos coord, int dimension) { - Vec3i chunkCoord = new Vec3i(coord.getX() >> 4, coord.getY() >> 4, dimension); - - if (overflowCache.containsKey(chunkCoord)) { - cacheOrder.addLast(chunkCoord); + protected void spawnMungeSW(World world, BlockPos origin, int quantity, double variation, + ISpawnEntry spawn, int dimension) { spawnMungeInner(random, quantity, dimension, Vec3i.NULL_VECTOR, spawn, world, origin); } + protected void spawnMungeNE(World world, BlockPos origin, int quantity, double variation, + ISpawnEntry spawn, int dimension) { spawnMungeInner(random, quantity, dimension, Vec3i.NULL_VECTOR, spawn, world, origin); } + protected void spawnMungeSW(World world, BlockPos origin, int quantity, double variation, + List replacements, int dimension, OreList ores) { spawnLegacy(world, origin, quantity, dimension, replacements, ores); } + protected void spawnMungeNE(World world, BlockPos origin, int quantity, double variation, + List replacements, int dimension, OreList ores) { spawnLegacy(world, origin, quantity, dimension, replacements, ores); } - if (cacheOrder.size() > MAX_CACHE_SIZE) { - Vec3i drop = cacheOrder.removeFirst(); - overflowCache.get(drop).clear(); - overflowCache.remove(drop); - } - - overflowCache.put(chunkCoord, new HashMap<>()); - } - - Map cache = overflowCache.getOrDefault(chunkCoord, new HashMap<>()); - cache.put(coord, bs); - } - - private Map retrieveCache(Vec3i chunkCoord) { - if (overflowCache.containsKey(chunkCoord)) { - Map cache = overflowCache.get(chunkCoord); - cacheOrder.remove(chunkCoord); - overflowCache.remove(chunkCoord); - return cache; - } else { - return Collections.emptyMap(); + private void spawnLegacy(World world, BlockPos origin, int quantity, int dimension, + List replacements, OreList ores) { + for (int i = 0; i < quantity; i++) { + com.mcmoddev.orespawn.api.os3.OreBuilder ore = ores.getRandomOre(random); + if (ore != null) spawn(ore.getOre(), world, origin, dimension, false, replacements, null); } } - protected void scramble(int[] target, Random prng) { - for (int i = target.length - 1; i > 0; i--) { - int n = prng.nextInt(i); - int temp = target[i]; - target[i] = target[n]; - target[n] = temp; - } - } - - private boolean canReplace(IBlockState target, List blockToReplace) { - return !target.getBlock().equals(Blocks.AIR) && blockToReplace.contains(target); - } - - protected static final Vec3i[] offsets_small = { - new Vec3i(0, 0, 0), new Vec3i(1, 0, 0), - new Vec3i(0, 1, 0), new Vec3i(1, 1, 0), - - new Vec3i(0, 0, 1), new Vec3i(1, 0, 1), - new Vec3i(0, 1, 1), new Vec3i(1, 1, 1) - }; - - protected static final Vec3i[] offsets = { - new Vec3i(-1, -1, -1), new Vec3i(0, -1, -1), new Vec3i(1, -1, -1), - new Vec3i(-1, 0, -1), new Vec3i(0, 0, -1), new Vec3i(1, 0, -1), - new Vec3i(-1, 1, -1), new Vec3i(0, 1, -1), new Vec3i(1, 1, -1), - - new Vec3i(-1, -1, 0), new Vec3i(0, -1, 0), new Vec3i(1, -1, 0), - new Vec3i(-1, 0, 0), new Vec3i(0, 0, 0), new Vec3i(1, 0, 0), - new Vec3i(-1, 1, 0), new Vec3i(0, 1, 0), new Vec3i(1, 1, 0), - - new Vec3i(-1, -1, 1), new Vec3i(0, -1, 1), new Vec3i(1, -1, 1), - new Vec3i(-1, 0, 1), new Vec3i(0, 0, 1), new Vec3i(1, 0, 1), - new Vec3i(-1, 1, 1), new Vec3i(0, 1, 1), new Vec3i(1, 1, 1) - }; - - protected static final int[] offsetIndexRef = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}; - protected static final int[] offsetIndexRef_small = {0, 1, 2, 3, 4, 5, 6, 7}; - - protected static void mergeDefaults(JsonObject parameters, JsonObject defaultParameters) { - defaultParameters.entrySet().forEach(entry -> { - if (!parameters.has(entry.getKey())) - parameters.add(entry.getKey(), entry.getValue()); - }); - } - - private double triangularDistribution(double a, double b, double c) { - double base = (c - a) / (b - a); - double rand = this.random.nextDouble(); + protected int getABC(int a, int b, int c) { return Math.max(a, Math.max(b, c)); } + protected int countItem(int value, boolean small) { return Math.max(0, value); } + protected boolean endCheck(boolean reverse, int value, double limit) { return reverse ? value <= limit : value >= limit; } + protected int getStart(boolean reverse, double value) { return (int) Math.floor(value); } - if (rand < base) { - return a + Math.sqrt(rand * (b - a) * (c - a)); - } else { - return b - Math.sqrt((1 - rand) * (b - a) * (b - c)); - } - } - - protected int getPoint(int lowerBound, int upperBound, int median) { - int t = (int)Math.round(triangularDistribution((float)lowerBound, (float)upperBound, (float)median)); - return t - median; - } - - protected void spawnMungeSW(World world, BlockPos blockPos, int rSqr, double radius, - List replaceBlock, int count, OreList possibleOres) { - Random prng = this.random; - int quantity = count; - for(int dy = (int)(-1 * radius); dy < radius; dy++){ - for(int dx = (int)(radius); dx >= (int)(-1 * radius); dx--){ - for(int dz = (int)(radius); dz >= (int)(-1 * radius); dz--){ - if((dx*dx + dy*dy + dz*dz) <= rSqr){ - IBlockState oreBlock = possibleOres.getRandomOre(prng).getOre(); - spawnOrCache(world,blockPos.add(dx,dy,dz),replaceBlock, oreBlock, true, world.provider.getDimension()); - quantity--; - } - if(quantity <= 0) { - return; - } - } - } - } - } - - - protected void spawnMungeNE(World world, BlockPos blockPos, int rSqr, double radius, - List replaceBlock, int count, OreList possibleOres) { - Random prng = this.random; - int quantity = count; - for(int dy = (int)(-1 * radius); dy < radius; dy++){ - for(int dz = (int)(-1 * radius); dz < radius; dz++){ - for(int dx = (int)(-1 * radius); dx < radius; dx++){ - if((dx*dx + dy*dy + dz*dz) <= rSqr){ - IBlockState oreBlock = possibleOres.getRandomOre(prng).getOre(); - spawnOrCache(world,blockPos.add(dx,dy,dz),replaceBlock, oreBlock, true, world.provider.getDimension()); - quantity--; - } - if(quantity <= 0) { - return; - } - } - } - } - } - - protected int getABC(int dx, int dy, int dz) { - return (dx * dx + dy * dy + dz * dz); - } - - protected int countItem(int dx, boolean toPositive) { - return toPositive ? dx + 1 : dx - 1; - } - - protected boolean endCheck(boolean toPositive, int dx, double radius) { - return toPositive ? (dx >= getStart(toPositive, radius)) : (dx < radius); - } - - protected int getStart(boolean toPositive, double radius) { - return ((int)(radius * (toPositive ? 1 : -1))); - } - - public class FunctionParameterWrapper { - private World world; - private BlockPos blockPos; - private List replacements; - private OreList ores; - private BiomeLocation biomes; - private ChunkPos chunkPos; - private IBlockState block; - - public FunctionParameterWrapper() {} - - public FunctionParameterWrapper(FunctionParameterWrapper other) { - world = other.getWorld(); - blockPos = other.getBlockPos(); - replacements = other.getReplacements(); - ores = other.getOres(); - biomes = other.getBiomes(); - chunkPos = other.getChunkPos(); - block = other.getBlock(); - } - - public BiomeLocation getBiomes() { - return biomes; - } - - public void setBiomes(BiomeLocation biomes) { - this.biomes = biomes; - } - - public World getWorld() { - return world; - } - - public void setWorld(World world) { - this.world = world; - } - - public BlockPos getBlockPos() { - return blockPos; - } - - public void setBlockPos(BlockPos blockPos) { - this.blockPos = blockPos; - } - - public List getReplacements() { - return replacements; - } - - public void setReplacements(List replacements) { - this.replacements = replacements; - } - - public OreList getOres() { - return ores; - } - - public void setOres(OreList ores) { - this.ores = ores; - } - - public ChunkPos getChunkPos() { - return chunkPos; - } - - public void setChunkPos(ChunkPos chunkPos) { - this.chunkPos = chunkPos; - } - - public IBlockState getBlock() { - return block; - } - - public void setBlock(IBlockState block) { - this.block = block; - } + private static Vec3i[] offsets(int radius) { + java.util.ArrayList result = new java.util.ArrayList<>(); + for (int y = -radius; y <= radius; y++) for (int z = -radius; z <= radius; z++) + for (int x = -radius; x <= radius; x++) result.add(new Vec3i(x, y, z)); + return result.toArray(new Vec3i[result.size()]); } + private static int[] indexes(int size) { int[] result = new int[size]; for (int i = 0; i < size; i++) result[i] = i; return result; } } diff --git a/src/main/java/com/mcmoddev/orespawn/api/IBlockList.java b/src/main/java/com/mcmoddev/orespawn/api/IBlockList.java new file mode 100644 index 00000000..e6c53208 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/IBlockList.java @@ -0,0 +1,23 @@ +package com.mcmoddev.orespawn.api; + +import java.util.Random; +import com.mcmoddev.orespawn.api.os3.IBlockDefinition; +import net.minecraft.block.state.IBlockState; + + +import com.mcmoddev.orespawn.api.os3.IBlockDefinition; + +import net.minecraft.block.state.IBlockState; + +public interface IBlockList { + + void addBlock(IBlockDefinition block); + + IBlockState getRandomBlock(Random rand); + + void startNewSpawn(); + + void dump(); + + int count(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/IDimensionList.java b/src/main/java/com/mcmoddev/orespawn/api/IDimensionList.java new file mode 100644 index 00000000..871fb126 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/IDimensionList.java @@ -0,0 +1,12 @@ +package com.mcmoddev.orespawn.api; + +import com.google.gson.JsonObject; + +public interface IDimensionList { + + JsonObject serialize(); + + default boolean matches(int dimensionId) { + return false; + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/IFeature.java b/src/main/java/com/mcmoddev/orespawn/api/IFeature.java index 8bf50b65..12721e14 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/IFeature.java +++ b/src/main/java/com/mcmoddev/orespawn/api/IFeature.java @@ -3,16 +3,36 @@ import java.util.Random; import com.google.gson.JsonObject; +import com.mcmoddev.orespawn.api.os3.ISpawnEntry; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.ChunkPos; import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkGenerator; import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.IChunkGenerator; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; -public interface IFeature { - void generate(World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider, - GeneratorParameters parameters); +/** + * Compatibility union. Each historical generate descriptor is a default so a + * binary compiled for the other OS3 generation can still be loaded safely. + */ +public interface IFeature extends IForgeRegistryEntry { + default void generate(World world, IChunkGenerator generator, IChunkProvider provider, + GeneratorParameters parameters) { + throw new UnsupportedOperationException("OS3 3.2 feature entry point is not implemented"); + } - void setRandom(Random rand); + default void generate(World world, IChunkGenerator generator, IChunkProvider provider, + ISpawnEntry spawn, ChunkPos pos) { + throw new UnsupportedOperationException("OS3 3.3 feature entry point is not implemented"); + } + + void setRandom(Random random); JsonObject getDefaultParameters(); + + @Override default IFeature setRegistryName(ResourceLocation name) { return this; } + @Override default ResourceLocation getRegistryName() { return null; } + @SuppressWarnings("unchecked") + @Override default Class getRegistryType() { return (Class) (Class) IFeature.class; } } diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/BadStateValueException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/BadStateValueException.java new file mode 100644 index 00000000..a5c10007 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/BadStateValueException.java @@ -0,0 +1,13 @@ +package com.mcmoddev.orespawn.api.exceptions; + +public class BadStateValueException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 3826628238012469423L; + + public BadStateValueException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/BadValueException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/BadValueException.java new file mode 100644 index 00000000..d6b3b722 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/BadValueException.java @@ -0,0 +1,23 @@ +package com.mcmoddev.orespawn.api.exceptions; + +import java.util.Locale; + +public class BadValueException extends Exception { + + private static final long serialVersionUID = 1143938140559149506L; + private final String keyName; + private final String keyValue; + + public BadValueException(final String keyName, final String keyValue) { + super(); + this.keyName = keyName; + this.keyValue = keyValue; + } + + @Override + public String getMessage() { + final String baseMessage = super.getMessage(); + return String.format(Locale.ENGLISH, "Unknown value %s for key %s%n%s", this.keyValue, this.keyName, + baseMessage); + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/MissingVersionException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/MissingVersionException.java new file mode 100644 index 00000000..d3675bd3 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/MissingVersionException.java @@ -0,0 +1,10 @@ +package com.mcmoddev.orespawn.api.exceptions; + +public class MissingVersionException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -4306852267351590384L; + +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/NotAProperConfigException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/NotAProperConfigException.java new file mode 100644 index 00000000..bba442ad --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/NotAProperConfigException.java @@ -0,0 +1,10 @@ +package com.mcmoddev.orespawn.api.exceptions; + +public class NotAProperConfigException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -7748241590958198482L; + +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/OldVersionException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/OldVersionException.java new file mode 100644 index 00000000..7adb98ba --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/OldVersionException.java @@ -0,0 +1,10 @@ +package com.mcmoddev.orespawn.api.exceptions; + +public class OldVersionException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 3760017140789193369L; + +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownFieldException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownFieldException.java new file mode 100644 index 00000000..53a8b103 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownFieldException.java @@ -0,0 +1,20 @@ +package com.mcmoddev.orespawn.api.exceptions; + +import java.util.Locale; + +public class UnknownFieldException extends Exception { + + private static final long serialVersionUID = 1L; + private final String message; + + public UnknownFieldException(final String theField) { + super(); + this.message = String.format(Locale.ENGLISH, "Unkown field %s in config", theField); + } + + @Override + public String getMessage() { + final String baseMessage = super.getMessage(); + return String.format(Locale.ENGLISH, "%s%n%s", this.message, baseMessage); + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownNameException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownNameException.java new file mode 100644 index 00000000..cca396fa --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownNameException.java @@ -0,0 +1,23 @@ +package com.mcmoddev.orespawn.api.exceptions; + +import java.util.Locale; + +public class UnknownNameException extends Exception { + + private static final long serialVersionUID = -3426121906665390773L; + private final String fieldName; + private final String fieldValue; + + public UnknownNameException(final String fieldName, final String fieldValue) { + super(); + this.fieldName = fieldName; + this.fieldValue = fieldValue; + } + + @Override + public String getMessage() { + final String baseMessage = super.getMessage(); + return String.format(Locale.ENGLISH, "Unknown %s name %s%n%s", this.fieldName, this.fieldValue, + baseMessage); + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownVersionException.java b/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownVersionException.java new file mode 100644 index 00000000..5448c6cb --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/exceptions/UnknownVersionException.java @@ -0,0 +1,10 @@ +package com.mcmoddev.orespawn.api.exceptions; + +public class UnknownVersionException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 3817238409227005355L; + +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IBiomeBuilder.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IBiomeBuilder.java new file mode 100644 index 00000000..f4a924b1 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IBiomeBuilder.java @@ -0,0 +1,28 @@ +package com.mcmoddev.orespawn.api.os3; + +import com.mcmoddev.orespawn.api.BiomeLocation; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.biome.Biome; + + +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.biome.Biome; + +public interface IBiomeBuilder { + + IBiomeBuilder addWhitelistEntry(Biome biome); + + IBiomeBuilder addWhitelistEntry(String biomeName); + + IBiomeBuilder addWhitelistEntry(ResourceLocation biomeResourceLocation); + + IBiomeBuilder addBlacklistEntry(Biome biome); + + IBiomeBuilder addBlacklistEntry(String biomeName); + + IBiomeBuilder addBlacklistEntry(ResourceLocation biomeResourceLocation); + + IBiomeBuilder setAcceptAll(); + + BiomeLocation create(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IBlockBuilder.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IBlockBuilder.java new file mode 100644 index 00000000..f77addea --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IBlockBuilder.java @@ -0,0 +1,159 @@ +package com.mcmoddev.orespawn.api.os3; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.ResourceLocation; + +public interface IBlockBuilder { + + /** + * + * @param blockState + * @return + */ + IBlockBuilder setFromBlockState(IBlockState blockState); + + /** + * + * @param block + * @return + */ + IBlockBuilder setFromBlock(Block block); + + /** + * + * @param blockName + * @return + */ + IBlockBuilder setFromName(String blockName); + + /** + * + * @param blockName + * @param state + * @return + */ + IBlockBuilder setFromName(String blockName, String state); + + /** + * + * @param blockName + * @param metadata + * @return + * @deprecated + */ + @Deprecated + IBlockBuilder setFromName(String blockName, int metadata); + + /** + * + * @param blockResourceLocation + * @return + */ + IBlockBuilder setFromName(ResourceLocation blockResourceLocation); + + /** + * + * @param blockResourceLocation + * @param state + * @return + */ + IBlockBuilder setFromName(ResourceLocation blockResourceLocation, String state); + + /** + * + * @param blockResourceLocation + * @param metadata + * @return + * @deprecated + */ + @Deprecated + IBlockBuilder setFromName(ResourceLocation blockResourceLocation, int metadata); + + /** + * + * @param blockState + * @param chance + * @return + */ + IBlockBuilder setFromBlockStateWithChance(IBlockState blockState, int chance); + + /** + * + * @param block + * @param chance + * @return + */ + IBlockBuilder setFromBlockWithChance(Block block, int chance); + + /** + * + * @param blockName + * @param chance + * @return + */ + IBlockBuilder setFromNameWithChance(String blockName, int chance); + + /** + * + * @param blockName + * @param state + * @param chance + * @return + */ + IBlockBuilder setFromNameWithChance(String blockName, String state, int chance); + + /** + * + * @param blockName + * @param metadata + * @param chance + * @return + * @deprecated + */ + @Deprecated + IBlockBuilder setFromNameWithChance(String blockName, int metadata, int chance); + + /** + * + * @param blockResourceLocation + * @param chance + * @return + */ + IBlockBuilder setFromNameWithChance(ResourceLocation blockResourceLocation, int chance); + + /** + * + * @param blockResourceLocation + * @param state + * @param chance + * @return + */ + IBlockBuilder setFromNameWithChance(ResourceLocation blockResourceLocation, String state, + int chance); + + /** + * + * @param blockResourceLocation + * @param metadata + * @param chance + * @return + * @deprecated + */ + @Deprecated + IBlockBuilder setFromNameWithChance(ResourceLocation blockResourceLocation, int metadata, + int chance); + + /** + * + * @param chance + * @return + */ + IBlockBuilder setChance(int chance); + + /** + * + * @return + */ + IBlockDefinition create(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IBlockDefinition.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IBlockDefinition.java new file mode 100644 index 00000000..fef77068 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IBlockDefinition.java @@ -0,0 +1,14 @@ +package com.mcmoddev.orespawn.api.os3; + +import net.minecraft.block.state.IBlockState; + +public interface IBlockDefinition { + + IBlockState getBlock(); + + int getChance(); + + default boolean isValid() { + return true; + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IDimensionBuilder.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IDimensionBuilder.java new file mode 100644 index 00000000..7aaa01f5 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IDimensionBuilder.java @@ -0,0 +1,18 @@ +package com.mcmoddev.orespawn.api.os3; + +import com.mcmoddev.orespawn.api.IDimensionList; + +public interface IDimensionBuilder { + + IDimensionBuilder addWhitelistEntry(int dimensionID); + + IDimensionBuilder addBlacklistEntry(int dimensionID); + + IDimensionBuilder setAcceptAll(); + + IDimensionBuilder setDenyAll(); + + IDimensionBuilder setAcceptAllOverworld(); + + IDimensionList create(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IFeatureBuilder.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IFeatureBuilder.java new file mode 100644 index 00000000..5352f9e9 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IFeatureBuilder.java @@ -0,0 +1,31 @@ +package com.mcmoddev.orespawn.api.os3; + +import com.google.gson.JsonElement; +import com.mcmoddev.orespawn.api.IFeature; +import net.minecraft.util.ResourceLocation; + + +import net.minecraft.util.ResourceLocation; + +public interface IFeatureBuilder { + + IFeatureBuilder setFeature(String featureName); + + IFeatureBuilder setFeature(ResourceLocation featureResourceLocation); + + IFeatureBuilder setFeature(IFeature feature); + + IFeatureBuilder setParameter(String parameterName, String parameterValue); + + IFeatureBuilder setParameter(String parameterName, int parameterValue); + + IFeatureBuilder setParameter(String parameterName, float parameterValue); + + IFeatureBuilder setParameter(String parameterName, boolean parameterValue); + + IFeatureBuilder setParameter(String parameterName, JsonElement parameterValue); + + IFeatureBuilder setUseFeatureDefaults(); + + IFeatureEntry create(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IFeatureEntry.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IFeatureEntry.java new file mode 100644 index 00000000..312abf6f --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IFeatureEntry.java @@ -0,0 +1,21 @@ +package com.mcmoddev.orespawn.api.os3; + +import com.google.gson.JsonObject; +import com.mcmoddev.orespawn.api.IFeature; + +public interface IFeatureEntry { + + IFeature getFeature(); + + String getFeatureName(); + + JsonObject getFeatureParameters(); + + void setParameter(String parameterName, String parameterValue); + + void setParameter(String parameterName, int parameterValue); + + void setParameter(String parameterName, boolean parameterValue); + + void setParameter(String parameterName, float parameterValue); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IReplacementBuilder.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IReplacementBuilder.java new file mode 100644 index 00000000..ab3ba943 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IReplacementBuilder.java @@ -0,0 +1,82 @@ +package com.mcmoddev.orespawn.api.os3; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.ResourceLocation; + +public interface IReplacementBuilder { + + /** + * + * @param entryName + * @return + */ + IReplacementBuilder setFromName(String entryName); + + /** + * + * @param name + * @return + */ + IReplacementBuilder setName(String name); + + /** + * + * @param blockState + * @return + */ + IReplacementBuilder addEntry(IBlockState blockState); + + /** + * + * @param blockName + * @return + */ + IReplacementBuilder addEntry(String blockName); + + /** + * + * @param blockName + * @param state + * @return + */ + IReplacementBuilder addEntry(String blockName, String state); + + /** + * + * @param blockName + * @param metadata + * @return + * @deprecated + */ + @Deprecated + IReplacementBuilder addEntry(String blockName, int metadata); + + /** + * + * @param blockResourceLocation + * @return + */ + IReplacementBuilder addEntry(ResourceLocation blockResourceLocation); + + /** + * + * @param blockResourceLocation + * @param state + * @return + */ + IReplacementBuilder addEntry(ResourceLocation blockResourceLocation, String state); + + /** + * + * @param blockResourceLocation + * @param metadata + * @return + * @deprecated + */ + @Deprecated + IReplacementBuilder addEntry(ResourceLocation blockResourceLocation, int metadata); + + boolean hasEntries(); + + IReplacementEntry create(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/IReplacementEntry.java b/src/main/java/com/mcmoddev/orespawn/api/os3/IReplacementEntry.java new file mode 100644 index 00000000..d8e9476f --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/IReplacementEntry.java @@ -0,0 +1,16 @@ +package com.mcmoddev.orespawn.api.os3; + +import java.util.List; +import net.minecraft.block.state.IBlockState; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; + + +import net.minecraft.block.state.IBlockState; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; + +public interface IReplacementEntry extends IForgeRegistryEntry { + + OreSpawnBlockMatcher getMatcher(); + + List getEntries(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/ISpawnBuilder.java b/src/main/java/com/mcmoddev/orespawn/api/os3/ISpawnBuilder.java new file mode 100644 index 00000000..02c198ce --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/ISpawnBuilder.java @@ -0,0 +1,67 @@ +package com.mcmoddev.orespawn.api.os3; + +import com.mcmoddev.orespawn.api.BiomeLocation; +import com.mcmoddev.orespawn.api.IDimensionList; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.ResourceLocation; + + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.ResourceLocation; + +public interface ISpawnBuilder { + + ISpawnBuilder setName(String name); + + ISpawnBuilder setDimensions(IDimensionList dimensions); + + ISpawnBuilder setBiomes(BiomeLocation biomes); + + ISpawnBuilder setEnabled(boolean enabled); + + ISpawnBuilder setRetrogen(boolean retrogen); + + ISpawnBuilder setReplacement(IReplacementEntry replacements); + + ISpawnBuilder setFeature(IFeatureEntry feature); + + ISpawnBuilder addBlock(String blockName); + + ISpawnBuilder addBlock(String blockName, String blockState); + + ISpawnBuilder addBlock(String blockName, int blockMetadata); + + ISpawnBuilder addBlock(ResourceLocation blockResourceLocation); + + ISpawnBuilder addBlock(ResourceLocation blockResourceLocation, String blockState); + + ISpawnBuilder addBlock(ResourceLocation blockResourceLocation, int blockMetadata); + + ISpawnBuilder addBlock(Block block); + + ISpawnBuilder addBlock(IBlockState block); + + ISpawnBuilder addBlockWithChance(String blockName, int chance); + + ISpawnBuilder addBlockWithChance(String blockName, String blockState, int chance); + + ISpawnBuilder addBlockWithChance(String blockName, int blockMetadata, int chance); + + ISpawnBuilder addBlockWithChance(ResourceLocation blockResourceLocation, int chance); + + ISpawnBuilder addBlockWithChance(ResourceLocation blockResourceLocation, String blockState, + int chance); + + ISpawnBuilder addBlockWithChance(ResourceLocation blockResourceLocation, int blockMetadata, + int chance); + + ISpawnBuilder addBlockWithChance(Block block, int chance); + + ISpawnBuilder addBlockWithChance(IBlockState block, int chance); + + ISpawnEntry create(); + + ISpawnBuilder addBlock(IBlockDefinition block); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/ISpawnEntry.java b/src/main/java/com/mcmoddev/orespawn/api/os3/ISpawnEntry.java new file mode 100644 index 00000000..dd6fde9c --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/ISpawnEntry.java @@ -0,0 +1,56 @@ +package com.mcmoddev.orespawn.api.os3; + +import java.util.Random; +import com.mcmoddev.orespawn.api.BiomeLocation; +import com.mcmoddev.orespawn.api.IBlockList; +import com.mcmoddev.orespawn.api.IDimensionList; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.IChunkGenerator; + + +import com.mcmoddev.orespawn.api.BiomeLocation; +import com.mcmoddev.orespawn.api.IBlockList; +import com.mcmoddev.orespawn.api.IDimensionList; + +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.IChunkGenerator; + +public interface ISpawnEntry { + + default boolean isEnabled() { + return false; + } + + default boolean isRetrogen() { + return false; + } + + String getSpawnName(); + + boolean dimensionAllowed(int dimension); + + boolean biomeAllowed(ResourceLocation biomeName); + + boolean biomeAllowed(Biome biome); + + IFeatureEntry getFeature(); + + OreSpawnBlockMatcher getMatcher(); + + IBlockList getBlocks(); + + void generate(Random random, World world, IChunkGenerator chunkGenerator, + IChunkProvider chunkProvider, ChunkPos pos); + + IDimensionList getDimensions(); + + BiomeLocation getBiomes(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/OS3API.java b/src/main/java/com/mcmoddev/orespawn/api/os3/OS3API.java index 17e6c7eb..dfabe566 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/os3/OS3API.java +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/OS3API.java @@ -1,33 +1,59 @@ package com.mcmoddev.orespawn.api.os3; -import javax.annotation.Nonnull; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; import com.google.common.collect.ImmutableMap; import com.mcmoddev.orespawn.api.IFeature; +import com.mcmoddev.orespawn.data.PresetsStorage; import com.mcmoddev.orespawn.util.OS3V2PresetStorage; import com.mcmoddev.orespawn.worldgen.OreSpawnWorldGen; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; +import net.minecraft.util.ResourceLocation; +/** Binary-compatible union of OS3 3.2.2 and 3.3.1 API descriptors. */ public interface OS3API { int dimensionWildcard(); int biomeWildcard(); - - // register replacement blocks - void registerReplacementBlock(@Nonnull String name, @Nonnull Block itemBlock); - void registerReplacementBlock(@Nonnull String name, @Nonnull IBlockState itemBlock); - - //register feature generators - void registerFeatureGenerator(@Nonnull String name, @Nonnull IFeature feature); - void registerFeatureGenerator(@Nonnull String name, @Nonnull Class feature); - void registerFeatureGenerator(@Nonnull String name, @Nonnull String className); - - BuilderLogic getLogic(@Nonnull String name); - void registerLogic(@Nonnull BuilderLogic logic); + void registerReplacementBlock(String name, Block block); + void registerReplacementBlock(String name, IBlockState state); + void registerFeatureGenerator(String name, IFeature feature); + void registerFeatureGenerator(String name, Class feature); + void registerFeatureGenerator(String name, String className); + BuilderLogic getLogic(String name); + void registerLogic(BuilderLogic logic); ImmutableMap getSpawns(); void registerSpawns(); - OreSpawnWorldGen getGenerator(); OS3V2PresetStorage getPresets(); -} \ No newline at end of file + + void addSpawn(ISpawnEntry spawnEntry); + void addFeature(String featureName, IFeature feature); + void addReplacement(IReplacementEntry replacementEntry); + Map getReplacements(); + IReplacementEntry getReplacement(String replacementName); + List getSpawns(int dimensionID); + ISpawnEntry getSpawn(String spawnName); + Map getAllSpawns(); + List getDimensionDefaultReplacements(int dimensionID); + ISpawnBuilder getSpawnBuilder(); + IDimensionBuilder getDimensionBuilder(); + IFeatureBuilder getFeatureBuilder(); + IBlockBuilder getBlockBuilder(); + IBiomeBuilder getBiomeBuilder(); + IReplacementBuilder getReplacementBuilder(); + boolean featureExists(String featureName); + boolean featureExists(ResourceLocation featureName); + IFeature getFeature(String featureName); + IFeature getFeature(ResourceLocation featureName); + PresetsStorage copyPresets(); + void loadConfigFiles(); + boolean hasReplacement(ResourceLocation resourceLocation); + boolean hasReplacement(String name); + void mapEntryToFile(Path path, String entryName); + List getSpawnsForFile(String fileName); + Map> getSpawnsByFile(); +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/OreSpawnBlockMatcher.java b/src/main/java/com/mcmoddev/orespawn/api/os3/OreSpawnBlockMatcher.java new file mode 100644 index 00000000..e4b9bee5 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/OreSpawnBlockMatcher.java @@ -0,0 +1,31 @@ +package com.mcmoddev.orespawn.api.os3; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; + +public class OreSpawnBlockMatcher implements Predicate { + private final List possibles; + public OreSpawnBlockMatcher(IBlockState... matches) { possibles = Arrays.asList(matches); } + public OreSpawnBlockMatcher(List matches) { possibles = new ArrayList<>(matches); } + private boolean has(IBlockState state) { return possibles.contains(state); } + @Override public boolean test(IBlockState state) { return state != null && state.getBlock() != Blocks.AIR && has(state); } + public JsonArray serialize() { + JsonArray result = new JsonArray(); + for (IBlockState state : possibles) { + JsonObject value = new JsonObject(); + value.addProperty("name", state.getBlock().getRegistryName().toString()); + int metadata = state.getBlock().getMetaFromState(state); + if (metadata != 0) value.addProperty("metadata", metadata); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, value); + } + return result; + } +} diff --git a/src/main/java/com/mcmoddev/orespawn/api/os3/package-info.java b/src/main/java/com/mcmoddev/orespawn/api/os3/package-info.java new file mode 100644 index 00000000..6755e2d5 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/api/os3/package-info.java @@ -0,0 +1,3 @@ +/** Deprecated OreSpawn 3 API compatibility surface. */ +@Deprecated +package com.mcmoddev.orespawn.api.os3; diff --git a/src/main/java/com/mcmoddev/orespawn/api/plugin/IOreSpawnPlugin.java b/src/main/java/com/mcmoddev/orespawn/api/plugin/IOreSpawnPlugin.java index 361e1e60..d3b0bf7c 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/plugin/IOreSpawnPlugin.java +++ b/src/main/java/com/mcmoddev/orespawn/api/plugin/IOreSpawnPlugin.java @@ -3,5 +3,6 @@ import com.mcmoddev.orespawn.api.os3.OS3API; public interface IOreSpawnPlugin { + void register(OS3API apiInterface); } diff --git a/src/main/java/com/mcmoddev/orespawn/api/plugin/OreSpawnPlugin.java b/src/main/java/com/mcmoddev/orespawn/api/plugin/OreSpawnPlugin.java index 6aeeb34c..7120c8be 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/plugin/OreSpawnPlugin.java +++ b/src/main/java/com/mcmoddev/orespawn/api/plugin/OreSpawnPlugin.java @@ -9,6 +9,7 @@ @Retention(RUNTIME) @Target(TYPE) public @interface OreSpawnPlugin { + // the Mod this is for - will be used for // generating the name of the json the config // will get saved to and should also be the @@ -19,5 +20,5 @@ // resource location segment to look in // for registered config files -String resourcePath() default "orespawn"; + String resourcePath() default "orespawn"; } diff --git a/src/main/java/com/mcmoddev/orespawn/api/plugin/PluginLoader.java b/src/main/java/com/mcmoddev/orespawn/api/plugin/PluginLoader.java index ec4898fe..c6ab8de3 100644 --- a/src/main/java/com/mcmoddev/orespawn/api/plugin/PluginLoader.java +++ b/src/main/java/com/mcmoddev/orespawn/api/plugin/PluginLoader.java @@ -1,40 +1,19 @@ package com.mcmoddev.orespawn.api.plugin; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.stream.Stream; +import com.mcmoddev.orespawn.compat.LegacyOs3Bridge; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.FileUtils; - -import com.mcmoddev.orespawn.api.plugin.IOreSpawnPlugin; -import com.mcmoddev.orespawn.data.Config; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.OreSpawn; - -import net.minecraft.crash.CrashReport; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.common.discovery.ASMDataTable.ASMData; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +/** + * Deprecated OS3 loader facade. OreSpawn 4 owns discovery and scheduling, so + * the historical two-step entry points safely converge on the same idempotent + * bridge initialization. + */ +@Deprecated public enum PluginLoader { - INSTANCE; - private class PluginData { + public final class PluginData { public final String modId; public final String resourcePath; public final IOreSpawnPlugin plugin; @@ -46,100 +25,15 @@ public PluginData(String modId, String resourcePath, IOreSpawnPlugin plugin) { } } - private static List dataStore = new ArrayList<>(); - - private String getAnnotationItem(String item, final ASMData asmData) { - if (asmData.getAnnotationInfo().get(item) != null) { - return asmData.getAnnotationInfo().get(item).toString(); - } else { - return ""; - } - } - public void load(FMLPreInitializationEvent event) { - for (final ASMData asmDataItem : event.getAsmData().getAll(OreSpawnPlugin.class.getCanonicalName())) { - final String modId = getAnnotationItem("modid", asmDataItem); - final String resourceBase = getAnnotationItem("resourcePath", asmDataItem); - final String clazz = asmDataItem.getClassName(); - IOreSpawnPlugin integration; - - try { - integration = Class.forName(clazz).asSubclass(IOreSpawnPlugin.class).newInstance(); - PluginData pd = new PluginData(modId, resourceBase, integration); - dataStore.add(pd); - } catch (final Exception ex) { - OreSpawn.LOGGER.error("Couldn't load integrations for " + modId, ex); - } - } + LegacyOs3Bridge.initialize(event); } public void register() { - dataStore.forEach(pd -> { scanResources(pd); pd.plugin.register(OreSpawn.API); }); + // Discovery, resource translation and registration are one atomic bridge step. } - public void scanResources(PluginData pd) { - if (Config.getKnownMods().contains(pd.modId)) { - return; - } - - String base = String.format("assets/%s/%s", pd.modId, pd.resourcePath); - URL resURL = getClass().getClassLoader().getResource(base); - - URI uri; - - try { - uri = resURL.toURI(); - } catch (URISyntaxException ex) { - CrashReport report = CrashReport.makeCrashReport(ex, String.format("Failed to get URI for %s", (new ResourceLocation(pd.modId, pd.resourcePath)).toString())); - report.getCategory().addCrashSection(Constants.CRASH_SECTION, Constants.VERSION); - return; - } - - if (uri.getScheme().equals("jar")) { - try (FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap())) { - copyout(fileSystem.getPath(base), pd.modId); - } catch (IOException exc) { - CrashReport report = CrashReport.makeCrashReport(exc, - String.format("Failed in getting FileSystem handler set up for %s", uri.getPath())); - report.getCategory().addCrashSection(Constants.CRASH_SECTION, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - } else { - copyout(Paths.get(uri), pd.modId); - } - - Config.addKnownMod(pd.modId); - } - - private void copyout(Path myPath, String modId) { - try(Stream walk = Files.walk(myPath, 1)) { - for (Iterator it = walk.iterator(); it.hasNext();) { - Path p = it.next(); - String name = p.getFileName().toString(); - - if ("json".equals(FilenameUtils.getExtension(name))) { - InputStream reader = null; - Path target; - - if ("_features".equals(FilenameUtils.getBaseName(name))) { - target = Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF, String.format("features-%s.json", modId)); - } else if ("_replacements".equals(FilenameUtils.getBaseName(name))) { - target = Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF, String.format("replacements-%s.json", modId)); - } else { - target = Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, String.format("%s.json", modId)); - } - - if (!target.toFile().exists()) { - reader = Files.newInputStream(p); - FileUtils.copyInputStreamToFile(reader, target.toFile()); - IOUtils.closeQuietly(reader); - } - } - } - } catch (IOException exc) { - CrashReport report = CrashReport.makeCrashReport(exc, String.format("Faulted while iterating %s for config files or copying them out", myPath)); - report.getCategory().addCrashSection(Constants.CRASH_SECTION, Constants.VERSION); - OreSpawn.LOGGER.error(report.getCompleteReport()); - } + public void scanResources(PluginData data) { + // Resources are scanned by LegacyOs3Bridge before profile baking. } } diff --git a/src/main/java/com/mcmoddev/orespawn/commands/AddOreCommand.java b/src/main/java/com/mcmoddev/orespawn/commands/AddOreCommand.java deleted file mode 100644 index 678ae6f7..00000000 --- a/src/main/java/com/mcmoddev/orespawn/commands/AddOreCommand.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.mcmoddev.orespawn.commands; - -import com.google.gson.*; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.os3.BiomeBuilder; -import com.mcmoddev.orespawn.api.os3.DimensionBuilder; -import com.mcmoddev.orespawn.api.os3.FeatureBuilder; -import com.mcmoddev.orespawn.api.os3.OreBuilder; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.data.ReplacementsRegistry; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.util.StateUtil; -import net.minecraft.block.state.IBlockState; -import net.minecraft.command.CommandBase; -import net.minecraft.command.CommandException; -import net.minecraft.command.ICommand; -import net.minecraft.command.ICommandSender; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemBlock; -import net.minecraft.item.ItemStack; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.EnumHand; -import net.minecraft.util.text.TextComponentString; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map.Entry; - -public class AddOreCommand extends CommandBase { - private static final String ALL = "all"; - - @Override - public String getName() { - return "addore"; - } - - @Override - public String getUsage(ICommandSender sender) { - return "/addore "; - } - - @Override - public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { - if (!(sender instanceof EntityPlayer)) { - throw new CommandException("Only players can use this command"); - } - - EntityPlayer player = (EntityPlayer) sender; - ItemStack stack = player.getHeldItem(EnumHand.MAIN_HAND); - String jsonArgs = null; - - if (stack == null) { - throw new CommandException("You have no item in your main hand"); - } else if (!(stack.getItem() instanceof ItemBlock)) { - throw new CommandException("The item in your main hand isn't a block"); - } else if (args.length < 2) { - throw new CommandException(this.getUsage(sender)); - } else if (args.length > 2) { - jsonArgs = getChatComponentFromNthArg(sender, args, 2).getUnformattedText(); - } - - String file = args[0]; - @SuppressWarnings("deprecation") - IBlockState state = ((ItemBlock) stack.getItem()).getBlock().getStateFromMeta(stack.getItemDamage()); - - int dimension = OreSpawn.API.dimensionWildcard(); - - try { - if (!args[1].equalsIgnoreCase(ALL)) { - dimension = Integer.parseInt(args[1]); - } - } catch (NumberFormatException e) { - throw new CommandException(args[1] + " isn't a valid dimension"); - } - - - JsonObject ore = new JsonObject(); - JsonObject oreArgs = null; - int size = 25; - int variation = 12; - int frequency = 20; - int minHeight = 0; - int maxHeight = 128; - - oreArgs = new JsonObject(); - oreArgs.addProperty(ConfigNames.DefaultFeatureProperties.SIZE, size); - oreArgs.addProperty(ConfigNames.DefaultFeatureProperties.VARIATION, variation); - oreArgs.addProperty(ConfigNames.DefaultFeatureProperties.FREQUENCY, frequency); - oreArgs.addProperty(ConfigNames.DefaultFeatureProperties.MINHEIGHT, minHeight); - oreArgs.addProperty(ConfigNames.DefaultFeatureProperties.MAXHEIGHT, maxHeight); - ore.addProperty(ConfigNames.BLOCK, state.getBlock().getRegistryName().toString()); - ore.addProperty(ConfigNames.STATE, StateUtil.serializeState(state)); - - if (jsonArgs != null) { - JsonObject newOreArgs = (new JsonParser()).parse(jsonArgs).getAsJsonObject(); - setProperties(oreArgs, newOreArgs); - } - - setOre(ore, oreArgs); - - this.putFile(file, ore, dimension); - - player.sendStatusMessage(new TextComponentString("Added " + state.getBlock().getRegistryName().toString() + " to the json"), true); - } - - private void setProperties(JsonObject oreArgs, JsonObject newOreArgs) { - for (Entry ent : newOreArgs.entrySet()) { - oreArgs.remove(ent.getKey()); - oreArgs.add(ent.getKey(), ent.getValue()); - } - } - - private void setOre(JsonObject ore, JsonObject oreArgs) { - for (Entry ent : oreArgs.entrySet()) { - ore.add(ent.getKey(), ent.getValue()); - } - } - - private void putFile(String file, JsonObject ore, int id) { - DimensionBuilder db = OreSpawn.API.getLogic(file).newDimensionBuilder(id); - SpawnBuilder sb = db.newSpawnBuilder(null); - OreBuilder ob = sb.newOreBuilder(); - String b = ore.get(ConfigNames.BLOCK).getAsString(); - ore.remove(ConfigNames.BLOCK); - String s = ore.get(ConfigNames.STATE).getAsString(); - ore.remove(ConfigNames.STATE); - - if (ConfigNames.STATE_NORMAL.equals(s)) { - ob.setOre(b); - } else { - ob.setOre(b, s); - } - - FeatureBuilder fb = sb.newFeatureBuilder(ConfigNames.DEFAULT); - fb.setGenerator(ConfigNames.DEFAULT).setDefaultParameters().setParameters(ore); - BiomeBuilder bb = sb.newBiomeBuilder(); - IBlockState rep = ReplacementsRegistry.getDimensionDefault(id).get(0); - List rl = new ArrayList<>(); - rl.add(rep); - sb.create(bb, fb, rl, ob); - db.create(sb); - OreSpawn.API.getLogic(file).create(db); - OreSpawn.API.registerLogic(OreSpawn.API.getLogic(file)); - OreSpawn.writer.writeAddOreEntry(file); - } - - @Override - public int compareTo(ICommand command) { - return this.getName().compareTo(command.getName()); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/commands/ClearChunkCommand.java b/src/main/java/com/mcmoddev/orespawn/commands/ClearChunkCommand.java deleted file mode 100644 index 95fa7b31..00000000 --- a/src/main/java/com/mcmoddev/orespawn/commands/ClearChunkCommand.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.mcmoddev.orespawn.commands; - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.stream.Collectors; - -import com.mcmoddev.orespawn.worldgen.OreSpawnWorldGen; -import net.minecraft.block.Block; -import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; -import net.minecraft.command.CommandBase; -import net.minecraft.command.CommandException; -import net.minecraft.command.ICommand; -import net.minecraft.command.ICommandSender; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.world.chunk.Chunk; -import net.minecraftforge.fml.common.registry.ForgeRegistries; - -public class ClearChunkCommand extends CommandBase { - private static final String STONE_ID = "minecraft:stone"; - private static final List stoneVariants = Arrays.asList(STONE_ID, "minecraft:diorite", "minecraft:andesite", "minecraft:granite", "minecraft:sandstone", "minecraft:red_sandstone", "minecraft:netherrack", "minecraft:end_stone"); - private static final List baseStones = Arrays.asList(STONE_ID, "minecraft:netherrack", "minecraft:end_stone", "minecraft:cobblestone", "minecraft:obsidian", "minecraft:magma", "minecraft:soul_sand"); - - private static final List dirtVariants = Arrays.asList("minecraft:dirt", "minecraft:grass"); - private static final List otherVariants = Arrays.asList("minecraft:gravel", "minecraft:sand"); - - @Override - public String getName() { - return "clearchunk"; - } - - @Override - public String getUsage(ICommandSender sender) { - return "/clearchunk "; - } - - @Override - public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { - if (!(sender instanceof EntityPlayer)) { - throw new CommandException("Only players can use this command"); - } - - EntityPlayer player = (EntityPlayer) sender; - Chunk chunk = player.getEntityWorld().getChunkFromBlockCoords(player.getPosition()); - ChunkPos chunkPos = chunk.getPos(); - List blocks; - - boolean flagClassic = args.length > 0 ? args[0].toLowerCase().equalsIgnoreCase("classic") : false; - - List blockNames = new LinkedList<>(); - getBlocks(args, blockNames); - - blocks = blockNames.stream() - .map(blockName -> ForgeRegistries.BLOCKS.getValue(new ResourceLocation(blockName))).collect(Collectors.toList()); - - List overburden = Arrays.asList("minecraft:dirt", "minecraft:sand", "minecraft:gravel", "minecraft:grass", "minecraft:sandstone", "minecraft:red_sandstone").stream() - .map(blockName -> ForgeRegistries.BLOCKS.getValue(new ResourceLocation(blockName))).collect(Collectors.toList()); - - clearBlocks(chunkPos, blocks, overburden, flagClassic, player); - - player.sendStatusMessage(new TextComponentString("chunk " + chunkPos.toString() + " cleared"), true); - } - - private void clearBlocks(ChunkPos chunkPos, List blocks, List overburden, boolean flagClassic, EntityPlayer player) { - for (int x = chunkPos.getXStart(); x <= chunkPos.getXEnd(); x++) { - for (int y = 256; y >= 0; y--) { - for (int z = chunkPos.getZStart(); z <= chunkPos.getZEnd(); z++) { - BlockPos pos = new BlockPos(x, y, z); - Block block = player.getEntityWorld().getBlockState(pos).getBlock(); - removeIfBlocks(player, pos, block, blocks, overburden, !flagClassic); - removeIfFluid(pos, player); - } - } - } - } - - private void removeIfFluid(BlockPos pos, EntityPlayer player) { - if (player.getEntityWorld().getBlockState(pos).getMaterial().isLiquid()) { - IBlockState bs = player.getEntityWorld().getBlockState(pos); - - if (bs.getMaterial().equals(Material.LAVA) || bs.getMaterial().equals(Material.WATER)) { - player.getEntityWorld().setBlockToAir(pos); - } - } - } - - private void removeIfBlocks(EntityPlayer player, BlockPos pos, Block block, List blocks, List overburden, boolean flagClassic) { - if (blocks.contains(block) || ((pos.getY() >= 64 && overburden.contains(block)) && flagClassic)) { - player.getEntityWorld().setBlockToAir(pos); - } - } - - private void getBlocks(String[] args, List blockNames) { - if (args.length > 0) { - switch (args[0].toLowerCase()) { - case "viewores": - blockNames.addAll(stoneVariants); - blockNames.addAll(dirtVariants); - blockNames.addAll(otherVariants); - break; - - case "dirtandgravel": - blockNames.add(STONE_ID); - blockNames.addAll(dirtVariants); - blockNames.addAll(otherVariants); - break; - - case "classic": - blockNames.addAll(OreSpawnWorldGen.getSpawnBlocks().stream().map(block -> block.getRegistryName().toString()).collect(Collectors.toList())); - break; - - default: - blockNames.addAll(baseStones); - } - } else { - blockNames.addAll(baseStones); - } - } - - @Override - public int compareTo(ICommand command) { - return this.getName().compareTo(command.getName()); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/commands/DumpBiomesCommand.java b/src/main/java/com/mcmoddev/orespawn/commands/DumpBiomesCommand.java deleted file mode 100644 index 779b089b..00000000 --- a/src/main/java/com/mcmoddev/orespawn/commands/DumpBiomesCommand.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.mcmoddev.orespawn.commands; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonPrimitive; -import net.minecraft.command.CommandBase; -import net.minecraft.command.CommandException; -import net.minecraft.command.ICommand; -import net.minecraft.command.ICommandSender; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.world.biome.Biome; -import net.minecraftforge.fml.common.registry.ForgeRegistries; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringEscapeUtils; -import org.apache.commons.codec.CharEncoding; - -import java.io.File; -import java.io.IOException; - -public class DumpBiomesCommand extends CommandBase { - @Override - public String getName() { - return "dumpbiomes"; - } - - @Override - public String getUsage(ICommandSender sender) { - return "/dumpbiomes"; - } - - @Override - public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { - JsonArray array = new JsonArray(); - - for (Biome biome : ForgeRegistries.BIOMES) { - array.add(new JsonPrimitive(biome.getRegistryName().toString())); - } - - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - String json = gson.toJson(array); - - try { - FileUtils.writeStringToFile(new File(".", "biome_dump.json"), StringEscapeUtils.unescapeJson(json), CharEncoding.UTF_8); - } catch (IOException e) { - throw new CommandException("Failed to save the json file"); - } - - sender.sendMessage(new TextComponentString("Done")); - } - - @Override - public int compareTo(ICommand command) { - return this.getName().compareTo(command.getName()); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/commands/WriteConfigsCommand.java b/src/main/java/com/mcmoddev/orespawn/commands/WriteConfigsCommand.java deleted file mode 100644 index 3576f02d..00000000 --- a/src/main/java/com/mcmoddev/orespawn/commands/WriteConfigsCommand.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.mcmoddev.orespawn.commands; - -import com.mcmoddev.orespawn.OreSpawn; - -import net.minecraft.command.CommandBase; -import net.minecraft.command.CommandException; -import net.minecraft.command.ICommandSender; -import net.minecraft.server.MinecraftServer; - -public class WriteConfigsCommand extends CommandBase { - - @Override - public String getName() { - return "osSaveConfigs"; - } - - @Override - public String getUsage(ICommandSender sender) { - return "/osSaveConfigs"; - } - - @Override - public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { - OreSpawn.writer.writeSpawnEntries(); - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java new file mode 100644 index 00000000..b483f405 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -0,0 +1,1839 @@ +package com.mcmoddev.orespawn.compat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mcmoddev.orespawn.api.BiomeLocation; +import com.mcmoddev.orespawn.api.GeneratorParameters; +import com.mcmoddev.orespawn.api.IBlockList; +import com.mcmoddev.orespawn.api.IDimensionList; +import com.mcmoddev.orespawn.api.IFeature; +import com.mcmoddev.orespawn.api.os3.BiomeBuilder; +import com.mcmoddev.orespawn.api.os3.BuilderLogic; +import com.mcmoddev.orespawn.api.os3.DimensionBuilder; +import com.mcmoddev.orespawn.api.os3.FeatureBuilder; +import com.mcmoddev.orespawn.api.os3.IBiomeBuilder; +import com.mcmoddev.orespawn.api.os3.IBlockBuilder; +import com.mcmoddev.orespawn.api.os3.IBlockDefinition; +import com.mcmoddev.orespawn.api.os3.IDimensionBuilder; +import com.mcmoddev.orespawn.api.os3.IFeatureBuilder; +import com.mcmoddev.orespawn.api.os3.IFeatureEntry; +import com.mcmoddev.orespawn.api.os3.IReplacementBuilder; +import com.mcmoddev.orespawn.api.os3.IReplacementEntry; +import com.mcmoddev.orespawn.api.os3.ISpawnBuilder; +import com.mcmoddev.orespawn.api.os3.ISpawnEntry; +import com.mcmoddev.orespawn.api.os3.OS3API; +import com.mcmoddev.orespawn.api.os3.OreBuilder; +import com.mcmoddev.orespawn.api.os3.OreSpawnBlockMatcher; +import com.mcmoddev.orespawn.api.os3.SpawnBuilder; +import com.mcmoddev.orespawn.api.plugin.IOreSpawnPlugin; +import com.mcmoddev.orespawn.api.plugin.OreSpawnPlugin; +import com.mcmoddev.orespawn.data.FeatureRegistry; +import com.mcmoddev.orespawn.data.PresetsStorage; +import com.mcmoddev.orespawn.util.OS3V2PresetStorage; +import com.mcmoddev.orespawn.util.OreList; +import com.mcmoddev.orespawn.worldgen.OreSpawnWorldGen; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.IChunkGenerator; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.discovery.ASMDataTable; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import net.minecraftforge.oredict.OreDictionary; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; +import net.minecraftforge.fml.common.registry.IForgeRegistry; +import net.minecraftforge.fml.common.registry.RegistryBuilder; +import zone.moddev.mc.orespawn.worldgen.LegacyOs3ProfileMigration; + +/** + * Deprecated OS3 binary/config bridge. It translates declarative entries to + * provider-schema 4 and owns the only scheduler for legacy custom generators. + */ +public final class LegacyOs3Bridge { + private static final Logger LOGGER = LogManager.getLogger("OreSpawn-OS3-Bridge"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + private static final IForgeRegistry SAVED_FEATURES = + new RegistryBuilder() + .setName(new ResourceLocation("orespawn", "feature_registry")) + .setType(IFeature.class).setIDRange(0, 4096).create(); + private static final IForgeRegistry SAVED_REPLACEMENTS = + new RegistryBuilder() + .setName(new ResourceLocation("orespawn", "replacements_registry")) + .setType(IReplacementEntry.class).setIDRange(0, 65535).create(); + private static final LegacyApi API = new LegacyApi(); + private static final FeatureRegistry FEATURES = new FeatureRegistry(); + private static final List REPORT = new ArrayList<>(); + private static boolean initialized; + + static { + for (String name : new String[] { "default", "vein", "normal-cloud", "precision", + "clusters", "underfluids" }) { + registerSavedFeature(name, new SavedFeature()); + } + } + + private LegacyOs3Bridge() { } + + public static OS3API api() { return API; } + public static FeatureRegistry features() { return FEATURES; } + + private static void registerSavedFeature(String name, IFeature feature) { + ResourceLocation id = validId(name) ? new ResourceLocation(name) + : new ResourceLocation("orespawn", safe(name)); + if (!SAVED_FEATURES.containsKey(id)) { + IFeature registryValue = feature.setRegistryName(id); + if (registryValue.getRegistryName() == null) { + registryValue = new SavedFeatureAlias(feature).setRegistryName(id); + } + SAVED_FEATURES.register(registryValue); + } + if (!FEATURES.hasFeature(id.toString())) FEATURES.addFeature(id.toString(), feature); + if (!name.contains(":") && !FEATURES.hasFeature(name)) FEATURES.addFeature(name, feature); + } + + private static void registerSavedReplacement(IReplacementEntry replacement) { + if (replacement != null && replacement.getRegistryName() != null + && !SAVED_REPLACEMENTS.containsKey(replacement.getRegistryName())) { + SAVED_REPLACEMENTS.register(replacement); + } + } + + static JsonObject translateForTests(String modId, JsonObject source, Path legacyDirectory, + Path legacyConfig) throws IOException { + return translate(modId, modId, source, legacyDirectory, LegacyFlags.read(legacyConfig)); + } + + static JsonObject translateStandaloneForTests(String sourceId, JsonObject source, Path legacyDirectory, + Path legacyConfig) throws IOException { + return translate("orespawn", sourceId, source, legacyDirectory, LegacyFlags.read(legacyConfig)); + } + + static void resetProgrammaticForTests(String owner) { + API.resetProgrammatic(owner); + REPORT.clear(); + } + + static Map programmaticSourcesForTests() { + return API.programmaticSources(); + } + + static List reportForTests() { + return Collections.unmodifiableList(new ArrayList<>(REPORT)); + } + + static int[] translatedProgrammaticCountsForTests() { + return new int[] { API.translatedSpawns.size(), API.translated322Spawns.size() }; + } + + public static synchronized void initialize(FMLPreInitializationEvent event) { + if (initialized) return; + initialized = true; + registerSavedReplacement(new LegacyReplacementEntry("orespawn:default", + Arrays.asList(Blocks.STONE.getDefaultState(), Blocks.NETHERRACK.getDefaultState(), + Blocks.END_STONE.getDefaultState()))); + Path config = event.getModConfigurationDirectory().toPath(); + scanPlugins(event.getAsmData()); + migrateConfigDirectory(config); + } + + public static void generate(Random random, int chunkX, int chunkZ, World world, + IChunkGenerator generator, IChunkProvider provider) { + API.generate(random, new ChunkPos(chunkX, chunkZ), world, generator, provider); + } + + private static void scanPlugins(ASMDataTable table) { + for (ASMDataTable.ASMData data : table.getAll(OreSpawnPlugin.class.getName())) { + try { + Object value = Class.forName(data.getClassName()).newInstance(); + if (!(value instanceof IOreSpawnPlugin)) { + REPORT.add("plugin_rejected=" + data.getClassName() + ":not_IOreSpawnPlugin"); + continue; + } + @SuppressWarnings("unchecked") Map info = data.getAnnotationInfo(); + String modId = String.valueOf(info.get("modid")); + Object configuredPath = info.get("resourcePath"); + String resourcePath = configuredPath == null ? "orespawn" : String.valueOf(configuredPath); + if (resourcePath.trim().isEmpty()) resourcePath = "orespawn"; + API.activeModId = modId; + ((IOreSpawnPlugin) value).register(API); + API.activeModId = "legacy"; + scanEmbeddedResources(modId, resourcePath); + REPORT.add("plugin_loaded=" + data.getClassName()); + } catch (ReflectiveOperationException | RuntimeException failure) { + REPORT.add("plugin_failed=" + data.getClassName() + ":" + failure.getClass().getSimpleName()); + LOGGER.error("Could not load legacy OreSpawn plugin {}", data.getClassName(), failure); + } + } + } + + private static void scanEmbeddedResources(String modId, String resourcePath) { + String prefix = "assets/" + modId + "/" + resourcePath + "/"; + boolean found = false; + try { + net.minecraftforge.fml.common.ModContainer container = Loader.instance().getIndexedModList().get(modId); + java.io.File source = container == null ? null : container.getSource(); + if (source != null && source.isFile()) { + try (JarFile jar = new JarFile(source)) { + List names = new ArrayList<>(); Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { String name = entries.nextElement().getName(); if (name.startsWith(prefix) && name.endsWith(".json")) names.add(name); } + Collections.sort(names); + for (String name : names) { try (InputStream input = jar.getInputStream(jar.getJarEntry(name))) { consumeEmbedded(modId, name, readElement(input)); found = true; } } + } + } else if (source != null && source.isDirectory()) { + Path directory = source.toPath().resolve(prefix.replace('/', java.io.File.separatorChar)); + if (Files.isDirectory(directory)) { + List files = new ArrayList<>(); try (java.util.stream.Stream walk = Files.walk(directory, 1)) { walk.filter(path -> path.toString().endsWith(".json")).forEach(files::add); } + files.sort((left, right) -> left.getFileName().toString().compareTo(right.getFileName().toString())); + for (Path file : files) { try (InputStream input = Files.newInputStream(file)) { consumeEmbedded(modId, prefix + file.getFileName(), readElement(input)); found = true; } } + } + } + } catch (IOException | RuntimeException failure) { + REPORT.add("resource_scan_failed=" + prefix + ":" + failure.getClass().getSimpleName()); + LOGGER.error("Could not scan legacy OreSpawn resources under {}", prefix, failure); + } + if (!found) { + String path = prefix + modId + ".json"; + try (InputStream input = LegacyOs3Bridge.class.getClassLoader().getResourceAsStream(path)) { + if (input != null) { consumeEmbedded(modId, path, readElement(input)); found = true; } + } catch (IOException | RuntimeException failure) { + REPORT.add("resource_failed=" + path + ":" + failure.getClass().getSimpleName()); + } + } + if (!found) REPORT.add("resource_missing=" + prefix); + } + + private static void consumeEmbedded(String modId, String path, JsonElement root) { + String file = path.substring(path.lastIndexOf('/') + 1); + if (file.startsWith("_features") && root.isJsonArray()) { + for (JsonElement value : root.getAsJsonArray()) { + if (!value.isJsonObject()) continue; JsonObject feature = value.getAsJsonObject(); + String name = text(feature, "name", ""), className = text(feature, "class", ""); + if (name.isEmpty() || className.isEmpty() || FEATURES.hasFeature(name)) continue; + try { API.registerFeatureGenerator(name, className); } + catch (RuntimeException failure) { REPORT.add("embedded_feature_failed=" + name + ":" + failure.getClass().getSimpleName()); } + } + } else if (file.startsWith("_replacements")) { + mergeReplacementElement(API.embeddedReplacements, root); + } else if (root.isJsonObject()) { + JsonObject normalized = normalizeLegacyDocument(root.getAsJsonObject(), path); + JsonObject combined = API.embedded.computeIfAbsent(modId, ignored -> new JsonObject()); + JsonObject combinedSpawns = object(combined, "spawns"); + for (Map.Entry spawn : object(normalized, "spawns").entrySet()) { + combinedSpawns.add(spawn.getKey(), new JsonParser().parse(spawn.getValue().toString())); + } + combined.addProperty("version", text(normalized, "version", "2.0")); combined.add("spawns", combinedSpawns); + } + REPORT.add("resource_loaded=" + path); + } + + private static void migrateConfigDirectory(Path configDirectory) { + Path legacyDirectory = configDirectory.resolve("orespawn3"); + Path bridgeMarker = configDirectory.resolve(".orespawn-os3-bridge-migrated"); + if (!hasLegacyConfig(legacyDirectory) && API.embedded.isEmpty() && !API.hasProgrammaticRegistrations()) { + return; + } + LegacyFlags flags = LegacyFlags.read(configDirectory.resolve("orespawn.cfg")); + try { + REPORT.add("profile_migration=" + LegacyOs3ProfileMigration.apply(configDirectory, + flags.replaceVanilla, flags.disableStandard, flags.retrogen, + flags.forceRetrogen, flags.flatBedrock, flags.retrogenBedrock, + flags.bedrockLayers).name().toLowerCase(java.util.Locale.ROOT)); + } catch (IOException failure) { + REPORT.add("profile_migration_failed=" + failure.getClass().getSimpleName()); + LOGGER.error("Could not migrate OS3 global world-generation settings", failure); + } + Map programmaticSources = API.programmaticSources(); + if (Files.isRegularFile(bridgeMarker)) { + LOGGER.info("OS3 provider migration already completed; retaining migrated files unchanged"); + return; + } + Map sources = new LinkedHashMap<>(); + for (Map.Entry source : programmaticSources.entrySet()) { + mergeLegacySource(sources, source.getKey(), source.getValue()); + } + for (Map.Entry source : API.embedded.entrySet()) { + mergeLegacySource(sources, source.getKey(), source.getValue()); + } + if (Files.isDirectory(legacyDirectory)) { + try (DirectoryStream files = Files.newDirectoryStream(legacyDirectory, "*.json")) { + List sorted = new ArrayList<>(); for (Path file : files) sorted.add(file); + sorted.sort((left, right) -> left.getFileName().toString().compareTo(right.getFileName().toString())); + for (Path file : sorted) { + String modId = file.getFileName().toString().replaceFirst("\\.json$", ""); + try (InputStream input = Files.newInputStream(file)) { + mergeLegacySource(sources, modId, readObject(input)); + REPORT.add("config_source=" + file.toAbsolutePath()); + } catch (IOException | RuntimeException failure) { + REPORT.add("config_rejected=" + file.getFileName() + ":" + failure.getClass().getSimpleName()); + } + } + } catch (IOException failure) { + REPORT.add("config_scan_failed=" + failure.getClass().getSimpleName()); + } + } + + Map providers = new LinkedHashMap<>(); + for (Map.Entry source : sources.entrySet()) { + if (!validModId(source.getKey())) { + REPORT.add("config_rejected=" + source.getKey() + ":invalid_owner"); + continue; + } + try { + boolean owned = isModLoaded(source.getKey()); + String providerModId = owned ? source.getKey() : "orespawn"; + JsonObject provider = translate(providerModId, source.getKey(), source.getValue(), legacyDirectory, flags); + if (!owned) REPORT.add("standalone_config_mapped=" + source.getKey() + ":provider=orespawn"); + mergeProvider(providers, providerModId, provider, source.getKey()); + } catch (RuntimeException | IOException failure) { + REPORT.add("translation_failed=" + source.getKey() + ":" + failure.getClass().getSimpleName() + + ":" + String.valueOf(failure.getMessage())); + LOGGER.error("Could not translate OS3 provider {}", source.getKey(), failure); + } + } + for (Map.Entry provider : providers.entrySet()) { + try { + writeAtomicIfChanged(configDirectory.resolve(provider.getKey() + "-orespawn.json"), provider.getValue()); + } catch (IOException failure) { + REPORT.add("provider_write_failed=" + provider.getKey() + ":" + failure.getClass().getSimpleName()); + LOGGER.error("Could not write migrated OS3 provider {}", provider.getKey(), failure); + writeReport(configDirectory.resolve("orespawn-os3-migration-report.json")); + return; + } + } + writeReport(configDirectory.resolve("orespawn-os3-migration-report.json")); + try { + writeMarker(bridgeMarker); + } catch (IOException failure) { + REPORT.add("migration_marker_failed=" + failure.getClass().getSimpleName()); + LOGGER.error("Could not mark OS3 provider migration complete", failure); + } + } + + private static void mergeLegacySource(Map sources, String owner, JsonObject incoming) { + incoming = normalizeLegacyDocument(incoming, owner); + JsonObject target = sources.computeIfAbsent(owner, ignored -> new JsonObject()); + target.addProperty("version", text(incoming, "version", "2.0")); + JsonObject targetSpawns = object(target, "spawns"); + for (Map.Entry spawn : object(incoming, "spawns").entrySet()) { + targetSpawns.add(spawn.getKey(), new JsonParser().parse(spawn.getValue().toString())); + } + target.add("spawns", targetSpawns); + } + + /** + * Mirrors the OS3 3.2.2 version-one reader for provider resources shipped by + * contemporary 1.11 mods. The bridge's internal representation remains the + * version-two {@code spawns} shape used by every later translation step. + */ + private static JsonObject normalizeLegacyDocument(JsonObject source, String sourceId) { + if (source.has("spawns") || !source.has("dimensions") || !source.get("dimensions").isJsonArray()) { + return source; + } + String version = text(source, "version", ""); + if (!("1".equals(version) || "1.1".equals(version) || "1.2".equals(version))) return source; + + JsonObject normalized = new JsonObject(); + normalized.addProperty("version", "2.0"); + JsonObject spawns = new JsonObject(); + int ordinal = 0; + for (JsonElement dimensionElement : source.getAsJsonArray("dimensions")) { + if (!dimensionElement.isJsonObject()) continue; + JsonObject dimension = dimensionElement.getAsJsonObject(); + JsonArray dimensions = new JsonArray(); + if (dimension.has("dimension")) zone.moddev.mc.orespawn.util.JsonCopies.add(dimensions, + dimension.get("dimension").getAsInt()); + for (JsonElement oreElement : array(dimension, "ores")) { + if (!oreElement.isJsonObject()) continue; + JsonObject legacy = oreElement.getAsJsonObject(); + JsonObject spawn = new JsonObject(); + spawn.addProperty("enabled", true); + spawn.addProperty("retrogen", true); + spawn.addProperty("feature", text(legacy, "feature", "default")); + spawn.addProperty("replaces", text(legacy, "replace_block", + text(legacy, "replaces", "default"))); + spawn.add("dimensions", new JsonParser().parse(dimensions.toString())); + spawn.add("parameters", new JsonParser().parse(object(legacy, "parameters").toString())); + if (legacy.has("biomes")) spawn.add("biomes", new JsonParser().parse(legacy.get("biomes").toString())); + + JsonArray blocks = new JsonArray(); + if ("1.2".equals(version) && legacy.has("blocks") && legacy.get("blocks").isJsonArray()) { + blocks = new JsonParser().parse(legacy.get("blocks").toString()).getAsJsonArray(); + } else { + String block = text(legacy, "block", text(legacy, "blockID", "")); + if (!block.isEmpty()) { + JsonObject output = new JsonObject(); output.addProperty("name", block); + if (legacy.has("metadata")) output.add("metadata", legacy.get("metadata")); + if (legacy.has("state")) output.add("state", legacy.get("state")); + output.addProperty("chance", legacy.has("chance") ? legacy.get("chance").getAsInt() : 100); + zone.moddev.mc.orespawn.util.JsonCopies.add(blocks, output); + } + } + spawn.add("blocks", blocks); + String blockName = blocks.size() == 0 ? "" : text(blocks.get(0).getAsJsonObject(), "name", ""); + String base = validId(blockName) ? safe(new ResourceLocation(blockName).getResourcePath()) : "ore"; + String name = base; + while (spawns.has(name)) name = base + "_" + (++ordinal); + spawns.add(name, spawn); + ordinal++; + } + } + normalized.add("spawns", spawns); + REPORT.add("legacy_v1_normalized=" + sourceId + ":spawns=" + + zone.moddev.mc.orespawn.util.JsonCopies.size(spawns)); + return normalized; + } + + private static boolean hasLegacyConfig(Path directory) { + if (!Files.isDirectory(directory)) return false; + try (DirectoryStream files = Files.newDirectoryStream(directory, "*.json")) { + return files.iterator().hasNext(); + } catch (IOException failure) { + REPORT.add("config_presence_failed=" + failure.getClass().getSimpleName()); + return false; + } + } + + private static void mergeProvider(Map providers, String providerModId, + JsonObject incoming, String sourceId) { + JsonObject target = providers.get(providerModId); + if (target == null) { + providers.put(providerModId, incoming); + return; + } + JsonObject targetOres = object(target, "ores"); + for (Map.Entry ore : object(incoming, "ores").entrySet()) { + if (targetOres.has(ore.getKey())) { + throw new IllegalArgumentException("duplicate synthesized ore id " + ore.getKey() + + " from " + sourceId); + } + targetOres.add(ore.getKey(), new JsonParser().parse(ore.getValue().toString())); + } + target.add("ores", targetOres); + } + + private static JsonObject translate(String providerModId, String sourceId, JsonObject source, Path legacyDirectory, + LegacyFlags flags) throws IOException { + source = normalizeLegacyDocument(source, sourceId); + JsonObject provider = new JsonObject(); + provider.addProperty("schema_version", 4); + provider.addProperty("provider_modid", providerModId); + provider.addProperty("provider_revision", 1); + provider.add("rocks", new JsonObject()); + JsonObject ores = new JsonObject(); provider.add("ores", ores); + provider.add("fluid_deposits", new JsonObject()); provider.add("geomes", new JsonObject()); + provider.add("biome_rules", new JsonObject()); provider.add("terrain_dimensions", new JsonObject()); + provider.add("biome_palettes", new JsonObject()); provider.add("dimension_materials", new JsonObject()); + provider.add("templates", new JsonObject()); + JsonObject replacements = readReplacements(legacyDirectory); + JsonObject spawns = object(source, "spawns"); + for (Map.Entry entry : spawns.entrySet()) { + if (!entry.getValue().isJsonObject()) { REPORT.add("spawn_ignored=" + sourceId + ":" + entry.getKey() + ":not_object"); continue; } + JsonObject migrated = translateSpawn(sourceId, entry.getKey(), entry.getValue().getAsJsonObject(), replacements, + flags); + if (migrated != null) { + String path = "legacy/" + (providerModId.equals(sourceId) ? "" : safe(sourceId) + "/") + + safe(entry.getKey()); + ores.add(new ResourceLocation(providerModId, path).toString(), migrated); + } + } + REPORT.add("provider_translated=" + sourceId + ":owner=" + providerModId + ":ores=" + zone.moddev.mc.orespawn.util.JsonCopies.size(ores)); + return provider; + } + + private static JsonObject translateSpawn(String modId, String name, JsonObject spawn, + JsonObject replacements, LegacyFlags flags) { + JsonArray blocks = array(spawn, "blocks"); + if (blocks.size() == 0) { REPORT.add("spawn_ignored=" + modId + ":" + name + ":no_blocks"); return null; } + JsonObject first = blocks.get(0).getAsJsonObject(); + String output = text(first, "name", ""); + if (!validId(output)) { REPORT.add("spawn_ignored=" + modId + ":" + name + ":invalid_output"); return null; } + JsonObject ore = new JsonObject(); ore.addProperty("enabled", bool(spawn, "enabled", true)); + ore.addProperty("block", output); ore.addProperty("source_mod", modId); + ore.addProperty("native_generation", false); + ore.addProperty("retrogen", bool(spawn, "retrogen", false)); + copyMetadata(first, ore); + if (flags.replaceVanilla && "minecraft".equals(new ResourceLocation(output).getResourceDomain())) { + ore.addProperty("suppress_vanilla", true); + } + JsonArray outputs = new JsonArray(); + for (JsonElement element : blocks) { + if (!element.isJsonObject()) continue; JsonObject old = element.getAsJsonObject(); + String block = text(old, "name", ""); if (!validId(block)) continue; + JsonObject value = new JsonObject(); value.addProperty("block", block); + value.addProperty("weight", Math.max(1, integer(old, "chance", 100))); + copyMetadata(old, value); zone.moddev.mc.orespawn.util.JsonCopies.add(outputs, value); + } + ore.add("outputs", outputs); + JsonObject dimensions = new JsonObject(); JsonObject selectors = new JsonObject(); + DimensionSelection selection = dimensions(spawn.get("dimensions")); + if (selection.defaultOverworld) { + JsonObject placement = placement(modId, name, spawn, replacements, flags); + if (placement == null) return null; + selectors.add("orespawn:all_except_nether_end", placement); + } else { + for (int dimension : selection.ids) { + JsonObject placement = placement(modId, name, spawn, replacements, flags); + if (placement == null) return null; + zone.moddev.mc.orespawn.util.JsonCopies.add(dimensions, dimensionId(dimension), placement); + } + } + if (zone.moddev.mc.orespawn.util.JsonCopies.size(dimensions) > 0) ore.add("dimensions", dimensions); + if (zone.moddev.mc.orespawn.util.JsonCopies.size(selectors) > 0) ore.add("dimension_selectors", selectors); + if (zone.moddev.mc.orespawn.util.JsonCopies.size(dimensions) == 0 && zone.moddev.mc.orespawn.util.JsonCopies.size(selectors) == 0) { + REPORT.add("spawn_ignored=" + modId + ":" + name + ":no_dimensions"); return null; + } + if (bool(spawn, "retrogen", false)) REPORT.add("retrogen_requested=" + modId + ":" + name); + return ore; + } + + private static JsonObject placement(String modId, String name, JsonObject spawn, + JsonObject replacements, LegacyFlags flags) { + String feature = normalizePattern(text(spawn, "feature", "default")); + JsonObject parameters = parameters(feature, object(spawn, "parameters")); JsonObject result = new JsonObject(); + result.addProperty("enabled", bool(spawn, "enabled", true)); + int minY = clamp(integer(parameters, "minHeight", 0), 0, 255); + int exclusiveMaxY = clamp(integer(parameters, "maxHeight", 256), 0, 256); + if (exclusiveMaxY <= minY) { + REPORT.add("spawn_ignored=" + modId + ":" + name + ":empty_height_range=" + minY + ".." + exclusiveMaxY); + return null; + } + result.addProperty("min_y", minY); result.addProperty("max_y", exclusiveMaxY - 1); + double frequency = legacyFrequency(feature, parameters); + result.addProperty("frequency", clampedFrequency(modId, name, frequency)); + addQuantity(result, modId, name, feature, parameters); + String pattern = feature; + if ("normal_cloud".equals(pattern) || "default".equals(pattern) || "vein".equals(pattern) + || "precision".equals(pattern) || "clusters".equals(pattern) || "underfluids".equals(pattern)) { + result.addProperty("pattern", "orespawn:" + pattern); + } else { + result.addProperty("pattern", "orespawn:default"); + REPORT.add("custom_feature_scheduled=" + feature); + } + result.addProperty("height_distribution", "uniform"); + result.addProperty("spread", clamp(integer(parameters, "maxSpread", 8), 0, 64)); + result.addProperty("vertical_spread", clamp(integer(parameters, "variation", 4), 0, 64)); + result.addProperty("node_size", clamp(integer(parameters, "nodeSize", + integer(parameters, "size", 4)), 1, 32)); + result.addProperty("length", clamp(integer(parameters, "length", 16), 1, 64)); + String fluid = text(parameters, "fluid", "water"); + result.addProperty("fluid", fluid.indexOf(':') >= 0 ? fluid : "minecraft:" + fluid); + JsonArray hosts = replacementHosts(text(spawn, "replaces", "default"), replacements); + if ("default".equals(text(spawn, "replaces", "default"))) { + for (String configured : flags.nonstandardHosts) addLegacyState(hosts, configured, ""); + } + if ("default".equals(text(spawn, "replaces", "default")) && isModLoaded("mineralogy")) { + appendMineralogyRockHosts(hosts); + } + hosts = uniqueHosts(hosts); + if (hosts.size() == 0) { + zone.moddev.mc.orespawn.util.JsonCopies.add(hosts, "minecraft:stone"); zone.moddev.mc.orespawn.util.JsonCopies.add(hosts, "minecraft:netherrack"); zone.moddev.mc.orespawn.util.JsonCopies.add(hosts, "minecraft:end_stone"); + } + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "host_blocks", hosts); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "host_tags", new JsonArray()); zone.moddev.mc.orespawn.util.JsonCopies.add(result, "host_families", new JsonArray()); + copyBiomeSelectors(spawn, result); + return result; + } + + private static JsonArray uniqueHosts(JsonArray hosts) { + JsonArray result = new JsonArray(); + Set seen = new LinkedHashSet<>(); + for (JsonElement host : hosts) { + String identity = host.toString(); + if (seen.add(identity)) zone.moddev.mc.orespawn.util.JsonCopies.add(result, new JsonParser().parse(identity)); + } + return result; + } + + private static JsonObject parameters(String feature, JsonObject configured) { + JsonObject result = new JsonObject(); + if ("default".equals(feature)) { + result.addProperty("minHeight", 0); result.addProperty("maxHeight", 256); + result.addProperty("variation", 16); result.addProperty("frequency", 0.5D); + result.addProperty("size", 8); + } else if ("vein".equals(feature)) { + result.addProperty("minHeight", 0); result.addProperty("maxHeight", 256); + result.addProperty("variation", 16); result.addProperty("frequency", 50); + result.addProperty("attemptsMin", 4); result.addProperty("attemptsMax", 8); + result.addProperty("length", 16); result.addProperty("size", 3); + } else if ("normal_cloud".equals(feature)) { + result.addProperty("maxSpread", 16); result.addProperty("size", 8); + result.addProperty("minHeight", 8); result.addProperty("maxHeight", 24); + result.addProperty("variation", 4); result.addProperty("frequency", 25); + result.addProperty("attemptsMin", 4); result.addProperty("attemptsMax", 4); + } else if ("precision".equals(feature)) { + result.addProperty("numObjects", 4); result.addProperty("minHeight", 16); + result.addProperty("maxHeight", 80); result.addProperty("size", 8); + } else if ("clusters".equals(feature)) { + result.addProperty("maxSpread", 16); result.addProperty("size", 8); + result.addProperty("numObjects", 8); result.addProperty("minHeight", 8); + result.addProperty("maxHeight", 24); result.addProperty("variation", 4); + result.addProperty("frequency", 25); result.addProperty("attemptsMin", 4); + result.addProperty("attemptsMax", 8); + } else if ("underfluids".equals(feature)) { + result.addProperty("minHeight", 0); result.addProperty("maxHeight", 256); + result.addProperty("variation", 16); result.addProperty("attemptsMin", 4); + result.addProperty("attemptsMax", 4); result.addProperty("size", 8); + result.addProperty("fluid", "water"); + } + for (Map.Entry entry : configured.entrySet()) { + zone.moddev.mc.orespawn.util.JsonCopies.add(result, entry.getKey(), new JsonParser().parse(entry.getValue().toString())); + } + return result; + } + + private static String normalizePattern(String value) { + String result = value == null ? "default" : value.trim().toLowerCase(java.util.Locale.ROOT) + .replace('-', '_'); + if ("cloud".equals(result)) return "normal_cloud"; + if ("cluster".equals(result)) return "clusters"; + if ("under_fluid".equals(result)) return "underfluids"; + return result; + } + + private static double legacyFrequency(String feature, JsonObject parameters) { + if ("default".equals(feature)) return decimal(parameters, "frequency", 0.5D); + if ("precision".equals(feature)) return integer(parameters, "numObjects", 4); + double min = integer(parameters, "attemptsMin", 1); + double max = integer(parameters, "attemptsMax", (int) min); + double attempts = (min + max) / 2.0D; + if ("underfluids".equals(feature)) return attempts; + return attempts * decimal(parameters, "frequency", 100.0D) / 100.0D; + } + + private static double clampedFrequency(String modId, String name, double value) { + double clamped = clamp(value, 0.0D, 64.0D); + if (!Double.isFinite(value) || value != clamped) { + REPORT.add("frequency_clamped=" + modId + ":" + name + ":" + value + "->" + clamped); + } + return Double.isFinite(clamped) ? clamped : 0.0D; + } + + private static void addQuantity(JsonObject result, String modId, String name, + String feature, JsonObject parameters) { + long size = integer(parameters, "size", 8); + long variation = Math.max(0, integer(parameters, "variation", 0)); + long minimum = size; + long maximum = size; + if ("vein".equals(feature)) { + long length = Math.max(1, integer(parameters, "length", 16)); + minimum = Math.max(1, length - variation) * size; + maximum = (length + variation - (variation > 0 ? 1 : 0)) * size; + } else if ("clusters".equals(feature)) { + long nodes = Math.max(1, integer(parameters, "numObjects", 8)); + minimum = Math.max(1, size - variation) * Math.max(1, nodes - variation); + maximum = (size + variation - (variation > 0 ? 1 : 0)) + * (nodes + variation - (variation > 0 ? 1 : 0)); + } else if (!"precision".equals(feature) && variation > 0) { + minimum = size - variation; + maximum = size + variation - 1; + } + int min = clampQuantity(modId, name, minimum); + int max = clampQuantity(modId, name, maximum); + if (min > max) { int swap = min; min = max; max = swap; } + if (min == max) result.addProperty("quantity", min); + else { result.addProperty("min_quantity", min); result.addProperty("max_quantity", max); } + } + + private static int clampQuantity(String modId, String name, long value) { + int clamped = (int) Math.max(1L, Math.min(64L, value)); + if (value != clamped) REPORT.add("quantity_clamped=" + modId + ":" + name + ":" + value + "->" + clamped); + return clamped; + } + + private static void copyBiomeSelectors(JsonObject spawn, JsonObject placement) { + JsonObject biomes = object(spawn, "biomes"); + JsonArray ids = new JsonArray(), excludedIds = new JsonArray(); + JsonArray dictionary = new JsonArray(), excludedDictionary = new JsonArray(); + copySelectors(biomes.get("includes"), ids, dictionary); + copySelectors(biomes.get("whitelist"), ids, dictionary); + copySelectors(biomes.get("excludes"), excludedIds, excludedDictionary); + copySelectors(biomes.get("blacklist"), excludedIds, excludedDictionary); + placement.add("biome_ids", ids); placement.add("excluded_biome_ids", excludedIds); + placement.add("biome_dictionary", dictionary); placement.add("excluded_biome_dictionary", excludedDictionary); + placement.add("geomes", new JsonObject()); + } + + private static void copySelectors(JsonElement source, JsonArray ids, JsonArray dictionary) { + if (source == null || !source.isJsonArray()) return; + for (JsonElement value : source.getAsJsonArray()) { + String text = value.getAsString(); + if (validId(text)) zone.moddev.mc.orespawn.util.JsonCopies.add(ids, text); else if (text.matches("[A-Za-z0-9_]+")) zone.moddev.mc.orespawn.util.JsonCopies.add(dictionary, text.toUpperCase(java.util.Locale.ROOT)); + else REPORT.add("biome_selector_ignored=" + text); + } + } + + private static DimensionSelection dimensions(JsonElement source) { + Set result = new LinkedHashSet<>(); + if (source == null || source.isJsonNull()) return new DimensionSelection(true, result); + if (source.isJsonArray()) { + for (JsonElement value : source.getAsJsonArray()) zone.moddev.mc.orespawn.util.JsonCopies.add(result, value.getAsInt()); + return new DimensionSelection(result.isEmpty(), result); + } + if (source.isJsonObject()) { + JsonObject object = source.getAsJsonObject(); + JsonArray included = array(object, "includes"); + if (included.size() == 0) return new DimensionSelection(true, result); + for (JsonElement value : included) zone.moddev.mc.orespawn.util.JsonCopies.add(result, value.getAsInt()); + for (JsonElement value : array(object, "excludes")) result.remove(value.getAsInt()); + if (result.isEmpty()) REPORT.add("dimension_selection_empty=" + source); + return new DimensionSelection(false, result); + } + zone.moddev.mc.orespawn.util.JsonCopies.add(result, source.getAsInt()); return new DimensionSelection(false, result); + } + + private static final class DimensionSelection { + final boolean defaultOverworld; final Set ids; + DimensionSelection(boolean defaultOverworld, Set ids) { + this.defaultOverworld = defaultOverworld; this.ids = ids; + } + } + + private static String dimensionId(int dimension) { + if (dimension == 0) return "minecraft:overworld"; + if (dimension == -1) return "minecraft:the_nether"; + if (dimension == 1) return "minecraft:the_end"; + return "legacy:dimension_" + dimension; + } + + private static JsonObject readReplacements(Path legacyDirectory) throws IOException { + JsonObject result = new JsonParser().parse(API.embeddedReplacements.toString()).getAsJsonObject(); + Path system = legacyDirectory.resolve("sysconf"); + if (!Files.isDirectory(system)) return result; + try (DirectoryStream files = Files.newDirectoryStream(system, "replacements-*.json")) { + for (Path file : files) try (InputStream input = Files.newInputStream(file)) { + JsonElement root = new JsonParser().parse(new InputStreamReader(input, StandardCharsets.UTF_8)); + mergeReplacementElement(result, root); + } + } + return result; + } + + private static void mergeReplacementElement(JsonObject result, JsonElement root) { + if (root.isJsonObject()) { + for (Map.Entry entry : root.getAsJsonObject().entrySet()) { + if (entry.getValue().isJsonArray()) zone.moddev.mc.orespawn.util.JsonCopies.add(result, entry.getKey(), new JsonParser().parse(entry.getValue().toString())); + } + } else if (root.isJsonArray()) { + for (JsonElement element : root.getAsJsonArray()) { + if (!element.isJsonObject()) continue; JsonObject entry = element.getAsJsonObject(); + String name = text(entry, "name", ""); + if (!name.isEmpty()) { + JsonArray values = result.has(name) && result.get(name).isJsonArray() + ? result.getAsJsonArray(name) : new JsonArray(); + zone.moddev.mc.orespawn.util.JsonCopies.add(values, new JsonParser().parse(entry.toString())); zone.moddev.mc.orespawn.util.JsonCopies.add(result, name, values); + } + } + } + } + + private static JsonArray replacementHosts(String name, JsonObject replacements) { + JsonArray result = new JsonArray(); + if (!replacements.has(name) || !replacements.get(name).isJsonArray()) { + if ("default".equals(name)) { + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "minecraft:stone"); zone.moddev.mc.orespawn.util.JsonCopies.add(result, "minecraft:netherrack"); zone.moddev.mc.orespawn.util.JsonCopies.add(result, "minecraft:end_stone"); + } + return result; + } + for (JsonElement element : replacements.getAsJsonArray(name)) { + if (!element.isJsonObject()) continue; + JsonObject replacement = element.getAsJsonObject(); + String block = text(replacement, "blockName", text(replacement, "name", "")); + if (replacement.has("metadata")) block += "@" + replacement.get("metadata").getAsInt(); + addLegacyState(result, block, text(replacement, "blockState", text(replacement, "state", ""))); + } + return result; + } + + private static void addLegacyState(JsonArray result, String block, String serializedState) { + int explicit = -1; + int at = block == null ? -1 : block.lastIndexOf('@'); + if (at > 0) { + try { explicit = Integer.parseInt(block.substring(at + 1)); block = block.substring(0, at); } + catch (NumberFormatException ignored) { } + } + if ("minecraft:granite".equals(block)) { block = "minecraft:stone"; explicit = 1; } + else if ("minecraft:diorite".equals(block)) { block = "minecraft:stone"; explicit = 3; } + else if ("minecraft:andesite".equals(block)) { block = "minecraft:stone"; explicit = 5; } + if (!validId(block)) return; + int value = explicit >= 0 ? explicit : metadata(block, serializedState); + if (value == 0) zone.moddev.mc.orespawn.util.JsonCopies.add(result, block); + else { JsonObject host = new JsonObject(); host.addProperty("block", block); host.addProperty("metadata", clamp(value, 0, 15)); zone.moddev.mc.orespawn.util.JsonCopies.add(result, host); } + } + + private static void appendMineralogyRockHosts(JsonArray result) { + Set known = new LinkedHashSet<>(); + for (JsonElement value : result) { + known.add(value.isJsonObject() ? text(value.getAsJsonObject(), "block", "") : value.getAsString()); + } + for (String oreName : OreDictionary.getOreNames()) { + if (!oreName.startsWith("stone")) continue; + for (ItemStack stack : OreDictionary.getOres(oreName, false)) { + Block block = Block.getBlockFromItem(stack.getItem()); + ResourceLocation id = block == null ? null : block.getRegistryName(); + if (id != null && "mineralogy".equals(id.getResourceDomain()) && known.add(id.toString())) { + zone.moddev.mc.orespawn.util.JsonCopies.add(result, id.toString()); + } + } + } + REPORT.add("mineralogy_hosts_imported=" + known.stream().filter(value -> value.startsWith("mineralogy:")).count()); + } + + private static void copyMetadata(JsonObject old, JsonObject target) { + if (old.has("metadata")) target.addProperty("metadata", clamp(old.get("metadata").getAsInt(), 0, 15)); + else if (old.has("state")) target.addProperty("metadata", metadata(text(old, "name", ""), old.get("state").getAsString())); + } + + private static int metadata(String block, String state) { + if (!"minecraft:stone".equals(block)) return 0; + String lower = state == null ? "" : state.toLowerCase(java.util.Locale.ROOT); + if (lower.contains("smooth_granite")) return 2; if (lower.contains("granite")) return 1; + if (lower.contains("smooth_diorite")) return 4; if (lower.contains("diorite")) return 3; + if (lower.contains("smooth_andesite")) return 6; if (lower.contains("andesite")) return 5; + return 0; + } + + private static void writeAtomicIfChanged(Path destination, JsonObject value) throws IOException { + byte[] data = (GSON.toJson(value) + System.lineSeparator()).getBytes(StandardCharsets.UTF_8); + if (Files.isRegularFile(destination) && Arrays.equals(Files.readAllBytes(destination), data)) { + REPORT.add("provider_unchanged=" + destination.getFileName()); return; + } + if (Files.isRegularFile(destination)) { + Path backup = destination.resolveSibling(destination.getFileName() + ".os3-backup"); + if (!Files.exists(backup)) Files.copy(destination, backup); + } + Path temporary = destination.resolveSibling(destination.getFileName() + ".tmp"); + Files.write(temporary, data); + try { Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } + catch (IOException failure) { Files.deleteIfExists(temporary); throw failure; } + REPORT.add("provider_written=" + destination.getFileName()); + } + + private static void writeMarker(Path destination) throws IOException { + Path temporary = destination.resolveSibling(destination.getFileName() + ".tmp"); + Files.write(temporary, ("provider_schema=4" + System.lineSeparator()).getBytes(StandardCharsets.UTF_8)); + try { Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } + catch (IOException failure) { Files.deleteIfExists(temporary); throw failure; } + } + + private static void writeReport(Path destination) { + try { + JsonObject report = new JsonObject(); report.addProperty("format", 1); report.addProperty("idempotent", true); + JsonArray rows = new JsonArray(); for (String row : REPORT) zone.moddev.mc.orespawn.util.JsonCopies.add(rows, row); report.add("entries", rows); + writeAtomicIfChanged(destination, report); + writeHumanUpgradeReport(destination.resolveSibling("orespawn-upgrade-report.txt")); + } catch (IOException failure) { LOGGER.error("Could not write OS3 migration report", failure); } + } + + private static void writeHumanUpgradeReport(Path destination) throws IOException { + Set sources = new LinkedHashSet<>(); + Set providers = new LinkedHashSet<>(); + Set warnings = new LinkedHashSet<>(); + Set details = new LinkedHashSet<>(REPORT); + for (String row : REPORT) { + String lower = row.toLowerCase(java.util.Locale.ROOT); + if (lower.startsWith("config_source=") || lower.startsWith("resource_loaded=") + || lower.startsWith("config_registered=")) sources.add(row); + if ((lower.startsWith("provider_written=") || lower.startsWith("provider_unchanged=")) + && !lower.contains("migration-report")) providers.add(row); + if (lower.contains("_failed=") || lower.contains("_rejected=") + || lower.contains("_ignored=") || lower.contains("_unresolved=") + || lower.contains("_clamped=") || lower.startsWith("resource_missing=")) { + warnings.add(row); + } + } + List lines = new ArrayList<>(); + lines.add("OreSpawn 4.0.16.111021 Upgrade Report"); + lines.add("================================"); + lines.add(""); + lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); + lines.add("- Legacy sources read: " + sources.size()); + lines.add("- OS4 provider files written or verified: " + providers.size()); + lines.add("- Unique items requiring review: " + warnings.size()); + lines.add(""); + lines.add(warnings.isEmpty() + ? "WARNINGS: None reported during translation." + : "WARNINGS: Review rejected, ignored, unresolved, clamped, missing, or failed entries below."); + lines.add(""); + lines.add("Detailed migration entries"); + for (String row : details) lines.add("- " + row); + lines.add(""); + lines.add("Machine-readable details: " + + destination.resolveSibling("orespawn-os3-migration-report.json").toAbsolutePath()); + lines.add("Original legacy configuration files were retained unchanged."); + byte[] data = (String.join(System.lineSeparator(), lines) + System.lineSeparator()) + .getBytes(StandardCharsets.UTF_8); + if (Files.isRegularFile(destination) && Arrays.equals(Files.readAllBytes(destination), data)) return; + Path temporary = destination.resolveSibling(destination.getFileName() + ".tmp"); + Files.write(temporary, data); + try { + Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException failure) { + Files.deleteIfExists(temporary); + throw failure; + } + } + + private static JsonObject readObject(InputStream input) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + JsonElement value = new JsonParser().parse(reader); if (!value.isJsonObject()) throw new IOException("root is not an object"); + return value.getAsJsonObject(); + } + } + + private static JsonElement readElement(InputStream input) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + JsonElement value = new JsonParser().parse(reader); + if (value == null || value.isJsonNull()) throw new IOException("empty JSON resource"); + return value; + } + } + + private static boolean validModId(String value) { return value != null && value.matches("[a-z][a-z0-9_.-]{1,63}"); } + private static boolean isModLoaded(String modId) { + try { return Loader.isModLoaded(modId); } + catch (RuntimeException unavailable) { return false; } + } + private static boolean validId(String value) { return value != null && value.matches("[a-z0-9_.-]+:[a-z0-9_./-]+"); } + private static String safe(String value) { return value.toLowerCase(java.util.Locale.ROOT).replaceAll("[^a-z0-9_./-]", "_"); } + private static JsonObject object(JsonObject value, String key) { return value.has(key) && value.get(key).isJsonObject() ? value.getAsJsonObject(key) : new JsonObject(); } + private static JsonArray array(JsonObject value, String key) { return value.has(key) && value.get(key).isJsonArray() ? value.getAsJsonArray(key) : new JsonArray(); } + private static String text(JsonObject value, String key, String fallback) { return value.has(key) ? value.get(key).getAsString() : fallback; } + private static boolean bool(JsonObject value, String key, boolean fallback) { return value.has(key) ? value.get(key).getAsBoolean() : fallback; } + private static int integer(JsonObject value, String key, int fallback) { return value.has(key) ? value.get(key).getAsInt() : fallback; } + private static double decimal(JsonObject value, String key, double fallback) { return value.has(key) ? value.get(key).getAsDouble() : fallback; } + private static int clamp(int value, int min, int max) { return Math.max(min, Math.min(max, value)); } + private static double clamp(double value, double min, double max) { return Math.max(min, Math.min(max, value)); } + + private static IBlockState state(Object value, int metadata) { + Block block = null; + if (value instanceof IBlockState) return (IBlockState) value; + if (value instanceof Block) block = (Block) value; + else if (value instanceof ResourceLocation) block = ForgeRegistries.BLOCKS.getValue((ResourceLocation) value); + else if (value instanceof String && validId((String) value)) block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation((String) value)); + return block == null || block == Blocks.AIR ? null : block.getStateFromMeta(clamp(metadata, 0, 15)); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static IBlockState state(Object value, Object serializedState) { + if (serializedState instanceof Integer) return state(value, ((Integer) serializedState).intValue()); + IBlockState result = state(value, 0); + if (result == null || !(serializedState instanceof String)) return result; + for (String assignment : ((String) serializedState).split(",")) { + String[] parts = assignment.trim().split("=", 2); + if (parts.length != 2) continue; + for (net.minecraft.block.properties.IProperty property : result.getPropertyKeys()) { + if (!property.getName().equals(parts[0].trim())) continue; + com.google.common.base.Optional parsed = property.parseValue(parts[1].trim()); + if (parsed.isPresent()) result = result.withProperty(property, (Comparable) parsed.get()); + break; + } + } + return result; + } + + private static final class LegacyApi implements OS3API { + private final Map replacements = new LinkedHashMap<>(); + private final Map spawns = new LinkedHashMap<>(); + private final Map spawnOwners = new LinkedHashMap<>(); + private final Set translatedSpawns = new LinkedHashSet<>(); + private final Map logics = new LinkedHashMap<>(); + private final Map logicOwners = new LinkedHashMap<>(); + private final Set translated322Spawns = + Collections.newSetFromMap(new java.util.IdentityHashMap()); + private final Map> entriesByFile = new LinkedHashMap<>(); + private final Map embedded = new LinkedHashMap<>(); + private final JsonObject embeddedReplacements = new JsonObject(); + private final OS3V2PresetStorage oldPresets = new OS3V2PresetStorage(); + private final PresetsStorage presets = new PresetsStorage(); + private String activeModId = "legacy"; + + boolean hasProgrammaticRegistrations() { return !spawns.isEmpty() || !logics.isEmpty(); } + + Map programmaticSources() { + Map result = new LinkedHashMap<>(); + translatedSpawns.clear(); + translated322Spawns.clear(); + for (Map.Entry registered : spawns.entrySet()) { + if (!(registered.getValue() instanceof LegacySpawnEntry)) continue; + LegacySpawnEntry spawn = (LegacySpawnEntry) registered.getValue(); + JsonObject migrated = spawn.toLegacyJson(this); + if (migrated == null) { + REPORT.add("programmatic_custom_scheduled=" + registered.getKey()); + continue; + } + String owner = spawnOwners.getOrDefault(registered.getKey(), activeModId); + JsonObject source = result.computeIfAbsent(owner, ignored -> new JsonObject()); + source.addProperty("version", "2.0"); + JsonObject sourceSpawns = object(source, "spawns"); + sourceSpawns.add(registered.getKey(), migrated); + source.add("spawns", sourceSpawns); + translatedSpawns.add(registered.getKey()); + REPORT.add("programmatic_provider_rule=" + owner + ":" + registered.getKey()); + } + for (Map.Entry registered : logics.entrySet()) { + if (!(registered.getValue() instanceof LegacyBuilderLogic)) continue; + String owner = logicOwners.getOrDefault(registered.getKey(), activeModId); + JsonObject source = result.computeIfAbsent(owner, ignored -> new JsonObject()); + source.addProperty("version", "2.0"); + JsonObject sourceSpawns = object(source, "spawns"); + ((LegacyBuilderLogic) registered.getValue()).contributeProviderRules( + owner, sourceSpawns, translated322Spawns, this); + source.add("spawns", sourceSpawns); + } + return result; + } + + @Override public int dimensionWildcard() { return Integer.MIN_VALUE; } + @Override public int biomeWildcard() { return Integer.MIN_VALUE; } + @Override public void registerReplacementBlock(String name, Block block) { registerReplacementBlock(name, block.getDefaultState()); } + @Override public void registerReplacementBlock(String name, IBlockState state) { + LegacyReplacementEntry entry = new LegacyReplacementEntry(name, Collections.singletonList(state)); + replacements.put(name, entry); + rememberReplacement(name, entry.getEntries()); + } + @Override public void registerFeatureGenerator(String name, IFeature feature) { addFeature(name, feature); } + @Override public void registerFeatureGenerator(String name, Class feature) { + try { Constructor constructor = feature.getDeclaredConstructor(); constructor.setAccessible(true); addFeature(name, constructor.newInstance()); } + catch (ReflectiveOperationException failure) { throw new IllegalArgumentException(failure); } + } + @Override public void registerFeatureGenerator(String name, String className) { + try { registerFeatureGenerator(name, Class.forName(className).asSubclass(IFeature.class)); } + catch (ClassNotFoundException failure) { throw new IllegalArgumentException(failure); } + } + @Override public BuilderLogic getLogic(String name) { + BuilderLogic existing = logics.get(name); + if (existing != null) return existing; + BuilderLogic created = new LegacyBuilderLogic(name); + logics.put(name, created); + logicOwners.put(name, activeModId); + return created; + } + @Override public void registerLogic(BuilderLogic logic) { + for (BuilderLogic existing : logics.values()) if (existing == logic) return; + String key = activeModId + ":logic_" + logics.size(); + logics.put(key, logic); + logicOwners.put(key, activeModId); + } + @Override public ImmutableMap getSpawns() { return ImmutableMap.copyOf(logics); } + @Override public void registerSpawns() { REPORT.add("programmatic_322_registered=" + logics.size()); } + @Override public OreSpawnWorldGen getGenerator() { return new OreSpawnWorldGen(Collections.emptyMap(), 0L); } + @Override public OS3V2PresetStorage getPresets() { return oldPresets; } + + @Override public void addSpawn(ISpawnEntry spawnEntry) { + if (spawnEntry == null || spawnEntry.getSpawnName() == null) throw new IllegalArgumentException("Unnamed OS3 spawn"); + if (spawns.putIfAbsent(spawnEntry.getSpawnName(), spawnEntry) != null) throw new IllegalArgumentException("Duplicate OS3 spawn " + spawnEntry.getSpawnName()); + spawnOwners.put(spawnEntry.getSpawnName(), activeModId); + } + @Override public void addFeature(String featureName, IFeature feature) { + FEATURES.addFeature(featureName, feature); + registerSavedFeature(featureName, feature); + REPORT.add("programmatic_feature=" + featureName); + } + @Override public void addReplacement(IReplacementEntry replacementEntry) { + String name = replacementEntry.getRegistryName() == null ? "replacement_" + zone.moddev.mc.orespawn.util.JsonCopies.size(replacements) : replacementEntry.getRegistryName().toString(); + replacements.put(name, replacementEntry); + rememberReplacement(name, replacementEntry.getEntries()); + registerSavedReplacement(replacementEntry); + } + @Override public Map getReplacements() { return Collections.unmodifiableMap(replacements); } + @Override public IReplacementEntry getReplacement(String replacementName) { return replacements.get(replacementName); } + @Override public List getSpawns(int dimensionID) { + List result = new ArrayList<>(); for (ISpawnEntry spawn : spawns.values()) if (spawn.dimensionAllowed(dimensionID)) zone.moddev.mc.orespawn.util.JsonCopies.add(result, spawn); return result; + } + @Override public ISpawnEntry getSpawn(String spawnName) { return spawns.get(spawnName); } + @Override public Map getAllSpawns() { return Collections.unmodifiableMap(spawns); } + @Override public List getDimensionDefaultReplacements(int dimensionID) { + return Collections.singletonList(dimensionID == -1 ? Blocks.NETHERRACK.getDefaultState() + : dimensionID == 1 ? Blocks.END_STONE.getDefaultState() : Blocks.STONE.getDefaultState()); + } + @Override public ISpawnBuilder getSpawnBuilder() { return builder(ISpawnBuilder.class); } + @Override public IDimensionBuilder getDimensionBuilder() { return builder(IDimensionBuilder.class); } + @Override public IFeatureBuilder getFeatureBuilder() { return builder(IFeatureBuilder.class); } + @Override public IBlockBuilder getBlockBuilder() { return builder(IBlockBuilder.class); } + @Override public IBiomeBuilder getBiomeBuilder() { return builder(IBiomeBuilder.class); } + @Override public IReplacementBuilder getReplacementBuilder() { return builder(IReplacementBuilder.class); } + @Override public boolean featureExists(String featureName) { return FEATURES.hasFeature(featureName); } + @Override public boolean featureExists(ResourceLocation featureName) { return FEATURES.hasFeature(featureName); } + @Override public IFeature getFeature(String featureName) { return FEATURES.getFeature(featureName); } + @Override public IFeature getFeature(ResourceLocation featureName) { return FEATURES.getFeature(featureName); } + @Override public PresetsStorage copyPresets() { PresetsStorage copy = new PresetsStorage(); copy.copy(presets); return copy; } + @Override public void loadConfigFiles() { } + @Override public boolean hasReplacement(ResourceLocation name) { return hasReplacement(name.toString()); } + @Override public boolean hasReplacement(String name) { return replacements.containsKey(name); } + @Override public void mapEntryToFile(Path path, String entryName) { entriesByFile.computeIfAbsent(path, key -> new ArrayList<>()).add(entryName); } + @Override public List getSpawnsForFile(String fileName) { + for (Map.Entry> entry : entriesByFile.entrySet()) if (entry.getKey().getFileName().toString().equals(fileName)) return entry.getValue(); + return Collections.emptyList(); + } + @Override public Map> getSpawnsByFile() { return Collections.unmodifiableMap(entriesByFile); } + + private T builder(Class type) { + return type.cast(java.lang.reflect.Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, new LegacyBuilderHandler(this, type))); + } + + void generate(Random random, ChunkPos pos, World world, IChunkGenerator generator, IChunkProvider provider) { + for (Map.Entry registered : spawns.entrySet()) { + if (translatedSpawns.contains(registered.getKey())) continue; + ISpawnEntry spawn = registered.getValue(); + if (spawn.isEnabled() && spawn.dimensionAllowed(world.provider.getDimension())) spawn.generate(random, world, generator, provider, pos); + } + for (BuilderLogic value : logics.values()) if (value instanceof LegacyBuilderLogic) { + ((LegacyBuilderLogic) value).generate(random, pos, world, generator, provider, + translated322Spawns); + } + } + + private void rememberReplacement(String name, List states) { + JsonArray entries = new JsonArray(); + for (IBlockState blockState : states) { + if (blockState == null || blockState.getBlock().getRegistryName() == null) continue; + JsonObject entry = new JsonObject(); + entry.addProperty("name", blockState.getBlock().getRegistryName().toString()); + int metadata = blockState.getBlock().getMetaFromState(blockState); + if (metadata != 0) entry.addProperty("metadata", metadata); + zone.moddev.mc.orespawn.util.JsonCopies.add(entries, entry); + } + embeddedReplacements.add(name, entries); + } + + private void resetProgrammatic(String owner) { + replacements.clear(); + spawns.clear(); + spawnOwners.clear(); + translatedSpawns.clear(); + logics.clear(); + logicOwners.clear(); + translated322Spawns.clear(); + entriesByFile.clear(); + embedded.clear(); + embeddedReplacements.entrySet().clear(); + activeModId = owner; + } + } + + private static final class LegacyBuilderHandler implements java.lang.reflect.InvocationHandler { + private final LegacyApi api; + private final Class type; + private final Map values = new LinkedHashMap<>(); + private final List blocks = new ArrayList<>(); + private final List replacementStates = new ArrayList<>(); + private final Set dimensionIncludes = new LinkedHashSet<>(); + private final Set dimensionExcludes = new LinkedHashSet<>(); + private final Set biomeIncludes = new LinkedHashSet<>(); + private final Set biomeExcludes = new LinkedHashSet<>(); + private Object blockSource; + private Object blockState; + private int blockChance = 100; + private boolean dimensionAll; + private boolean dimensionOverworld = true; + private boolean dimensionDenied; + private boolean biomeAll; + private String featureName; + private IFeature feature; + private JsonObject featureParameters = new JsonObject(); + private boolean featureUseDefaults; + + LegacyBuilderHandler(LegacyApi api, Class type) { this.api = api; this.type = type; } + + @Override public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) { + String name = method.getName(); + Object[] arguments = args == null ? new Object[0] : args; + if (method.getDeclaringClass() == Object.class) { + if ("toString".equals(name)) return "LegacyOS3" + type.getSimpleName(); + if ("hashCode".equals(name)) return System.identityHashCode(proxy); + if ("equals".equals(name)) return proxy == arguments[0]; + } + if (type == IBlockBuilder.class && name.startsWith("setFrom")) { + captureBlock(name, arguments); + return proxy; + } + if ("setChance".equals(name)) { blockChance = (Integer) arguments[0]; return proxy; } + if (type == IFeatureBuilder.class && "setFeature".equals(name)) { + if (arguments[0] instanceof IFeature) feature = (IFeature) arguments[0]; + else { + featureName = arguments[0].toString(); + feature = api.getFeature(featureName); + } + return proxy; + } + if (type == IFeatureBuilder.class && "setParameter".equals(name)) { + JsonElement parameter = arguments[1] instanceof JsonElement + ? new JsonParser().parse(arguments[1].toString()) : GSON.toJsonTree(arguments[1]); + featureParameters.add((String) arguments[0], parameter); + return proxy; + } + if (type == IFeatureBuilder.class && "setUseFeatureDefaults".equals(name)) { + featureUseDefaults = true; + return proxy; + } + if (type == IDimensionBuilder.class) { + if ("addWhitelistEntry".equals(name)) { dimensionIncludes.add((Integer) arguments[0]); dimensionOverworld = false; } + else if ("addBlacklistEntry".equals(name)) { dimensionExcludes.add((Integer) arguments[0]); dimensionOverworld = false; } + else if ("setAcceptAll".equals(name)) { dimensionAll = true; dimensionOverworld = false; dimensionDenied = false; } + else if ("setAcceptAllOverworld".equals(name)) { dimensionAll = true; dimensionOverworld = true; dimensionDenied = false; } + else if ("setDenyAll".equals(name)) { dimensionDenied = true; dimensionAll = false; dimensionOverworld = false; } + if (!"create".equals(name)) return proxy; + } + if (type == IBiomeBuilder.class) { + if ("addWhitelistEntry".equals(name)) addBiome(biomeIncludes, arguments[0]); + else if ("addBlacklistEntry".equals(name)) addBiome(biomeExcludes, arguments[0]); + else if ("setAcceptAll".equals(name)) biomeAll = true; + if (!"create".equals(name)) return proxy; + } + if (type == ISpawnBuilder.class && name.startsWith("addBlock")) { + if (arguments.length == 1 && arguments[0] instanceof IBlockDefinition) zone.moddev.mc.orespawn.util.JsonCopies.add(blocks, (IBlockDefinition) arguments[0]); + else zone.moddev.mc.orespawn.util.JsonCopies.add(blocks, blockDefinition(name, arguments)); + return proxy; + } + if (type == IReplacementBuilder.class && "addEntry".equals(name)) { + IBlockState state = argumentState(arguments); + if (state != null) replacementStates.add(state); + return proxy; + } + if (type == IReplacementBuilder.class && "setFromName".equals(name)) { + String entryName = String.valueOf(arguments[0]); + values.put(name, entryName); + IReplacementEntry existing = api.getReplacement(entryName); + if (existing == null) throw new IllegalArgumentException("Unknown OS3 replacement " + entryName); + replacementStates.addAll(existing.getEntries()); + return proxy; + } + if ("hasEntries".equals(name)) return !replacementStates.isEmpty(); + if ("create".equals(name)) return create(proxy); + if (name.startsWith("set")) { + values.put(name, arguments.length == 1 ? arguments[0] : Arrays.asList(arguments)); + return proxy; + } + if (method.getReturnType().isInstance(proxy)) return proxy; + return primitiveDefault(method.getReturnType()); + } + + private Object create(Object proxy) { + if (type == IBlockBuilder.class) return new LegacyBlockDefinition(state(blockSource, blockState), blockChance); + if (type == IDimensionBuilder.class) { + if (dimensionDenied) return LegacyDimensionList.none(); + if (dimensionAll || (dimensionIncludes.isEmpty() && dimensionExcludes.isEmpty())) { + return new LegacyDimensionList(Collections.emptySet(), dimensionExcludes, true, dimensionOverworld); + } + if (!dimensionIncludes.isEmpty()) return new LegacyDimensionList(dimensionIncludes, dimensionExcludes, false, false); + return new LegacyDimensionList(Collections.emptySet(), dimensionExcludes, true, false); + } + if (type == IBiomeBuilder.class) return biomeAll + ? LegacyBiomeLocation.all() + : new LegacyBiomeLocation(biomeIncludes, biomeExcludes, + biomeIncludes.isEmpty() && !biomeExcludes.isEmpty()); + if (type == IFeatureBuilder.class) { + JsonObject merged = feature == null || feature.getDefaultParameters() == null + ? new JsonObject() : new JsonParser().parse(feature.getDefaultParameters().toString()).getAsJsonObject(); + if (!featureUseDefaults) for (Map.Entry parameter : featureParameters.entrySet()) { + merged.add(parameter.getKey(), new JsonParser().parse(parameter.getValue().toString())); + } + return new LegacyFeatureEntry(featureName, feature, merged); + } + if (type == IReplacementBuilder.class) { + String name = stringValue("setName", stringValue("setFromName", + "replacement_" + api.replacements.size())); + return new LegacyReplacementEntry(name, replacementStates); + } + if (type == ISpawnBuilder.class) { + String name = stringValue("setName", api.activeModId + ":spawn_" + api.spawns.size()); + LegacySpawnEntry result = new LegacySpawnEntry(name, + (IDimensionList) values.getOrDefault("setDimensions", LegacyDimensionList.all()), + (BiomeLocation) values.getOrDefault("setBiomes", LegacyBiomeLocation.all()), + booleanValue("setEnabled", false), booleanValue("setRetrogen", false), + (IReplacementEntry) values.get("setReplacement"), + (IFeatureEntry) values.get("setFeature"), new LegacyBlockList(blocks)); + return result; + } + return proxy; + } + + private void captureBlock(String method, Object[] arguments) { + blockSource = arguments.length == 0 ? null : arguments[0]; + blockState = arguments.length > 1 && !method.endsWith("WithChance") ? arguments[1] : null; + if (method.endsWith("WithChance")) { + blockChance = (Integer) arguments[arguments.length - 1]; + blockState = arguments.length > 2 ? arguments[1] : null; + } + } + + private LegacyBlockDefinition blockDefinition(String method, Object[] arguments) { + int chance = method.contains("WithChance") ? (Integer) arguments[arguments.length - 1] : 100; + Object serialized = method.contains("WithChance") + ? (arguments.length > 2 ? arguments[1] : null) + : (arguments.length > 1 ? arguments[1] : null); + return new LegacyBlockDefinition(state(arguments[0], serialized), chance); + } + + private IBlockState argumentState(Object[] arguments) { + return state(arguments.length == 0 ? null : arguments[0], arguments.length > 1 ? arguments[1] : null); + } + + private static void addBiome(Set target, Object value) { + Biome biome = value instanceof Biome ? (Biome) value + : ForgeRegistries.BIOMES.getValue(value instanceof ResourceLocation + ? (ResourceLocation) value : new ResourceLocation(String.valueOf(value))); + if (biome != null) target.add(biome); + } + + private String stringValue(String key, String fallback) { Object value = values.get(key); return value == null ? fallback : String.valueOf(value); } + private boolean booleanValue(String key, boolean fallback) { Object value = values.get(key); return value instanceof Boolean ? (Boolean) value : fallback; } + } + + private static final class LegacyBlockDefinition implements IBlockDefinition { + private final IBlockState block; private final int chance; + LegacyBlockDefinition(IBlockState block, int chance) { this.block = block; this.chance = Math.max(0, chance); } + @Override public IBlockState getBlock() { return block; } + @Override public int getChance() { return chance; } + @Override public boolean isValid() { return block != null && block.getBlock() != Blocks.AIR; } + } + + private static final class LegacyBlockList implements IBlockList { + private final List blocks = new ArrayList<>(); private int total; + LegacyBlockList(List values) { for (IBlockDefinition value : values) addBlock(value); } + @Override public void addBlock(IBlockDefinition block) { if (block != null && block.isValid()) { zone.moddev.mc.orespawn.util.JsonCopies.add(blocks, block); total += Math.max(0, block.getChance()); } } + @Override public IBlockState getRandomBlock(Random random) { + if (blocks.isEmpty()) return null; int selected = random.nextInt(Math.max(1, total)); + for (IBlockDefinition block : blocks) { selected -= Math.max(0, block.getChance()); if (selected < 0) return block.getBlock(); } + return blocks.get(blocks.size() - 1).getBlock(); + } + @Override public void startNewSpawn() { } + @Override public void dump() { } + @Override public int count() { return blocks.size(); } + } + + private static final class LegacyDimensionList implements IDimensionList { + private final Set allowed; + private final Set denied; + private final boolean all; + private final boolean overworldOnly; + private LegacyDimensionList(Set allowed, Set denied, boolean all, boolean overworldOnly) { + this.allowed = Collections.unmodifiableSet(new LinkedHashSet<>(allowed)); + this.denied = Collections.unmodifiableSet(new LinkedHashSet<>(denied)); + this.all = all; + this.overworldOnly = overworldOnly; + } + static LegacyDimensionList all() { return new LegacyDimensionList(Collections.emptySet(), Collections.emptySet(), true, true); } + static LegacyDimensionList none() { return new LegacyDimensionList(Collections.emptySet(), Collections.emptySet(), false, false); } + static LegacyDimensionList only(int id) { return only(Collections.singleton(id)); } + static LegacyDimensionList only(Set ids) { return new LegacyDimensionList(ids, Collections.emptySet(), false, false); } + @Override public boolean matches(int dimensionId) { + if (denied.contains(dimensionId)) return false; + if (all) return !overworldOnly || (dimensionId != -1 && dimensionId != 1); + return allowed.contains(dimensionId); + } + @Override public JsonObject serialize() { + JsonObject result = new JsonObject(); + result.addProperty("accept_all", all); + result.addProperty("overworld_only", overworldOnly); + JsonArray ids = new JsonArray(); for (Integer id : allowed) zone.moddev.mc.orespawn.util.JsonCopies.add(ids, id); zone.moddev.mc.orespawn.util.JsonCopies.add(result, "includes", ids); + JsonArray excluded = new JsonArray(); for (Integer id : denied) zone.moddev.mc.orespawn.util.JsonCopies.add(excluded, id); zone.moddev.mc.orespawn.util.JsonCopies.add(result, "excludes", excluded); + return result; + } + } + + private static final class LegacyBiomeLocation implements BiomeLocation { + private final Set included; + private final Set excluded; + private final Set includedTypes; + private final Set excludedTypes; + private final boolean all; + LegacyBiomeLocation(Set included, Set excluded, boolean all) { + this(included, excluded, Collections.emptySet(), Collections.emptySet(), all); + } + LegacyBiomeLocation(Set included, Set excluded, Set includedTypes, + Set excludedTypes, boolean all) { + this.included = Collections.unmodifiableSet(new LinkedHashSet<>(included)); + this.excluded = Collections.unmodifiableSet(new LinkedHashSet<>(excluded)); + this.includedTypes = Collections.unmodifiableSet(new LinkedHashSet<>(includedTypes)); + this.excludedTypes = Collections.unmodifiableSet(new LinkedHashSet<>(excludedTypes)); + this.all = all; + } + static LegacyBiomeLocation all() { + return new LegacyBiomeLocation(Collections.emptySet(), Collections.emptySet(), true); + } + @Override public boolean matches(Biome biome) { + if (excluded.contains(biome) || matchesType(biome, excludedTypes)) return false; + return all || included.contains(biome) || matchesType(biome, includedTypes); + } + @Override public JsonElement serialize() { + JsonObject result = new JsonObject(); + JsonArray includes = new JsonArray(); + for (Biome biome : included) if (biome.getRegistryName() != null) zone.moddev.mc.orespawn.util.JsonCopies.add(includes, biome.getRegistryName().toString()); + for (String type : includedTypes) zone.moddev.mc.orespawn.util.JsonCopies.add(includes, type); + JsonArray excludes = new JsonArray(); + for (Biome biome : excluded) if (biome.getRegistryName() != null) zone.moddev.mc.orespawn.util.JsonCopies.add(excludes, biome.getRegistryName().toString()); + for (String type : excludedTypes) zone.moddev.mc.orespawn.util.JsonCopies.add(excludes, type); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "includes", includes); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "excludes", excludes); + return result; + } + private static boolean matchesType(Biome biome, Set names) { + for (String name : names) { + if (net.minecraftforge.common.BiomeDictionary.hasType(biome, + net.minecraftforge.common.BiomeDictionary.Type.getType(name))) return true; + } + return false; + } + } + + private static final class LegacyFeatureEntry implements IFeatureEntry { + private final String name; private final IFeature feature; private final JsonObject parameters; + LegacyFeatureEntry(String name, IFeature feature, JsonObject parameters) { this.name = name; this.feature = feature; this.parameters = parameters; } + @Override public IFeature getFeature() { return feature; } + @Override public String getFeatureName() { return name; } + @Override public JsonObject getFeatureParameters() { return parameters; } + @Override public void setParameter(String key, String value) { parameters.addProperty(key, value); } + @Override public void setParameter(String key, int value) { parameters.addProperty(key, value); } + @Override public void setParameter(String key, boolean value) { parameters.addProperty(key, value); } + @Override public void setParameter(String key, float value) { parameters.addProperty(key, value); } + } + + private static final class LegacyReplacementEntry extends IForgeRegistryEntry.Impl implements IReplacementEntry { + private final OreSpawnBlockMatcher matcher; private final List entries; + LegacyReplacementEntry(String name, List entries) { + this.entries = Collections.unmodifiableList(new ArrayList<>(entries)); this.matcher = new OreSpawnBlockMatcher(entries); + if (validId(name)) setRegistryName(new ResourceLocation(name)); else setRegistryName(new ResourceLocation("legacy", safe(name))); + } + @Override public OreSpawnBlockMatcher getMatcher() { return matcher; } + @Override public List getEntries() { return entries; } + } + + private static final class SavedFeature extends IForgeRegistryEntry.Impl + implements IFeature { + @Override public void generate(World world, IChunkGenerator generator, + IChunkProvider provider, GeneratorParameters parameters) { } + @Override public void generate(World world, IChunkGenerator generator, + IChunkProvider provider, ISpawnEntry spawn, ChunkPos pos) { } + @Override public void setRandom(Random random) { } + @Override public JsonObject getDefaultParameters() { return new JsonObject(); } + } + + private static final class SavedFeatureAlias extends IForgeRegistryEntry.Impl + implements IFeature { + private final IFeature delegate; + SavedFeatureAlias(IFeature delegate) { this.delegate = delegate; } + @Override public void generate(World world, IChunkGenerator generator, + IChunkProvider provider, GeneratorParameters parameters) { + delegate.generate(world, generator, provider, parameters); + } + @Override public void generate(World world, IChunkGenerator generator, + IChunkProvider provider, ISpawnEntry spawn, ChunkPos pos) { + delegate.generate(world, generator, provider, spawn, pos); + } + @Override public void setRandom(Random random) { delegate.setRandom(random); } + @Override public JsonObject getDefaultParameters() { return delegate.getDefaultParameters(); } + } + + private static final class LegacySpawnEntry implements ISpawnEntry { + private final String name; private final IDimensionList dimensions; private final BiomeLocation biomes; + private final boolean enabled, retrogen; private final IReplacementEntry replacement; private final IFeatureEntry feature; private final IBlockList blocks; + LegacySpawnEntry(String name, IDimensionList dimensions, BiomeLocation biomes, boolean enabled, + boolean retrogen, IReplacementEntry replacement, IFeatureEntry feature, IBlockList blocks) { + this.name = name; this.dimensions = dimensions; this.biomes = biomes; this.enabled = enabled; this.retrogen = retrogen; + this.replacement = replacement; this.feature = feature; this.blocks = blocks; + } + @Override public boolean isEnabled() { return enabled; } + @Override public boolean isRetrogen() { return retrogen; } + @Override public String getSpawnName() { return name; } + @Override public boolean dimensionAllowed(int dimension) { return dimensions == null || dimensions.matches(dimension); } + @Override public boolean biomeAllowed(ResourceLocation biome) { Biome value = ForgeRegistries.BIOMES.getValue(biome); return value != null && biomeAllowed(value); } + @Override public boolean biomeAllowed(Biome biome) { return biomes == null || biomes.matches(biome); } + @Override public IFeatureEntry getFeature() { return feature; } + @Override public OreSpawnBlockMatcher getMatcher() { return replacement == null ? new OreSpawnBlockMatcher(Blocks.STONE.getDefaultState()) : replacement.getMatcher(); } + @Override public IBlockList getBlocks() { return blocks; } + @Override public IDimensionList getDimensions() { return dimensions; } + @Override public BiomeLocation getBiomes() { return biomes; } + @Override public void generate(Random random, World world, IChunkGenerator generator, IChunkProvider provider, ChunkPos pos) { + if (feature == null || feature.getFeature() == null) return; feature.getFeature().setRandom(random); + feature.getFeature().generate(world, generator, provider, this, pos); + } + + JsonObject toLegacyJson(LegacyApi api) { + if (feature == null || !(blocks instanceof LegacyBlockList)) return null; + String featureName = feature.getFeatureName(); + if (featureName == null) return null; + String normalized = normalizePattern(featureName.contains(":") + ? featureName.substring(featureName.indexOf(':') + 1) : featureName); + if (!("default".equals(normalized) || "vein".equals(normalized) + || "normal_cloud".equals(normalized) || "precision".equals(normalized) + || "clusters".equals(normalized) || "underfluids".equals(normalized))) return null; + if (!(dimensions instanceof LegacyDimensionList)) return null; + LegacyDimensionList dimensionList = (LegacyDimensionList) dimensions; + if ((!dimensionList.all && dimensionList.allowed.isEmpty()) + || (dimensionList.all && !dimensionList.overworldOnly) + || !dimensionList.denied.isEmpty()) return null; + + JsonObject result = new JsonObject(); + result.addProperty("enabled", enabled); + result.addProperty("retrogen", retrogen); + result.addProperty("feature", normalized); + result.addProperty("replaces", replacement == null || replacement.getRegistryName() == null + ? "default" : replacement.getRegistryName().toString()); + JsonArray dimensionIds = new JsonArray(); + if (!dimensionList.all) for (Integer id : dimensionList.allowed) zone.moddev.mc.orespawn.util.JsonCopies.add(dimensionIds, id); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "dimensions", dimensionIds); + JsonObject biomeFilter = new JsonObject(); + JsonArray included = new JsonArray(); + JsonArray excluded = new JsonArray(); + if (biomes instanceof LegacyBiomeLocation) { + LegacyBiomeLocation location = (LegacyBiomeLocation) biomes; + for (Biome biome : location.included) if (biome.getRegistryName() != null) zone.moddev.mc.orespawn.util.JsonCopies.add(included, biome.getRegistryName().toString()); + for (String type : location.includedTypes) zone.moddev.mc.orespawn.util.JsonCopies.add(included, type); + for (Biome biome : location.excluded) if (biome.getRegistryName() != null) zone.moddev.mc.orespawn.util.JsonCopies.add(excluded, biome.getRegistryName().toString()); + for (String type : location.excludedTypes) zone.moddev.mc.orespawn.util.JsonCopies.add(excluded, type); + } + biomeFilter.add("includes", included); + biomeFilter.add("excludes", excluded); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "biomes", biomeFilter); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "parameters", new JsonParser().parse(feature.getFeatureParameters().toString())); + JsonArray outputs = new JsonArray(); + for (IBlockDefinition definition : ((LegacyBlockList) blocks).blocks) { + IBlockState state = definition.getBlock(); + if (state == null || state.getBlock().getRegistryName() == null) continue; + JsonObject output = new JsonObject(); + output.addProperty("name", state.getBlock().getRegistryName().toString()); + int metadata = state.getBlock().getMetaFromState(state); + if (metadata != 0) output.addProperty("metadata", metadata); + output.addProperty("chance", definition.getChance()); + zone.moddev.mc.orespawn.util.JsonCopies.add(outputs, output); + } + if (outputs.size() == 0) return null; + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "blocks", outputs); + if (replacement != null && replacement.getRegistryName() != null) { + api.rememberReplacement(replacement.getRegistryName().toString(), replacement.getEntries()); + } + return result; + } + } + + private static final class LegacyFlags { + private static final Pattern PROPERTY = Pattern.compile("^[BIS]:\\\"?([^\\\"=]+)\\\"?=(.*)$"); + boolean replaceVanilla; + boolean disableStandard; + boolean retrogen; + boolean forceRetrogen; + boolean flatBedrock; + boolean retrogenBedrock; + int bedrockLayers = 1; + final List nonstandardHosts = new ArrayList<>(); + + static LegacyFlags read(Path path) { + LegacyFlags result = new LegacyFlags(); + if (!Files.isRegularFile(path)) return result; + try { + for (String raw : Files.readAllLines(path, StandardCharsets.UTF_8)) { + Matcher match = PROPERTY.matcher(raw.trim()); + if (!match.matches()) continue; + String key = match.group(1).trim(); String value = match.group(2).trim(); + if ("Replace Vanilla Oregen".equals(key)) result.replaceVanilla = Boolean.parseBoolean(value); + else if ("disable_standard_ore_generation".equals(key)) result.disableStandard = Boolean.parseBoolean(value); + else if ("Retrogen".equals(key)) result.retrogen = Boolean.parseBoolean(value); + else if ("Force Retrogen".equals(key) || "force_ore_generation".equals(key)) result.forceRetrogen |= Boolean.parseBoolean(value); + else if ("Flatten Bedrock".equals(key)) result.flatBedrock = Boolean.parseBoolean(value); + else if ("Retrogen Flat Bedrock".equals(key)) result.retrogenBedrock = Boolean.parseBoolean(value); + else if ("Bedrock Thickness".equals(key)) { + try { result.bedrockLayers = clamp(Integer.parseInt(value), 1, 4); } + catch (NumberFormatException ignored) { REPORT.add("legacy_flag_clamped=Bedrock Thickness:" + value); } + } else if ("nonstandard_spawn_blocks".equals(key)) { + for (String host : value.split(",")) if (!host.trim().isEmpty()) result.nonstandardHosts.add(host.trim()); + } else if ("ignore_missing_blocks".equals(key)) { + REPORT.add("legacy_flag_preserved=ignore_missing_blocks:" + value); + } + } + REPORT.add("legacy_flags=manage_vanilla:" + result.replaceVanilla + + ",suppress_all:" + result.disableStandard + ",retrogen:" + result.retrogen + + ",force:" + result.forceRetrogen + ",flat_bedrock:" + result.flatBedrock + + ",retrogen_bedrock:" + result.retrogenBedrock + ",layers:" + result.bedrockLayers); + } catch (IOException failure) { + REPORT.add("legacy_flags_failed=" + failure.getClass().getSimpleName()); + } + return result; + } + } + + private static Object primitiveDefault(Class type) { + if (!type.isPrimitive()) return null; if (type == boolean.class) return false; if (type == char.class) return '\0'; + if (type == byte.class) return (byte) 0; if (type == short.class) return (short) 0; if (type == int.class) return 0; + if (type == long.class) return 0L; if (type == float.class) return 0F; return 0D; + } + + private static final class LegacyBuilderLogic implements BuilderLogic { + private final String name; private final Map dimensions = new LinkedHashMap<>(); + LegacyBuilderLogic(String name) { this.name = name; } + @Override public DimensionBuilder newDimensionBuilder(String value) { + String normalized = value == null ? "+" : value.trim().toLowerCase(java.util.Locale.ROOT); + if ("+".equals(normalized)) return LegacyDimensionBuilder322.overworldOnly(); + if ("overworld".equals(normalized) || "the_overworld".equals(normalized)) return newDimensionBuilder(0); + if ("nether".equals(normalized) || "the_nether".equals(normalized)) return newDimensionBuilder(-1); + if ("end".equals(normalized) || "the_end".equals(normalized)) return newDimensionBuilder(1); + return newDimensionBuilder(Integer.parseInt(normalized)); + } + @Override public DimensionBuilder newDimensionBuilder(int id) { return LegacyDimensionBuilder322.specific(id); } + @Override public DimensionBuilder newDimensionBuilder() { return LegacyDimensionBuilder322.allDimensions(); } + @Override public BuilderLogic create(DimensionBuilder... values) { for (DimensionBuilder value : values) if (value instanceof LegacyDimensionBuilder322) dimensions.put(((LegacyDimensionBuilder322) value).id, value); return this; } + @Override public DimensionBuilder getDimension(String value) { + String normalized = value == null ? "+" : value.trim().toLowerCase(java.util.Locale.ROOT); + if ("+".equals(normalized)) return getDimension(Integer.MIN_VALUE); + if ("overworld".equals(normalized) || "the_overworld".equals(normalized)) return getDimension(0); + if ("nether".equals(normalized) || "the_nether".equals(normalized)) return getDimension(-1); + if ("end".equals(normalized) || "the_end".equals(normalized)) return getDimension(1); + return getDimension(Integer.parseInt(normalized)); + } + @Override public DimensionBuilder getDimension(int id) { return dimensions.get(id); } + @Override public ImmutableMap getAllDimensions() { return ImmutableMap.copyOf(dimensions); } + void contributeProviderRules(String owner, JsonObject target, + Set translated, LegacyApi api) { + int ordinal = 0; + for (DimensionBuilder dimension : dimensions.values()) { + if (!(dimension instanceof LegacyDimensionBuilder322)) continue; + LegacyDimensionBuilder322 legacyDimension = (LegacyDimensionBuilder322) dimension; + for (SpawnBuilder spawn : dimension.getAllSpawns()) { + if (!(spawn instanceof LegacySpawnBuilder322)) continue; + LegacySpawnBuilder322 legacySpawn = (LegacySpawnBuilder322) spawn; + String ruleName = legacySpawn.name == null || legacySpawn.name.trim().isEmpty() + ? safe(name) + "_spawn_" + ordinal : safe(legacySpawn.name); + while (target.has(ruleName)) ruleName = safe(legacySpawn.name) + "_" + (++ordinal); + String replacementName = owner + ":programmatic/" + safe(name) + "/" + ruleName; + JsonObject migrated = legacySpawn.toLegacyJson(legacyDimension, replacementName, api); + if (migrated == null) { + REPORT.add("programmatic_322_custom_scheduled=" + owner + ":" + name + ":" + ruleName); + } else { + target.add(ruleName, migrated); + translated.add(legacySpawn); + REPORT.add("programmatic_322_provider_rule=" + owner + ":" + name + ":" + ruleName); + } + ordinal++; + } + } + } + void generate(Random random, ChunkPos pos, World world, IChunkGenerator generator, + IChunkProvider provider, Set translated) { + int id = world.provider.getDimension(); + for (DimensionBuilder dimension : dimensions.values()) { + if (!(dimension instanceof LegacyDimensionBuilder322) + || !((LegacyDimensionBuilder322) dimension).matches(id)) continue; + for (SpawnBuilder spawn : dimension.getAllSpawns()) { + if (!(spawn instanceof LegacySpawnBuilder322) || !spawn.enabled() || (spawn.hasExtendedDimensions() && !spawn.extendedDimensionsMatch(id))) continue; + LegacySpawnBuilder322 legacy = (LegacySpawnBuilder322) spawn; + if (translated.contains(legacy)) continue; + IFeature feature = legacy.feature == null ? null : legacy.feature.getGenerator(); if (feature == null) continue; + feature.setRandom(random); feature.generate(world, generator, provider, + new GeneratorParameters(pos, legacy.ores, legacy.replacements, legacy.biomes, legacy.feature.getParameters())); + } + } + } + @Override public String toString() { return "LegacyBuilderLogic[" + name + "]"; } + } + + private static final class LegacyDimensionBuilder322 implements DimensionBuilder { + private final int id; + private final boolean overworldOnly; + private final boolean allDimensions; + private final List spawns = new ArrayList<>(); + private LegacyDimensionBuilder322(int id, boolean overworldOnly, boolean allDimensions) { + this.id = id; this.overworldOnly = overworldOnly; this.allDimensions = allDimensions; + } + static LegacyDimensionBuilder322 specific(int id) { return new LegacyDimensionBuilder322(id, false, false); } + static LegacyDimensionBuilder322 overworldOnly() { return new LegacyDimensionBuilder322(Integer.MIN_VALUE, true, false); } + static LegacyDimensionBuilder322 allDimensions() { return new LegacyDimensionBuilder322(Integer.MIN_VALUE, false, true); } + boolean matches(int dimension) { + return allDimensions || (overworldOnly ? dimension != -1 && dimension != 1 : dimension == id); + } + @Override public SpawnBuilder newSpawnBuilder(String name) { return new LegacySpawnBuilder322(name); } + @Override public DimensionBuilder create(SpawnBuilder... values) { spawns.addAll(Arrays.asList(values)); return this; } + @Override public ImmutableList getSpawnByName(String name) { ImmutableList.Builder result = ImmutableList.builder(); for (SpawnBuilder spawn : spawns) if (spawn instanceof LegacySpawnBuilder322 && java.util.Objects.equals(name, ((LegacySpawnBuilder322) spawn).name)) result.add(spawn); return result.build(); } + @Override public ImmutableList getAllSpawns() { return ImmutableList.copyOf(spawns); } + } + + private static final class LegacySpawnBuilder322 implements SpawnBuilder { + private final String name; private BiomeLocation biomes = LegacyBiomeLocation.all(); private FeatureBuilder feature; + private List replacements = Collections.singletonList(Blocks.STONE.getDefaultState()); private final OreList ores = new OreList(); + private List oreBuilders = new ArrayList<>(); private boolean enabled = true, retrogen; private int[] include, exclude; + LegacySpawnBuilder322(String name) { this.name = name; } + @Override public FeatureBuilder newFeatureBuilder(String featureName) { return new LegacyFeatureBuilder322(featureName); } + @Override public BiomeBuilder newBiomeBuilder() { return new LegacyBiomeBuilder322(); } + @Override public OreBuilder newOreBuilder() { return new LegacyOreBuilder(); } + @Override public SpawnBuilder create(BiomeBuilder biomes, FeatureBuilder feature, List replacements, OreBuilder... ores) { + return create(biomes, feature, replacements, null, ores); + } + @Override public SpawnBuilder create(BiomeBuilder biomes, FeatureBuilder feature, List replacements, JsonObject exDim, OreBuilder... values) { + this.biomes = biomes.getBiomes(); this.feature = feature; this.replacements = new ArrayList<>(replacements); + this.oreBuilders = Arrays.asList(values); this.ores.build(oreBuilders); + if (exDim != null) { this.include = ints(exDim.get("includes")); this.exclude = ints(exDim.get("excludes")); } + return this; + } + @Override public BiomeLocation getBiomes() { return biomes; } + @Override public ImmutableList getOres() { return ImmutableList.copyOf(oreBuilders); } + @Override public ImmutableList getReplacementBlocks() { return ImmutableList.copyOf(replacements); } + @Override public FeatureBuilder getFeatureGen() { return feature; } + @Override public OreBuilder getRandomOre(Random random) { return ores.getRandomOre(random); } + @Override public OreList getOreSpawns() { return ores; } + @Override public boolean enabled() { return enabled; } + @Override public void enabled(boolean value) { enabled = value; } + @Override public boolean retrogen() { return retrogen; } + @Override public void retrogen(boolean value) { retrogen = value; } + @Override public boolean hasExtendedDimensions() { return include != null || exclude != null; } + @Override public boolean extendedDimensionsMatch(int dimension) { return (include == null || include.length == 0 || contains(include, dimension)) && (exclude == null || !contains(exclude, dimension)); } + + JsonObject toLegacyJson(LegacyDimensionBuilder322 dimension, String replacementName, LegacyApi api) { + if (feature == null || feature.getFeatureName() == null || oreBuilders.isEmpty()) return null; + String featureName = feature.getFeatureName(); + String normalized = normalizePattern(featureName.contains(":") + ? featureName.substring(featureName.indexOf(':') + 1) : featureName); + if (!("default".equals(normalized) || "vein".equals(normalized) + || "normal_cloud".equals(normalized) || "precision".equals(normalized) + || "clusters".equals(normalized) || "underfluids".equals(normalized))) return null; + + JsonArray dimensions = representableDimensions(dimension); + if (dimensions == null) return null; + JsonObject result = new JsonObject(); + result.addProperty("enabled", enabled); + result.addProperty("retrogen", retrogen); + result.addProperty("feature", normalized); + result.addProperty("replaces", replacementName); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "dimensions", dimensions); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "biomes", biomes == null ? LegacyBiomeLocation.all().serialize() : biomes.serialize()); + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "parameters", feature.getParameters() == null + ? new JsonObject() : new JsonParser().parse(feature.getParameters().toString())); + + JsonArray outputs = new JsonArray(); + for (OreBuilder ore : oreBuilders) { + IBlockState state = ore.getOre(); + if (state == null || state.getBlock().getRegistryName() == null) continue; + JsonObject output = new JsonObject(); + output.addProperty("name", state.getBlock().getRegistryName().toString()); + int metadata = state.getBlock().getMetaFromState(state); + if (metadata != 0) output.addProperty("metadata", metadata); + output.addProperty("chance", Math.max(0, ore.getChance())); + zone.moddev.mc.orespawn.util.JsonCopies.add(outputs, output); + } + if (outputs.size() == 0) return null; + zone.moddev.mc.orespawn.util.JsonCopies.add(result, "blocks", outputs); + api.rememberReplacement(replacementName, replacements); + return result; + } + + private JsonArray representableDimensions(LegacyDimensionBuilder322 dimension) { + Set selected = new LinkedHashSet<>(); + if (!dimension.allDimensions && !dimension.overworldOnly) { + if (!extendedDimensionsMatch(dimension.id)) return null; + selected.add(dimension.id); + } else if (include != null && include.length > 0) { + for (int id : include) { + if (dimension.overworldOnly && (id == -1 || id == 1)) continue; + if (exclude == null || !contains(exclude, id)) selected.add(id); + } + if (selected.isEmpty()) return null; + } else if (dimension.overworldOnly) { + for (int denied : exclude == null ? new int[0] : exclude) { + if (denied != -1 && denied != 1) return null; + } + return new JsonArray(); + } else { + // OS4's declarative selector cannot losslessly express every registered + // dimension minus an open-ended blacklist. Keep this on the one legacy scheduler. + return null; + } + JsonArray result = new JsonArray(); + for (Integer id : selected) zone.moddev.mc.orespawn.util.JsonCopies.add(result, id); + return result; + } + } + + private static final class LegacyFeatureBuilder322 implements FeatureBuilder { + private String name; private IFeature feature; private JsonObject parameters = new JsonObject(); + LegacyFeatureBuilder322(String name) { this.name = name; feature = FEATURES.getFeature(name); } + @Override public FeatureBuilder setGenerator(String name) { this.name = name; feature = FEATURES.getFeature(name); return this; } + @Override public FeatureBuilder addParameter(String key, boolean value) { parameters.addProperty(key, value); return this; } + @Override public FeatureBuilder addParameter(String key, int value) { parameters.addProperty(key, value); return this; } + @Override public FeatureBuilder addParameter(String key, float value) { parameters.addProperty(key, value); return this; } + @Override public FeatureBuilder addParameter(String key, String value) { parameters.addProperty(key, value); return this; } + @Override public FeatureBuilder setParameters(JsonObject value) { parameters = new JsonParser().parse(value.toString()).getAsJsonObject(); return this; } + @Override public FeatureBuilder setDefaultParameters() { if (feature != null) parameters = new JsonParser().parse(feature.getDefaultParameters().toString()).getAsJsonObject(); return this; } + @Override public IFeature getGenerator() { return feature; } + @Override public JsonObject getParameters() { return parameters; } + @Override public String getFeatureName() { return name; } + } + + private static final class LegacyBiomeBuilder322 implements BiomeBuilder { + private final Set included = new LinkedHashSet<>(), excluded = new LinkedHashSet<>(); + private final Set includedTypes = new LinkedHashSet<>(), excludedTypes = new LinkedHashSet<>(); + private BiomeLocation value; + @Override public BiomeBuilder whitelistBiome(Biome biome) { zone.moddev.mc.orespawn.util.JsonCopies.add(included, biome); return this; } + @Override public BiomeBuilder whitelistBiomeByName(String name) { Biome biome = validId(name) ? ForgeRegistries.BIOMES.getValue(new ResourceLocation(name)) : null; if (biome != null) zone.moddev.mc.orespawn.util.JsonCopies.add(included, biome); return this; } + @Override public BiomeBuilder whitelistBiomeByDictionary(String type) { includedTypes.add(type.toUpperCase(java.util.Locale.ROOT)); return this; } + @Override public BiomeBuilder blacklistBiome(Biome biome) { zone.moddev.mc.orespawn.util.JsonCopies.add(excluded, biome); return this; } + @Override public BiomeBuilder blacklistBiomeByName(String name) { Biome biome = validId(name) ? ForgeRegistries.BIOMES.getValue(new ResourceLocation(name)) : null; if (biome != null) zone.moddev.mc.orespawn.util.JsonCopies.add(excluded, biome); return this; } + @Override public BiomeBuilder blacklistBiomeByDictionary(String type) { excludedTypes.add(type.toUpperCase(java.util.Locale.ROOT)); return this; } + @Override public BiomeBuilder setFromBiomeLocation(BiomeLocation biomes) { value = biomes; return this; } + @Override public BiomeLocation getBiomes() { return value == null + ? new LegacyBiomeLocation(included, excluded, includedTypes, excludedTypes, + included.isEmpty() && includedTypes.isEmpty()) : value; } + } + + private static final class LegacyOreBuilder implements OreBuilder { + private IBlockState ore; private int chance = 100; + @Override public OreBuilder setOre(String name) { ore = state(name, 0); return this; } + @Override public OreBuilder setOre(String name, String serializedState) { ore = state(name, metadata(name, serializedState)); return this; } + @Override public OreBuilder setOre(String name, int metadata) { ore = state(name, metadata); return this; } + @Override public OreBuilder setOre(Block block) { ore = block.getDefaultState(); return this; } + @Override public OreBuilder setOre(Block block, String serializedState) { ore = state(block, metadata(block.getRegistryName().toString(), serializedState)); return this; } + @Override public OreBuilder setOre(net.minecraft.item.Item item, int metadata) { ore = state(Block.getBlockFromItem(item), metadata); return this; } + @Override public OreBuilder setOre(net.minecraft.item.ItemStack item) { ore = state(Block.getBlockFromItem(item.getItem()), item.getMetadata()); return this; } + @Override public OreBuilder setOre(String name, String serializedState, int chance) { setOre(name, serializedState); return setChance(chance); } + @Override public OreBuilder setOre(String name, int metadata, int chance) { setOre(name, metadata); return setChance(chance); } + @Override public OreBuilder setOre(Block block, String serializedState, int chance) { setOre(block, serializedState); return setChance(chance); } + @Override public OreBuilder setOre(net.minecraft.item.Item item, int metadata, int chance) { setOre(item, metadata); return setChance(chance); } + @Override public OreBuilder setOre(net.minecraft.item.ItemStack item, int chance) { setOre(item); return setChance(chance); } + @Override public OreBuilder setChance(int value) { chance = value; return this; } + @Override public IBlockState getOre() { return ore; } + @Override public int getChance() { return chance; } + } + + private static int[] ints(JsonElement element) { if (element == null || !element.isJsonArray()) return null; int[] result = new int[element.getAsJsonArray().size()]; for (int i = 0; i < result.length; i++) result[i] = element.getAsJsonArray().get(i).getAsInt(); return result; } + private static boolean contains(int[] values, int target) { for (int value : values) if (value == target) return true; return false; } +} diff --git a/src/main/java/com/mcmoddev/orespawn/data/Config.java b/src/main/java/com/mcmoddev/orespawn/data/Config.java deleted file mode 100644 index 1b5decef..00000000 --- a/src/main/java/com/mcmoddev/orespawn/data/Config.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.mcmoddev.orespawn.data; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.FileSystems; -import java.nio.file.Path; -import java.util.List; - -import org.apache.commons.io.FileUtils; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonParser; -import com.mcmoddev.orespawn.OreSpawn; -import com.google.gson.JsonPrimitive; - -import java.util.ArrayList; -import java.util.HashMap; - -import net.minecraft.crash.CrashReport; -import net.minecraftforge.common.config.Configuration; - -public class Config { - private static Configuration configuration; - - private Config() { - } - - public static void loadConfig() { - configuration = new Configuration(new File(Constants.CONFIG_FILE)); - - // Load our Boolean Values - boolVals.put(Constants.RETROGEN_KEY, configuration.getBoolean(Constants.RETROGEN_KEY, Configuration.CATEGORY_GENERAL, false, "Do we have Retrogen active and generating anything different from the last run in already existing chunks ?")); - boolVals.put(Constants.FORCE_RETROGEN_KEY, configuration.getBoolean(Constants.FORCE_RETROGEN_KEY, Configuration.CATEGORY_GENERAL, false, "Force all chunks to retrogen regardless of anything else")); - boolVals.put(Constants.REPLACE_VANILLA_OREGEN, configuration.getBoolean(Constants.REPLACE_VANILLA_OREGEN, Configuration.CATEGORY_GENERAL, false, "Replace vanilla ore-generation entirely")); - boolVals.put(Constants.FLAT_BEDROCK, configuration.getBoolean(Constants.FLAT_BEDROCK, Configuration.CATEGORY_GENERAL, false, "Flatten the bedrock during world generation")); - boolVals.put(Constants.RETRO_BEDROCK, configuration.getBoolean(Constants.RETRO_BEDROCK, Configuration.CATEGORY_GENERAL, false, "Retroactively flatten bedrock")); - intVals.put(Constants.BEDROCK_LAYERS, configuration.getInt(Constants.BEDROCK_LAYERS, Configuration.CATEGORY_GENERAL, 1, 1, 4, "How thick should the shell of bedrock be?")); - knownKeys.add(Constants.RETROGEN_KEY); - knownKeys.add(Constants.FORCE_RETROGEN_KEY); - knownKeys.add(Constants.REPLACE_VANILLA_OREGEN); - knownKeys.add(Constants.KNOWN_MODS); - knownKeys.add(Constants.FLAT_BEDROCK); - knownKeys.add(Constants.RETRO_BEDROCK); - knownKeys.add(Constants.BEDROCK_LAYERS); - - loadExtractedConfigs(); - } - - private static void loadExtractedConfigs() { - Path p = FileSystems.getDefault().getPath("config", "orespawn3", "sysconf", "known-configs.json"); - - if (!p.toFile().exists()) { - return; - } - - File in = p.toFile(); - String rawData; - - try { - rawData = FileUtils.readFileToString(in, Charset.defaultCharset()); - } catch (IOException e) { - return; - } - - if (rawData.isEmpty()) { - return; - } - - JsonArray data = new JsonParser().parse(rawData).getAsJsonArray(); - data.forEach(item -> addKnownMod(item.getAsString())); - } - - public static List getKnownMods() { - return ImmutableList.copyOf(extractedConfigs); - } - - public static void addKnownMod(String modId) { - extractedConfigs.add(modId); - } - - public static boolean getBoolean(String keyname) { - if (knownKeys.contains(keyname) && boolVals.containsKey(keyname)) { - return boolVals.get(keyname); - } - - return false; - } - - public static String getString(String keyname) { - if (knownKeys.contains(keyname) && stringVals.containsKey(keyname)) { - return stringVals.get(keyname); - } - - return ""; - } - - public static int getInt(String keyname) { - if (knownKeys.contains(keyname) && intVals.containsKey(keyname)) { - return intVals.get(keyname); - } - - return 0; - } - - public static float getFloat(String keyname) { - if (knownKeys.contains(keyname) && floatVals.containsKey(keyname)) { - return floatVals.get(keyname); - } - - return 0.0f; - } - - public static void saveConfig() { - if (!extractedConfigs.isEmpty()) { - saveKnownConfigs(); - } - - configuration.save(); - } - - private static void saveKnownConfigs() { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - Path p = FileSystems.getDefault().getPath("config", "orespawn3", "sysconf", "known-configs.json"); - - if (!p.toFile().getParentFile().exists()) { - p.toFile().mkdirs(); - } - - File in = p.toFile(); - - JsonArray data = new JsonArray(); - - extractedConfigs.forEach(val -> data.add(new JsonPrimitive(val))); - - try { - FileUtils.writeStringToFile(in, gson.toJson(data), Charset.defaultCharset()); - } catch (IOException e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed saving list of already extracted mod configs"); - report.getCategory().addCrashSection("OreSpawn Version", Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - } - - private static final HashMap boolVals = new HashMap<>(); - private static final HashMap stringVals = new HashMap<>(); - private static final HashMap intVals = new HashMap<>(); - private static final HashMap floatVals = new HashMap<>(); - private static final List knownKeys = new ArrayList<>(); - private static final List extractedConfigs = new ArrayList<>(); -} diff --git a/src/main/java/com/mcmoddev/orespawn/data/Constants.java b/src/main/java/com/mcmoddev/orespawn/data/Constants.java deleted file mode 100644 index afd88567..00000000 --- a/src/main/java/com/mcmoddev/orespawn/data/Constants.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.mcmoddev.orespawn.data; - -public class Constants { - public static final String MODID = "orespawn"; - public static final String NAME = "MMD OreSpawn"; - public static final String VERSION = "3.2.2"; - public static final String RETROGEN_KEY = "Retrogen"; - public static final String CONFIG_FILE = "config/orespawn.cfg"; - public static final String FORCE_RETROGEN_KEY = "Force Retrogen"; - public static final String CHUNK_TAG_NAME = "MMD OreSpawn Data"; - public static final String ORE_TAG = "ores"; - public static final String FEATURES_TAG = "features"; - public static final String REPLACE_VANILLA_OREGEN = "Replace Vanilla Oregen"; - public static final String OVERWORLD = "overworld"; - public static final String THE_OVERWORLD = "the overworld"; - public static final String NETHER = "nether"; - public static final String THE_NETHER = "the nether"; - public static final String END = "end"; - public static final String THE_END = "the end"; - public static final String DEFAULT_GEN = "default"; - public static final String VEIN_GEN = "vein"; - public static final String NORMAL_CLOUD = "normal-cloud"; - public static final String CLUSTERS = "clusters"; - public static final String CRASH_SECTION = "OreSpawn Version"; - public static final String KNOWN_MODS = "already-extracted"; - public static final String RETRO_BEDROCK = "Retrogen Flat Bedrock"; - public static final String FLAT_BEDROCK = "Flatten Bedrock"; - public static final String RETRO_BEDROCK_TAG = "retro-bedrock"; - public static final String BEDROCK_LAYERS = "Bedrock Thickness"; - public static final String ORESPAWN_VERSION_CRASH_MESSAGE = "OreSpawn Version"; - public static final String PRECISION = "precision"; - - public final class FormatBits { - - private FormatBits() {} - - public static final String MAX_SPREAD = "maxSpread"; - public static final String MEDIAN_SIZE = "medianSize"; - public static final String MIN_HEIGHT = "minHeight"; - public static final String MAX_HEIGHT = "maxHeight"; - public static final String VARIATION = "variation"; - public static final String FREQUENCY = "frequency"; - public static final String NODE_SIZE = "size"; - public static final String ATTEMPTS = "attempts"; - public static final String LENGTH = "length"; - public static final String WANDER = "wander"; - public static final String NODE_COUNT = "numObjects"; - public static final String ATTEMPTS_MIN = "minAttempts"; - public static final String ATTEMPTS_MAX = "maxAttempts"; - } - - public final class FileBits { - private FileBits() {} - - public static final String CONFIG_DIR = "config"; - public static final String OS3 = "orespawn3"; - public static final String SYSCONF = "sysconf"; - public static final String PRESETS = "presets.json"; - } - - public final class ConfigNames { - private ConfigNames() {} - public static final String DEFAULT = "default"; - public static final String STATE_NORMAL = "normal"; - public static final String DIMENSION = "dimension"; - public static final String ORES = "ores"; - public static final String DIMENSIONS = "dimensions"; - public static final String BLOCKID = "blockID"; - public static final String BLOCK = "block"; - public static final String BLOCKS = "blocks"; - public static final String CHANCE = "chance"; - public static final String METADATA = "metaData"; - public static final String BIOMES = "biomes"; - public static final String STATE = "state"; - public static final String REPLACEMENT = "replace_block"; - public static final String REPLACEMENT_V2 = "replaces"; - public static final String FEATURE = "feature"; - public static final String PARAMETERS = "parameters"; - public static final String FILE_VERSION = "version"; - public static final String BLOCK_V2 = "name"; - - public final class V2 { - private V2() {} - public static final String ENABLED = "enabled"; - public static final String RETROGEN = "retrogen"; - public static final String REPLACES = "replaces"; - public static final String GENERATOR = "generator"; - public static final String MINIMUM = "minimum"; - public static final String MAXIMUM = "maximum"; - } - - public final class BiomeStuff { - private BiomeStuff() {} - public static final String WHITELIST = "includes"; - public static final String BLACKLIST = "excludes"; - } - public final class DefaultFeatureProperties { - private DefaultFeatureProperties() {} - public static final String SIZE = "size"; - public static final String VARIATION = "variation"; - public static final String FREQUENCY = "frequency"; - public static final String MAXHEIGHT = "maxHeight"; - public static final String MINHEIGHT = "minHeight"; - } - public final class DimensionStuff { - private DimensionStuff() {} - public static final String INCLUDE = "includes"; - public static final String EXCLUDE = "excludes"; - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/data/FeatureRegistry.java b/src/main/java/com/mcmoddev/orespawn/data/FeatureRegistry.java index c45879fc..fe612c4b 100644 --- a/src/main/java/com/mcmoddev/orespawn/data/FeatureRegistry.java +++ b/src/main/java/com/mcmoddev/orespawn/data/FeatureRegistry.java @@ -1,166 +1,28 @@ package com.mcmoddev.orespawn.data; import java.io.File; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.nio.charset.Charset; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -import java.util.Map.Entry; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringEscapeUtils; -import org.apache.commons.codec.CharEncoding; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mcmoddev.orespawn.OreSpawn; import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.impl.features.ClusterGenerator; -import com.mcmoddev.orespawn.impl.features.DefaultFeatureGenerator; -import com.mcmoddev.orespawn.impl.features.NormalCloudGenerator; -import com.mcmoddev.orespawn.impl.features.PrecisionGenerator; -import com.mcmoddev.orespawn.impl.features.VeinGenerator; -import net.minecraft.crash.CrashReport; +import net.minecraft.util.ResourceLocation; +/** Union of the two published OS3 feature-registry facades. */ public class FeatureRegistry { - private static final String ORE_SPAWN_VERSION = "OreSpawn Version"; - private Map features; - private Map featuresInverse; - - public FeatureRegistry() { - features = new HashMap<>(); - featuresInverse = new HashMap<>(); - IFeature defaultGen = new DefaultFeatureGenerator(); - features.put(Constants.DEFAULT_GEN, defaultGen); - featuresInverse.put(defaultGen, Constants.DEFAULT_GEN); - IFeature veinGen = new VeinGenerator(); - features.put(Constants.VEIN_GEN, veinGen); - featuresInverse.put(veinGen, Constants.VEIN_GEN); - IFeature normalCloudGen = new NormalCloudGenerator(); - features.put(Constants.NORMAL_CLOUD, normalCloudGen); - featuresInverse.put(normalCloudGen, Constants.NORMAL_CLOUD); - IFeature clusterGen = new ClusterGenerator(); - features.put(Constants.CLUSTERS, clusterGen); - featuresInverse.put(clusterGen, Constants.CLUSTERS); - IFeature precision = new PrecisionGenerator(); - features.put(Constants.PRECISION, precision); - featuresInverse.put(precision, Constants.PRECISION); - } - - public Map getFeatures() { - return Collections.unmodifiableMap(features); - } - - public String getFeatureName(IFeature feature) { - if (this.hasFeature(feature)) { - return this.featuresInverse.get(feature); - } else { - return Constants.DEFAULT_GEN; - } - } - - public IFeature getFeature(String name) { - if (this.hasFeature(name)) { - return this.features.get(name); - } else { - return this.features.get(Constants.DEFAULT_GEN); - } - } - - public boolean hasFeature(String name) { - return features.containsKey(name); - } - - public boolean hasFeature(IFeature feature) { - return featuresInverse.containsKey(feature); - } - - public void addFeature(String name, IFeature feature) { - this.addFeature(name, feature.getClass().getName()); - } - - public void addFeature(JsonObject entry) { - this.addFeature(entry.get("name").getAsString(), entry.get("class").getAsString()); - } - - public void addFeature(String name, String className) { - IFeature feature = getInstance(className); - - if (feature != null && !features.containsKey(name)) { - features.put(name, feature); - featuresInverse.put(feature, name); - } - } - - private IFeature getInstance(String className) { - Class featureClazz; - Constructor featureCons; - IFeature feature; - - try { - featureClazz = Class.forName(className); - featureCons = featureClazz.getConstructor(); - feature = (IFeature)featureCons.newInstance(); - } catch (Exception e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed to load and instantiate an instance of the feature generator named " + className + " that was specified as a feature generator"); - report.getCategory().addCrashSection(ORE_SPAWN_VERSION, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - return null; - } - - return feature; - } - - public void loadFeaturesFile(File file) { - JsonParser parser = new JsonParser(); - String rawJson; - JsonArray elements; - - try { - rawJson = FileUtils.readFileToString(file, Charset.defaultCharset()); - } catch (IOException e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed reading config " + file.getName()); - report.getCategory().addCrashSection(ORE_SPAWN_VERSION, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - return; - } - - elements = parser.parse(rawJson).getAsJsonArray(); - - for (JsonElement elem : elements) { - this.addFeature(elem.getAsJsonObject()); - } - } - - public void writeFeatures(File file) { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - JsonArray root = new JsonArray(); - - if (!features.equals(Collections.emptyMap())) { - for (Entry feature : features.entrySet()) { - JsonObject entry = new JsonObject(); - entry.addProperty("name", feature.getKey()); - entry.addProperty("class", feature.getValue().getClass().getName()); - root.add(entry); - } - } - - String json = gson.toJson(root); - - try { - FileUtils.writeStringToFile(file, StringEscapeUtils.unescapeJson(json), CharEncoding.UTF_8); - } catch (IOException e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed writing config " + file.getName()); - report.getCategory().addCrashSection(ORE_SPAWN_VERSION, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - } + private final Map features = new LinkedHashMap<>(); + public Map getFeatures() { return Collections.unmodifiableMap(features); } + public String getFeatureName(IFeature feature) { for (Map.Entry e : features.entrySet()) if (e.getValue() == feature) return e.getKey(); return null; } + public IFeature getFeature(String name) { return features.get(name); } + public IFeature getFeature(ResourceLocation name) { return getFeature(name.toString()); } + public boolean hasFeature(String name) { return features.containsKey(name); } + public boolean hasFeature(ResourceLocation name) { return hasFeature(name.toString()); } + public boolean hasFeature(IFeature feature) { return features.containsValue(feature); } + public void addFeature(String name, IFeature feature) { if (features.putIfAbsent(name, feature) != null) throw new IllegalArgumentException("Duplicate OS3 feature " + name); } + public void addFeature(JsonObject feature) { } + public void addFeature(String name, String className) { try { addFeature(name, (IFeature) Class.forName(className).newInstance()); } catch (ReflectiveOperationException e) { throw new IllegalArgumentException(e); } } + public void loadFeaturesFile(File file) { } + public void writeFeatures(File file) { } } diff --git a/src/main/java/com/mcmoddev/orespawn/data/PresetsStorage.java b/src/main/java/com/mcmoddev/orespawn/data/PresetsStorage.java new file mode 100644 index 00000000..18794801 --- /dev/null +++ b/src/main/java/com/mcmoddev/orespawn/data/PresetsStorage.java @@ -0,0 +1,23 @@ +package com.mcmoddev.orespawn.data; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** Deprecated OS3 3.3 preset snapshot; migration owns file parsing. */ +public class PresetsStorage { + private final Map> storage = new LinkedHashMap<>(); + public void setSymbolSection(String symbol, String section, JsonElement value) { + storage.computeIfAbsent(symbol, key -> new LinkedHashMap<>()).put(section, new JsonParser().parse(value.toString())); + } + public JsonElement getSymbolSection(String symbol, String section) { + Map values = storage.get(symbol); return values == null ? null : values.get(section); + } + public void copy(PresetsStorage source) { clear(); source.storage.forEach((s, values) -> values.forEach((k, v) -> setSymbolSection(s, k, v))); } + public void clear() { storage.clear(); } + public void load(Path path) { } + public JsonElement get(String symbol) { return storage.containsKey(symbol) ? new com.google.gson.JsonObject() : null; } +} diff --git a/src/main/java/com/mcmoddev/orespawn/data/ReplacementsRegistry.java b/src/main/java/com/mcmoddev/orespawn/data/ReplacementsRegistry.java deleted file mode 100644 index 9861260d..00000000 --- a/src/main/java/com/mcmoddev/orespawn/data/ReplacementsRegistry.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.mcmoddev.orespawn.data; - -import com.mcmoddev.orespawn.util.StateUtil; -import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.common.registry.ForgeRegistries; -import net.minecraftforge.oredict.OreDictionary; - -import java.util.*; - -import static java.util.Arrays.asList; - -public class ReplacementsRegistry { - private static Map blocks = new HashMap<>(); - - private ReplacementsRegistry() { - } - - @SuppressWarnings("deprecation") - public static List getDimensionDefault(int dimension) { - String[] names = { "minecraft:netherrack", "minecraft:stone", "minecraft:end_stone" }; - - if (dimension < -1 || dimension > 1 || dimension == 0) { - List rv = new ArrayList<>(); - - for (ItemStack iS : OreDictionary.getOres("stone")) { - rv.add(Block.getBlockFromItem(iS.getItem()).getStateFromMeta(iS.getMetadata())); - } - - return rv; - } - - return asList(ForgeRegistries.BLOCKS.getValue(new ResourceLocation(names[dimension + 1])).getDefaultState()); - } - - public static IBlockState getBlock(String name) { - return blocks.get(name); - } - - public static void addBlock(String name, String blockName, String blockState) { - Block nb = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(blockName)); - blocks.put(name, "default".equals(blockState) ? nb.getDefaultState() : StateUtil.deserializeState(nb, blockState)); - } - - public static Map getBlocks() { - return Collections.unmodifiableMap(blocks); - } - - public static void addBlock(String name, IBlockState state) { - blocks.put(name, state); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/data/VanillaOres.java b/src/main/java/com/mcmoddev/orespawn/data/VanillaOres.java deleted file mode 100644 index 56b3194f..00000000 --- a/src/main/java/com/mcmoddev/orespawn/data/VanillaOres.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.mcmoddev.orespawn.data; - - -import com.mcmoddev.orespawn.api.os3.*; -import com.mcmoddev.orespawn.api.plugin.IOreSpawnPlugin; -import com.mcmoddev.orespawn.api.plugin.OreSpawnPlugin; - -@OreSpawnPlugin(modid = "orespawn", resourcePath = "configs") -public class VanillaOres implements IOreSpawnPlugin { - - @Override - public void register(OS3API apiInterface) { - // nothing for us to do - all of our ores are in the - // jar and the code handles that - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/features/ClusterGenerator.java b/src/main/java/com/mcmoddev/orespawn/impl/features/ClusterGenerator.java deleted file mode 100644 index fe674576..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/features/ClusterGenerator.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.mcmoddev.orespawn.impl.features; - -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.FeatureBase; -import com.mcmoddev.orespawn.api.GeneratorParameters; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.util.OreList; -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.util.math.Vec3i; -import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkProvider; -import net.minecraft.world.chunk.IChunkGenerator; - -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -public class ClusterGenerator extends FeatureBase implements IFeature { - - private ClusterGenerator(Random rand) { - super(rand); - } - - public ClusterGenerator() { - this(new Random()); - } - - @Override - public void generate(World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider, - GeneratorParameters parameters) { - ChunkPos pos = parameters.getChunk(); - List blockReplace = new LinkedList<>(); - blockReplace.addAll(parameters.getReplacements()); - JsonObject params = parameters.getParameters(); - OreList ores = parameters.getOres(); - BiomeLocation biomes = parameters.getBiomes(); - - // First, load cached blocks for neighboring chunk ore spawns - int chunkX = pos.x; - int chunkZ = pos.z; - - mergeDefaults(params, getDefaultParameters()); - - runCache(chunkX, chunkZ, world, blockReplace); - - // now to ore spawn - - int blockX = chunkX * 16 + 8; - int blockZ = chunkZ * 16 + 8; - - int maxSpread = params.get(Constants.FormatBits.MAX_SPREAD).getAsInt(); - int minHeight = params.get(Constants.FormatBits.MIN_HEIGHT).getAsInt(); - int maxHeight = params.get(Constants.FormatBits.MAX_HEIGHT).getAsInt(); - int variance = params.get(Constants.FormatBits.VARIATION).getAsInt(); - int frequency = params.get(Constants.FormatBits.FREQUENCY).getAsInt(); - int triesMin = params.get(Constants.FormatBits.ATTEMPTS_MIN).getAsInt(); - int triesMax = params.get(Constants.FormatBits.ATTEMPTS_MAX).getAsInt(); - int clusterSize = params.get(Constants.FormatBits.NODE_SIZE).getAsInt(); - int clusterCount = params.get(Constants.FormatBits.NODE_COUNT).getAsInt(); - - int tries; - - if (triesMax == triesMin) { - tries = triesMax; - } else { - tries = random.nextInt(triesMax - triesMin) + triesMin; - } - - while (tries > 0) { - if (this.random.nextInt(100) <= frequency) { - int xRand = random.nextInt(16); - int zRand = random.nextInt(16); - - int x = blockX + xRand - (maxSpread / 2); - int y = random.nextInt(maxHeight - minHeight) + minHeight; - int z = blockZ + zRand - (maxSpread / 2); - - FunctionParameterWrapper fp = new FunctionParameterWrapper(); - fp.setBlockPos(new BlockPos(x, y, z)); - fp.setWorld(world); - fp.setReplacements(blockReplace); - fp.setBiomes(biomes); - fp.setOres(ores); - - spawnCluster(clusterSize, variance, clusterCount, maxSpread, minHeight, maxHeight, fp); - } - - tries--; - } - } - - private void spawnCluster(int clusterSize, int variance, int clusterCount, int maxSpread, int minHeight, - int maxHeight, FunctionParameterWrapper params) { - // spawn a cluster at the center, then a bunch around the outside... - int r = clusterSize - variance; - - if (variance > 0) { - r += this.random.nextInt(2 * variance) - variance; - } - - spawnChunk(params, r); - - int count = this.random.nextInt(clusterCount - 1); // always at least the first, but vary inside that - - if (variance > 0) { - count += this.random.nextInt(2 * variance) - variance; - } - - while (count >= 0) { - r = clusterSize - variance; - - if (variance > 0) { - r += this.random.nextInt(2 * variance) - variance; - } - - int radius = maxSpread / 2; - - int xp = getPoint(-radius, radius, 0); - int yp = getPoint(minHeight, maxHeight, (maxHeight - minHeight) / 2); - int zp = getPoint(-radius, radius, 0); - - BlockPos p = params.getBlockPos().add(xp, yp, zp); - FunctionParameterWrapper np = new FunctionParameterWrapper(params); - np.setBlockPos(p); - spawnChunk(np, r); - - count -= r; - } - } - - private void spawnChunk(FunctionParameterWrapper params, int quantity) { - int count = quantity; - int lutType = (quantity < 8) ? offsetIndexRef_small.length : offsetIndexRef.length; - int[] lut = (quantity < 8) ? offsetIndexRef_small : offsetIndexRef; - Vec3i[] offs = new Vec3i[lutType]; - - System.arraycopy((quantity < 8) ? offsets_small : offsets, 0, offs, 0, lutType); - - int dimension = params.getWorld().provider.getDimension(); - - if (quantity < 27) { - int[] scrambledLUT = new int[lutType]; - System.arraycopy(lut, 0, scrambledLUT, 0, scrambledLUT.length); - scramble(scrambledLUT, this.random); - int z = 0; - - while (count > 0) { - IBlockState oreBlock = params.getOres().getRandomOre(this.random).getOre(); - - if (!spawn(oreBlock, params.getWorld(), params.getBlockPos().add(offs[scrambledLUT[--count]]), dimension, true, params.getReplacements(), params.getBiomes())) { - count++; - z++; - } else { - z = 0; - } - - if (z > 5) { - count--; - z = 0; - OreSpawn.LOGGER.warn("Unable to place block for chunk after 5 tries"); - } - } - - return; - } - - doSpawnFill(this.random.nextBoolean(), count, params); - } - - private void doSpawnFill(boolean nextBoolean, int quantity, FunctionParameterWrapper params) { - int count = quantity; - double radius = Math.pow(quantity, 1.0/3.0) * (3.0 / 4.0 / Math.PI) + 2; - int rSqr = (int)(radius * radius); - if( nextBoolean ) { - spawnMungeNE( params.getWorld(), params.getBlockPos(), rSqr, radius, params.getReplacements(), count, params.getOres() ); - } else { - spawnMungeSW( params.getWorld(), params.getBlockPos(), rSqr, radius, params.getReplacements(), count, params.getOres() ); - } - } - - - @Override - public void setRandom(Random rand) { - this.random = rand; - } - - @Override - public JsonObject getDefaultParameters() { - JsonObject defParams = new JsonObject(); - defParams.addProperty(Constants.FormatBits.MAX_SPREAD, 16); - defParams.addProperty(Constants.FormatBits.NODE_SIZE, 8); - defParams.addProperty(Constants.FormatBits.NODE_COUNT, 8); - defParams.addProperty(Constants.FormatBits.MIN_HEIGHT, 8); - defParams.addProperty(Constants.FormatBits.MAX_HEIGHT, 24); - defParams.addProperty(Constants.FormatBits.VARIATION, 4); - defParams.addProperty(Constants.FormatBits.FREQUENCY, 25); - defParams.addProperty(Constants.FormatBits.ATTEMPTS_MIN, 4); - defParams.addProperty(Constants.FormatBits.ATTEMPTS_MAX, 8); - return defParams; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/features/DefaultFeatureGenerator.java b/src/main/java/com/mcmoddev/orespawn/impl/features/DefaultFeatureGenerator.java deleted file mode 100644 index 3addb380..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/features/DefaultFeatureGenerator.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.mcmoddev.orespawn.impl.features; - -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.FeatureBase; -import com.mcmoddev.orespawn.api.GeneratorParameters; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.util.OreList; - -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.util.math.Vec3i; -import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkGenerator; -import net.minecraft.world.chunk.IChunkProvider; - - -public class DefaultFeatureGenerator extends FeatureBase implements IFeature { - - public DefaultFeatureGenerator() { - super(new Random()); - } - - @Override - public void generate(World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider, - GeneratorParameters parameters) { - ChunkPos pos = parameters.getChunk(); - List replaceBlock = new LinkedList<>(); - replaceBlock.addAll(parameters.getReplacements()); - JsonObject params = parameters.getParameters(); - OreList ores = parameters.getOres(); - BiomeLocation biomes = parameters.getBiomes(); - - // First, load cached blocks for neighboring chunk ore spawns - int chunkX = pos.x; - int chunkZ = pos.z; - - mergeDefaults(params, getDefaultParameters()); - - runCache(chunkX, chunkZ, world, replaceBlock); - - // now to ore spawn - - int blockX = chunkX * 16 + 8; - int blockZ = chunkZ * 16 + 8; - - int minY = params.get(Constants.FormatBits.MIN_HEIGHT).getAsInt(); - int maxY = params.get(Constants.FormatBits.MAX_HEIGHT).getAsInt(); - int vari = params.get(Constants.FormatBits.VARIATION).getAsInt(); - float freq = params.get(Constants.FormatBits.FREQUENCY).getAsFloat(); - int size = params.get(Constants.FormatBits.NODE_SIZE).getAsInt(); - - FunctionParameterWrapper fp = new FunctionParameterWrapper(); - fp.setWorld(world); - fp.setReplacements(replaceBlock); - fp.setBiomes(biomes); - fp.setOres(ores); - - if (freq >= 1) { - for (int i = 0; i < freq; i++) { - int x = blockX + random.nextInt(16); - int y = random.nextInt(maxY - minY) + minY; - int z = blockZ + random.nextInt(16); - - final int r; - - if (vari > 0) { - r = random.nextInt(2 * vari) - vari; - } else { - r = 0; - } - - fp.setBlockPos(new BlockPos(x, y, z)); - spawnOre(fp, size + r); - } - } else if (random.nextFloat() < freq) { - int x = blockX + random.nextInt(8); - int y = random.nextInt(maxY - minY) + minY; - int z = blockZ + random.nextInt(8); - final int r; - - if (vari > 0) { - r = random.nextInt(2 * vari) - vari; - } else { - r = 0; - } - - fp.setBlockPos(new BlockPos(x, y, z)); - spawnOre(fp, size + r); - } - - } - - private void spawnOre(FunctionParameterWrapper params, int quantity) { - int count = quantity; - int lutType = (quantity < 8) ? offsetIndexRef_small.length : offsetIndexRef.length; - int[] lut = (quantity < 8) ? offsetIndexRef_small : offsetIndexRef; - Vec3i[] offs = new Vec3i[lutType]; - - System.arraycopy((quantity < 8) ? offsets_small : offsets, 0, offs, 0, lutType); - - if (quantity < 27) { - int[] scrambledLUT = new int[lutType]; - System.arraycopy(lut, 0, scrambledLUT, 0, scrambledLUT.length); - scramble(scrambledLUT, this.random); - - while (count > 0) { - IBlockState oreBlock = params.getOres().getRandomOre(this.random).getOre(); - BlockPos target = params.getBlockPos().add(offs[scrambledLUT[--count]]); - spawn(oreBlock, params.getWorld(), target, - params.getWorld().provider.getDimension(), true, params.getReplacements(), params.getBiomes()); - } - - return; - } - - doSpawnFill(this.random.nextBoolean(), count, params); - } - - private void doSpawnFill(boolean nextBoolean, int quantity, FunctionParameterWrapper params) { - int count = quantity; - double radius = Math.pow(quantity, 1.0/3.0) * (3.0 / 4.0 / Math.PI) + 2; - int rSqr = (int)(radius * radius); - if( nextBoolean ) { - spawnMungeNE( params.getWorld(), params.getBlockPos(), rSqr, radius, params.getReplacements(), count, params.getOres() ); - } else { - spawnMungeSW( params.getWorld(), params.getBlockPos(), rSqr, radius, params.getReplacements(), count, params.getOres() ); - } - } - - @Override - public JsonObject getDefaultParameters() { - JsonObject defParams = new JsonObject(); - defParams.addProperty(Constants.FormatBits.MIN_HEIGHT, 0); - defParams.addProperty(Constants.FormatBits.MAX_HEIGHT, 256); - defParams.addProperty(Constants.FormatBits.VARIATION, 16); - defParams.addProperty(Constants.FormatBits.FREQUENCY, 0.5); - defParams.addProperty(Constants.FormatBits.NODE_SIZE, 8); - return defParams; - } - - - @Override - public void setRandom(Random rand) { - this.random = rand; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/features/NormalCloudGenerator.java b/src/main/java/com/mcmoddev/orespawn/impl/features/NormalCloudGenerator.java deleted file mode 100644 index ab0edcf0..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/features/NormalCloudGenerator.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.mcmoddev.orespawn.impl.features; - -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.FeatureBase; -import com.mcmoddev.orespawn.api.GeneratorParameters; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.util.OreList; - -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkProvider; -import net.minecraft.world.chunk.IChunkGenerator; - -public class NormalCloudGenerator extends FeatureBase implements IFeature { - - private NormalCloudGenerator(Random rand) { - super(rand); - } - - public NormalCloudGenerator() { - this(new Random()); - } - - @Override - public void generate(World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider, - GeneratorParameters parameters) { - ChunkPos pos = parameters.getChunk(); - List blockReplace = new LinkedList<>(); - blockReplace.addAll(parameters.getReplacements()); - JsonObject params = parameters.getParameters(); - OreList ores = parameters.getOres(); - BiomeLocation biomes = parameters.getBiomes(); - // First, load cached blocks for neighboring chunk ore spawns - int chunkX = pos.x; - int chunkZ = pos.z; - - mergeDefaults(params, getDefaultParameters()); - - runCache(chunkX, chunkZ, world, blockReplace); - - // now to ore spawn - - // lets not offset blind, - int blockX = chunkX * 16; - int blockZ = chunkZ * 16; - - int maxSpread = params.get(Constants.FormatBits.MAX_SPREAD).getAsInt(); - int medianSize = params.get(Constants.FormatBits.MEDIAN_SIZE).getAsInt(); - int minHeight = params.get(Constants.FormatBits.MIN_HEIGHT).getAsInt(); - int maxHeight = params.get(Constants.FormatBits.MAX_HEIGHT).getAsInt(); - int variance = params.get(Constants.FormatBits.VARIATION).getAsInt(); - int frequency = params.get(Constants.FormatBits.FREQUENCY).getAsInt(); - int triesMin = params.get(Constants.FormatBits.ATTEMPTS_MIN).getAsInt(); - int triesMax = params.get(Constants.FormatBits.ATTEMPTS_MAX).getAsInt(); - - // on the X and Z you have a possible 2-chunk range - 32 blocks - subtract the spread to get - // a size that will let us insert by the radius - int offsetXZ = 32 - maxSpread; - - // you have the distance between minHeight and maxHeight - // this is the actual size of the space - int sizeY = (maxHeight - minHeight); - int offsetY = sizeY - maxSpread; - int radiusXZ = offsetXZ / 2; - - // actual radius for placement is the size minus the spread to center it in the space and keep - // from overflowing - int radiusY = offsetY / 2; - - // we center at the minimum plus the half the height - int blockY = minHeight + (sizeY / 2); - - int fSave = frequency; - int tryCount = 0; - - int tries; - - if (triesMax == triesMin) { - tries = triesMax; - } else { - tries = random.nextInt(triesMax - triesMin) + triesMin; - } - - while (tries > 0) { - if (this.random.nextInt(100) <= frequency) { - frequency = fSave; - int x = blockX + getPoint(0, offsetXZ, radiusXZ) + radiusXZ; - // this should, hopefully, keep us centered between minHeight and maxHeight with nothing going above/below those values - int y = blockY + getPoint(0, offsetY, radiusY); - int z = blockZ + getPoint(0, offsetXZ, radiusXZ) + radiusXZ; - - int r = medianSize - variance; - - if (variance > 0) { - r += random.nextInt(2 * variance) - variance; - } - - FunctionParameterWrapper fp = new FunctionParameterWrapper(); - fp.setBlockPos(new BlockPos(x, y, z)); - fp.setWorld(world); - fp.setReplacements(blockReplace); - fp.setBiomes(biomes); - fp.setOres(ores); - - if (!spawnCloud(r, maxSpread, minHeight, maxHeight, fp) && tryCount < 5) { - // make another try! - tries++; - frequency = 100; - tryCount++; - } else { - tryCount = 0; - } - } - - tries--; - } - } - - private boolean spawnCloud(int size, int maxSpread, int minHeight, int maxHeight, FunctionParameterWrapper params) { - // spawn one right at the center here, then generate for the cloud and do the math - - if (!spawn(params.getOres().getRandomOre(random).getOre(), params.getWorld(), params.getBlockPos(), - params.getWorld().provider.getDimension(), true, params.getReplacements(), params.getBiomes())) { - return false; - } - - int radius = maxSpread / 2; - boolean alreadySpewed = false; - int count = Math.min(size, (int)Math.round(Math.PI * Math.pow(radius, 2))); - - while (count > 0) { - int xp = getPoint(0, maxSpread, radius); - int yp = getPoint(minHeight, maxHeight, (maxHeight - minHeight) / 2); - int zp = getPoint(0, maxSpread, radius); - - BlockPos p = params.getBlockPos().add(xp, yp, zp); - - int z = 0; - - while (z < 5 && !spawn(params.getOres().getRandomOre(random).getOre(), params.getWorld(), p, - params.getWorld().provider.getDimension(), true, params.getReplacements(), params.getBiomes())) { - xp = getPoint(0, maxSpread, radius); - yp = getPoint(minHeight, maxHeight, (maxHeight - minHeight) / 2); - zp = getPoint(0, maxSpread, radius); - - p = params.getBlockPos().add(xp, yp, zp); - - z++; - } - - if (z >= 5 && !alreadySpewed) { - OreSpawn.LOGGER.info("unable to achieve requested cloud density for cloud centered at %s", params.getBlockPos()); - alreadySpewed = true; - } - - count--; - } - - return true; - } - - @Override - public void setRandom(Random rand) { - this.random = rand; - } - - @Override - public JsonObject getDefaultParameters() { - JsonObject defParams = new JsonObject(); - defParams.addProperty(Constants.FormatBits.MAX_SPREAD, 16); - defParams.addProperty(Constants.FormatBits.MEDIAN_SIZE, 8); - defParams.addProperty(Constants.FormatBits.MIN_HEIGHT, 8); - defParams.addProperty(Constants.FormatBits.MAX_HEIGHT, 24); - defParams.addProperty(Constants.FormatBits.VARIATION, 4); - defParams.addProperty(Constants.FormatBits.FREQUENCY, 25); - defParams.addProperty(Constants.FormatBits.ATTEMPTS_MIN, 4); - defParams.addProperty(Constants.FormatBits.ATTEMPTS_MAX, 4); - return defParams; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/features/PrecisionGenerator.java b/src/main/java/com/mcmoddev/orespawn/impl/features/PrecisionGenerator.java deleted file mode 100644 index 7017d22c..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/features/PrecisionGenerator.java +++ /dev/null @@ -1,304 +0,0 @@ -package com.mcmoddev.orespawn.impl.features; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.FeatureBase; -import com.mcmoddev.orespawn.api.GeneratorParameters; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.data.Constants.FormatBits; -import com.mcmoddev.orespawn.util.OreList; - -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.util.math.Vec3i; -import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkProvider; -import net.minecraft.world.chunk.IChunkGenerator; - -public class PrecisionGenerator extends FeatureBase implements IFeature { - - private PrecisionGenerator(Random rand) { - super(rand); - } - - public PrecisionGenerator() { - this(new Random()); - } - - - @Override - public void generate(World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider, - GeneratorParameters parameters) { - ChunkPos pos = parameters.getChunk(); - List blockReplace = new LinkedList<>(); - blockReplace.addAll(parameters.getReplacements()); - JsonObject params = parameters.getParameters(); - OreList ores = parameters.getOres(); - BiomeLocation biomes = parameters.getBiomes(); - - // First, load cached blocks for neighboring chunk ore spawns - int chunkX = pos.x; - int chunkZ = pos.z; - - mergeDefaults(params, getDefaultParameters()); - - runCache(chunkX, chunkZ, world, blockReplace); - - // extract parameters - int nodeCount = params.get(FormatBits.NODE_COUNT).getAsInt(); - int maxHeight = params.get(FormatBits.MAX_HEIGHT).getAsInt(); - int minHeight = params.get(FormatBits.MIN_HEIGHT).getAsInt(); - int nodeSize = params.get(FormatBits.NODE_SIZE).getAsInt(); - - int thisNode = nodeSize; - - // now to use them - for (int c = nodeCount; c > 0; c--) { - int sc; - HeightRange hr = new HeightRange(minHeight, maxHeight); - BlockPos spot = chooseSpot(chunkX, chunkZ, hr); - FunctionParameterWrapper fp = new FunctionParameterWrapper(); - fp.setBlockPos(spot); - fp.setWorld(world); - fp.setReplacements(blockReplace); - fp.setBiomes(biomes); - fp.setOres(ores); - fp.setChunkPos(new ChunkPos(chunkX, chunkZ)); - - sc = spawnAtSpot(thisNode, hr, fp); - - // bit of feedback - if we underproduce or overproduce a node, the next one gets a correction - if (sc != thisNode && sc != 0) { - thisNode += (nodeSize - sc); - OreSpawn.LOGGER.debug("node at %s of size %d instead of %d - modding to %d", - spot, sc, nodeSize, thisNode); - } else if (sc == thisNode) { - // if we produced exact size, reset the size - thisNode = nodeSize; - } - - // if we hit a node of size zero or less, we've done something wrong - if (thisNode <= 0) { - thisNode = nodeSize; - } - } - } - - private int spawnAtSpot(int nodeSize, HeightRange heightRange, FunctionParameterWrapper params) { - int spawned = 0; - int c; - - FunctionParameterWrapper np = new FunctionParameterWrapper(params); - BlockPos act = params.getBlockPos(); - int counter = nodeSize; - - while (counter > 0 && spawned < nodeSize) { - np.setBlockPos(act); - c = spawnOreNode(np, nodeSize, heightRange); - - if (c == 0) { - OreSpawn.LOGGER.debug("Unable to place block at %s (chunk %s)", np.getBlockPos(), np.getChunkPos()); - act = chooseSpot(Math.floorDiv(params.getBlockPos().getX(), 16), Math.floorDiv(params.getBlockPos().getZ(), 16), heightRange); - } - - counter -= (c + 1); - spawned += c; - } - - return spawned; - } - - private int spawnOreNode(FunctionParameterWrapper params, int nodeSize, HeightRange heightRange) { - - int count = nodeSize; - int lutType = (nodeSize < 8) ? offsetIndexRef_small.length : offsetIndexRef.length; - int[] lut = (nodeSize < 8) ? offsetIndexRef_small : offsetIndexRef; - Vec3i[] offs = new Vec3i[lutType]; - - System.arraycopy((nodeSize < 8) ? offsets_small : offsets, 0, offs, 0, lutType); - - if (nodeSize < 27) { - int[] scrambledLUT = new int[lutType]; - System.arraycopy(lut, 0, scrambledLUT, 0, scrambledLUT.length); - scramble(scrambledLUT, this.random); - - int nc = 0; - - for (; count > 0 && nc <= nodeSize; count--) { - IBlockState oreBlock = params.getOres().getRandomOre(this.random).getOre(); - Vec3i offset = offs[scrambledLUT[--count]]; - BlockPos p = fixMungeOffset(offset, params.getBlockPos(), heightRange, params.getChunkPos()); - int dimension = params.getWorld().provider.getDimension(); - - if (spawn(oreBlock, params.getWorld(), p, dimension, true, params.getReplacements(), params.getBiomes())) { - nc++; - } - } - - return nc; - } - - return spawnFill(params, nodeSize, heightRange); - } - - private BlockPos fixMungeOffset(Vec3i offset, BlockPos spot, HeightRange heightRange, ChunkPos pos) { - BlockPos p = spot.add(offset); - ChunkPos x1z1 = new ChunkPos(pos.x + 1, pos.z + 1); - int xMax = x1z1.getXEnd(); - int zMax = x1z1.getZEnd(); - int xMin = pos.getXStart(); - int zMin = pos.getZStart(); - - int xmod = offset.getX(); - int ymod = offset.getY(); - int zmod = offset.getZ(); - - // correct the points values to not cause the Y coordinate to go outside the permissable range - if (p.getY() < heightRange.getMin() || p.getY() > heightRange.getMax()) { - ymod = rescaleOffset(ymod, spot.getY(), heightRange.getMin(), heightRange.getMax()); - } - - if (p.getX() < xMin || p.getX() > xMax) { - xmod = rescaleOffset(xmod, spot.getX(), xMin, xMax); - } - - if (p.getZ() < zMin || p.getZ() > zMax) { - zmod = rescaleOffset(zmod, spot.getZ(), zMin, zMax); - } - - BlockPos rVal = spot.add(xmod, ymod, zmod); - OreSpawn.LOGGER.debug("rescaled %s to %s", spot, rVal); - return rVal; - } - - private int rescaleOffset(final int offsetIn, final int centerIn, final int minimumIn, final int maximumIn) { - int actual = centerIn + offsetIn; - int wrapDistance; - int range = maximumIn - minimumIn; - int workingPoint; - - if (actual < minimumIn) { - wrapDistance = minimumIn - actual; - } else { - wrapDistance = actual - maximumIn; - } - - if (wrapDistance < 0) { - wrapDistance = ((-1) * wrapDistance) % range; - } else { - wrapDistance %= range; - } - - if (actual < minimumIn) { - workingPoint = maximumIn - wrapDistance; - } else { - workingPoint = minimumIn + wrapDistance; - } - - return workingPoint - centerIn; - } - - private int spawnFill(FunctionParameterWrapper params, int nodeSize, HeightRange heightRange) { - - double radius = Math.pow(nodeSize, 1.0 / 3.0) * (3.0 / 4.0 / Math.PI) + 2; - int rSqr = (int)Math.ceil(radius * radius); - - if (this.random.nextBoolean()) { - return spawnPrecise(params, heightRange, false, radius, rSqr, nodeSize); - } else { - return spawnPrecise(params, heightRange, true, radius, rSqr, nodeSize); - } - } - - private int spawnPrecise(FunctionParameterWrapper params, HeightRange heightRange, boolean toPositive, double radius, - int rSqr, int nodeSize) { - int quantity = nodeSize; - int nc = 0; - - for (int dy = (int)(-1 * radius); dy < radius; dy++) { - for (int dx = getStart(toPositive, radius); endCheck(toPositive, dx, radius); dx = countItem(dx, toPositive)) { - for (int dz = getStart(toPositive, radius); endCheck(toPositive, dz, radius); dz = countItem(dz, toPositive)) { - if (doCheckSpawn(dx, dy, dz, rSqr, heightRange, params) >= 0) { - nc++; - quantity--; - - if (nc >= nodeSize || quantity <= 0) { - return nc; - } - } - } - } - } - - return nc; - } - - private int doCheckSpawn(int dx, int dy, int dz, int rSqr, HeightRange heightRange, FunctionParameterWrapper params) { - if (getABC(dx, dy, dz) <= rSqr) { - BlockPos p = fixMungeOffset(new Vec3i(dx, dy, dz), params.getBlockPos(), heightRange, params.getChunkPos()); - IBlockState bl = params.getOres().getRandomOre(this.random).getOre(); - return spawn(bl, params.getWorld(), p, params.getWorld().provider.getDimension(), true, - params.getReplacements(), params.getBiomes()) ? 1 : 0; - } - - return -1; - } - - private int getPoint(int lowerBound, int upperBound) { - List arr = new ArrayList<> (); - - for (int i = lowerBound; i <= upperBound; i++) { - arr.add(i); - } - - return arr.get(this.random.nextInt(arr.size())); - } - - private BlockPos chooseSpot(int xPosition, int zPosition, HeightRange heightRange) { - int xRet = getPoint(0, 15) + (xPosition * 16); - int zRet = getPoint(0, 15) + (zPosition * 16); - int yRet = getPoint(heightRange.getMin(), heightRange.getMax()); - - return new BlockPos(xRet, yRet, zRet); - } - - @Override - public void setRandom(Random rand) { - this.random = rand; - } - - @Override - public JsonObject getDefaultParameters() { - JsonObject defaults = new JsonObject(); - defaults.addProperty(FormatBits.NODE_COUNT, 4); - defaults.addProperty(FormatBits.MIN_HEIGHT, 16); - defaults.addProperty(FormatBits.MAX_HEIGHT, 80); - defaults.addProperty(FormatBits.NODE_SIZE, 8); - return defaults; - } - - private class HeightRange { - private int min; - private int max; - - HeightRange(int min, int max) { - this.min = min; - this.max = max; - } - - int getMin() { - return this.min; - } - - int getMax() { - return this.max; - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/features/VeinGenerator.java b/src/main/java/com/mcmoddev/orespawn/impl/features/VeinGenerator.java deleted file mode 100644 index 03bfa1ac..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/features/VeinGenerator.java +++ /dev/null @@ -1,278 +0,0 @@ -package com.mcmoddev.orespawn.impl.features; - -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.FeatureBase; -import com.mcmoddev.orespawn.api.GeneratorParameters; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.util.OreList; - -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.util.math.Vec3i; -import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkProvider; -import net.minecraft.world.chunk.IChunkGenerator; - -public class VeinGenerator extends FeatureBase implements IFeature { - - private VeinGenerator(Random rand) { - super(rand); - } - - public VeinGenerator() { - this(new Random()); - } - - @Override - public void generate(World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider, - GeneratorParameters parameters) { - ChunkPos pos = parameters.getChunk(); - List blockReplace = new LinkedList<>(); - blockReplace.addAll(parameters.getReplacements()); - JsonObject params = parameters.getParameters(); - OreList ores = parameters.getOres(); - BiomeLocation biomes = parameters.getBiomes(); - - // First, load cached blocks for neighboring chunk ore spawns - int chunkX = pos.x; - int chunkZ = pos.z; - - runCache(chunkX, chunkZ, world, blockReplace); - mergeDefaults(params, getDefaultParameters()); - - // now to ore spawn - - int blockX = chunkX * 16 + 8; - int blockZ = chunkZ * 16 + 8; - - int minY = params.get(Constants.FormatBits.MIN_HEIGHT).getAsInt(); - int maxY = params.get(Constants.FormatBits.MAX_HEIGHT).getAsInt(); - int vari = params.get(Constants.FormatBits.VARIATION).getAsInt(); - int freq = params.get(Constants.FormatBits.FREQUENCY).getAsInt(); - int length = params.get(Constants.FormatBits.LENGTH).getAsInt(); - int wander = params.get(Constants.FormatBits.WANDER).getAsInt(); - int nodeSize = params.get(Constants.FormatBits.NODE_SIZE).getAsInt(); - int triesMin = params.get(Constants.FormatBits.ATTEMPTS_MIN).getAsInt(); - int triesMax = params.get(Constants.FormatBits.ATTEMPTS_MAX).getAsInt(); - - int tries; - - if (triesMax == triesMin) { - tries = triesMax; - } else { - tries = random.nextInt(triesMax - triesMin) + triesMin; - } - - // we have an offset into the chunk but actually need something more - while (tries > 0) { - if (this.random.nextInt(100) <= freq) { - int x = blockX + random.nextInt(16); - int y = random.nextInt(maxY - minY) + minY; - int z = blockZ + random.nextInt(16); - - final int r; - - if (vari > 0) { - r = random.nextInt(2 * vari) - vari; - } else { - r = 0; - } - - FunctionParameterWrapper fp = new FunctionParameterWrapper(); - fp.setBlockPos(new BlockPos(x, y, z)); - fp.setWorld(world); - fp.setReplacements(blockReplace); - fp.setBiomes(biomes); - fp.setOres(ores); - - spawnVein(length + r, nodeSize, wander, fp); - } - - tries--; - } - } - - // for proper use we need to map these in on the selections later - // probability of any given vertex-point of a face being selected, each - // row of this is a row on the face, each float is a point - private float[][] rowMap = new float[][] { - { 0.5f, 0.75f, 0.5f }, - { 0.5f, 1.00f, 0.5f }, - { 0.5f, 0.75f, 0.5f } - }; - - private float[] colMap = new float[] { - 0.75f, 1.00f, 0.75f - }; - - private int triangularDistributionNoRandom(double current) { - if (current < 0.5f) { - return (int) Math.sqrt(current * 2); - } else { - return (int)(2 - Math.sqrt((1 - current) * 2)); - } - } - - private enum EnumFace { - UP, - FRONT, - DOWN, - BACK, - LEFT, - RIGHT; - - public static EnumFace getRandomFace(Random random) { - return values()[random.nextInt(values().length)]; - } - - } - - private int[][][][] facePosMap = new int[][][][] { - { - // top face - // x y z - { { -1, 1, 1 }, { 0, 1, 1 }, { 1, 1, 1 } }, // line vertex 1 to 2 - { { -1, 1, 0 }, { 0, 1, 0 }, { 1, 0, 1 } }, // center row, no line - { { -1, 1, -1 }, { 0, 1, -1 }, { 1, 1, -1 } } // line vertex 3 to 4 - }, - { - // front face - // x y z - { { -1, 1, 1 }, { 0, 1, 1 }, { 1, 1, 1 } }, // line vertex 1 to 2 - { { -1, 0, 1 }, { 0, 0, 1 }, { 1, 0, 1 } }, // center row, no line - { { -1, -1, 1 }, { 0, -1, 1 }, { 1, -1, 1 } } // line vertex 7 to 8 - }, - { - // down face - // x y z - { { -1, -1, 1 }, { 0, -1, 1 }, { 1, -1, 1 } }, // line vertex 7 to 8 - { { -1, -1, 0 }, { 0, -1, 0 }, { 1, -1, 0 } }, // center, no line - { { -1, -1, -1 }, { 0, -1, -1 }, { 1, -1, -1 } } // line vertex 6 to 5 - }, - { - // back face - // x y z - { { -1, 1, -1 }, { 0, 1, -1 }, { 1, 1, -1 } }, // line vertex 3 to 4 - { { -1, 0, -1 }, { 0, -1, 0 }, { 1, 0, -1 } }, // center, no line - { { -1, -1, -1 }, { 0, -1, -1 }, { 1, -1, -1 } } // line vertex 6 to 5 - }, - { - // left face - // x y z - { { 1, 1, 1 }, { 1, 1, 0 }, { 1, 1, -1 } }, // line vertex 2 to 3 - { { 1, 0, 1 }, { 1, 0, 0 }, { 1, 0, -1 } }, // line, vertex 2 to 8 - { { 1, -1, 1 }, { 1, -1, 0 }, { 1, -1, -1 } } // line, vertex 3 to 5 - }, - { - // right face - // x y z - { { -1, 1, 1 }, { -1, 1, 0 }, { -1, 1, -1 } }, // line vertex 2 to 3 - { { -1, 0, 1 }, { -1, 0, 0 }, { -1, 0, -1 } }, // line, vertex 2 to 8 - { { -1, -1, 1 }, { -1, -1, 0 }, { -1, -1, -1 } } // line, vertex 3 to 5 - } - }; - - private BlockPos adjustPos(BlockPos pos, int row, int col, EnumFace face) { - int faceOrd = face.ordinal(); - int[] adjust = facePosMap[faceOrd][row][col]; - return pos.add(adjust[0], adjust[1], adjust[2]); - } - - private void spawnVein(int length, int nodeSize, int wander, FunctionParameterWrapper params) { - // passed in POS is our start - we start with a weighting favoring straight directions - // and three-quarters that to the edges - // and one-half to the corners - - // generate a node here - spawnOre(params, nodeSize); - // select a direction, decrement length, repeat - float curRow = 1.00f; - float curCol = 1.00f; - int colAdj = 2; - int rowAdj = 2; - EnumFace faceToUse = EnumFace.getRandomFace(random); - int l = length; - BlockPos workPos = new BlockPos(params.getBlockPos()); - - while (l > 0) { - workPos = adjustPos(workPos, colAdj, rowAdj, faceToUse); - - l--; - - // allow for the "wandering vein" parameter - if (random.nextInt(100) <= wander) { - colAdj = triangularDistributionNoRandom(curCol); - curCol += colMap[colAdj]; - - while (curCol > 1) { - curCol /= 10; - } - - rowAdj = triangularDistributionNoRandom(curCol); - curRow += rowMap[colAdj][rowAdj]; - - while (curRow > 1) { - curRow /= 10; - } - - FunctionParameterWrapper np = new FunctionParameterWrapper(params); - np.setBlockPos(workPos); - spawnOre(np, nodeSize); - - // when nodes are small, the veins get badly broken if we do face wandering - if (nodeSize > 2) { - faceToUse = EnumFace.getRandomFace(random); - } - } - } - } - - private void spawnOre(FunctionParameterWrapper params, int nodeSize) { - int count = nodeSize; - int lutType = (count < 8) ? offsetIndexRef_small.length : offsetIndexRef.length; - int[] lut = (count < 8) ? offsetIndexRef_small : offsetIndexRef; - Vec3i[] offs = new Vec3i[lutType]; - - System.arraycopy((count < 8) ? offsets_small : offsets, 0, offs, 0, lutType); - - int[] scrambledLUT = new int[lutType]; - System.arraycopy(lut, 0, scrambledLUT, 0, scrambledLUT.length); - scramble(scrambledLUT, this.random); - int dimension = params.getWorld().provider.getDimension(); - - while (count > 0) { - spawn(params.getOres().getRandomOre(random).getOre(), params.getWorld(), - params.getBlockPos().add(offs[scrambledLUT[--count]]), dimension, true, - params.getReplacements(), params.getBiomes()); - } - } - - @Override - public JsonObject getDefaultParameters() { - JsonObject defParams = new JsonObject(); - defParams.addProperty(Constants.FormatBits.MIN_HEIGHT, 0); - defParams.addProperty(Constants.FormatBits.MAX_HEIGHT, 256); - defParams.addProperty(Constants.FormatBits.VARIATION, 16); - defParams.addProperty(Constants.FormatBits.FREQUENCY, 50); - defParams.addProperty(Constants.FormatBits.ATTEMPTS_MAX, 8); - defParams.addProperty(Constants.FormatBits.ATTEMPTS_MIN, 4); - defParams.addProperty(Constants.FormatBits.LENGTH, 16); - defParams.addProperty(Constants.FormatBits.WANDER, 75); - defParams.addProperty(Constants.FormatBits.NODE_SIZE, 3); - return defParams; - } - - - @Override - public void setRandom(Random rand) { - this.random = rand; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationComposition.java b/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationComposition.java deleted file mode 100644 index 10debedb..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationComposition.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.mcmoddev.orespawn.impl.location; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.mcmoddev.orespawn.api.BiomeLocation; -import net.minecraft.world.biome.Biome; - -import java.util.Objects; -import java.util.List; -import java.util.LinkedList; - -public final class BiomeLocationComposition implements BiomeLocation { - private final ImmutableSet inclusions; - - private final ImmutableSet exclusions; - - private final int hash; - - public BiomeLocationComposition(ImmutableSet inclusions, ImmutableSet exclusions) { - this.inclusions = inclusions; - this.exclusions = exclusions; - this.hash = Objects.hash(inclusions, exclusions); - } - - private boolean matchBiome(Biome biome, BiomeLocation loc) { - return (loc.getBiomes().stream().filter(b -> b.equals(biome)).distinct().count() > 0); - } - - @Override - public boolean matches(Biome biome) { - boolean inWhite = this.inclusions.asList().stream().anyMatch(bl -> matchBiome(biome, bl)); - boolean inBlack = this.exclusions.asList().stream().anyMatch(bl -> matchBiome(biome, bl)); - - return !inBlack && inWhite; - } - - @Override - public int hashCode() { - return this.hash; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (obj instanceof BiomeLocationComposition) { - BiomeLocationComposition other = (BiomeLocationComposition) obj; - return this.inclusions.equals(other.inclusions) && this.exclusions.equals(other.exclusions); - } - - return false; - } - - @Override - public ImmutableList getBiomes() { - List temp = new LinkedList<>(); - this.inclusions.stream().forEach(bl -> temp.addAll(bl.getBiomes())); - this.exclusions.stream().forEach(bl -> temp.addAll(bl.getBiomes())); - return ImmutableList.copyOf(temp); - } - - public ImmutableSet getInclusions() { - return this.inclusions; - } - - public ImmutableSet getExclusions() { - return this.exclusions; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationDictionary.java b/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationDictionary.java deleted file mode 100644 index ca26b096..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationDictionary.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.mcmoddev.orespawn.impl.location; - -import com.google.common.collect.ImmutableList; -import com.mcmoddev.orespawn.api.BiomeLocation; -import net.minecraft.world.biome.Biome; -import net.minecraftforge.common.BiomeDictionary; - -public final class BiomeLocationDictionary implements BiomeLocation { - private final BiomeDictionary.Type type; - - private final int hash; - - public BiomeLocationDictionary(BiomeDictionary.Type type) { - this.type = type; - this.hash = type.hashCode(); - } - - @Override - public boolean matches(Biome biome) { - return BiomeDictionary.hasType(biome, this.type); - } - - @Override - public int hashCode() { - return this.hash; - } - - @Override - public boolean equals(Object obj) { - return (obj == this) || ((obj instanceof BiomeLocationDictionary) && this.type.equals(((BiomeLocationDictionary) obj).type)); - } - - public BiomeDictionary.Type getType() { - return this.type; - } - - @Override - public ImmutableList getBiomes() { - return ImmutableList.copyOf(BiomeDictionary.getBiomes(this.type)); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationList.java b/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationList.java deleted file mode 100644 index e16a50da..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationList.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.mcmoddev.orespawn.impl.location; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.mcmoddev.orespawn.api.BiomeLocation; -import net.minecraft.world.biome.Biome; - -import java.util.List; -import java.util.LinkedList; - -public final class BiomeLocationList implements BiomeLocation { - private final ImmutableSet locations; - - private final int hash; - - public BiomeLocationList(ImmutableSet locations) { - this.locations = locations; - this.hash = locations.hashCode(); - } - - @Override - public boolean matches(Biome biome) { - return this.locations.stream().anyMatch(loc -> loc.matches(biome)); - } - - @Override - public int hashCode() { - return this.hash; - } - - @Override - public boolean equals(Object obj) { - return (obj == this) || ((obj instanceof BiomeLocationList) && this.locations.equals(((BiomeLocationList) obj).locations)); - } - - @Override - public ImmutableList getBiomes() { - List temp = new LinkedList<>(); - locations.stream().forEach(bl -> temp.addAll(bl.getBiomes())); - return ImmutableList.copyOf(temp); - } - - public ImmutableSet getLocations() { - return this.locations; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationSingle.java b/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationSingle.java deleted file mode 100644 index bd144835..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/location/BiomeLocationSingle.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.mcmoddev.orespawn.impl.location; - -import com.google.common.collect.ImmutableList; -import com.mcmoddev.orespawn.api.BiomeLocation; -import net.minecraft.world.biome.Biome; - -public final class BiomeLocationSingle implements BiomeLocation { - private final Biome biome; - - private final int hash; - - public BiomeLocationSingle(Biome biome) { - this.biome = biome; - this.hash = biome.hashCode(); - } - - @Override - public boolean matches(Biome biome) { - return this.biome.equals(biome); - } - - @Override - public ImmutableList getBiomes() { - return ImmutableList.of(this.biome); - } - - @Override - public int hashCode() { - return this.hash; - } - - @Override - public boolean equals(Object obj) { - return (obj == this) || ((obj instanceof BiomeLocationSingle) && this.biome.equals(((BiomeLocationSingle) obj).biome)); - } - - public Biome getBiome() { - return this.biome; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/BiomeBuilderImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/BiomeBuilderImpl.java deleted file mode 100644 index d50e16e8..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/BiomeBuilderImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import java.util.List; -import java.util.ArrayList; -import javax.annotation.Nonnull; - -import com.google.common.collect.ImmutableSet; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.os3.BiomeBuilder; -import com.mcmoddev.orespawn.impl.location.*; - -import net.minecraft.util.ResourceLocation; -import net.minecraft.world.biome.Biome; -import net.minecraftforge.common.BiomeDictionary; -import net.minecraftforge.fml.common.registry.ForgeRegistries; - -public class BiomeBuilderImpl implements BiomeBuilder { - private List biomeWhitelist; - private List biomeBlacklist; - private BiomeLocation loc; - - BiomeBuilderImpl() { - this.biomeWhitelist = new ArrayList<>(); - this.biomeBlacklist = new ArrayList<>(); - } - - private Biome getBiomeByName(String name) { - return ForgeRegistries.BIOMES.getValue(new ResourceLocation(name)); - } - - private BiomeDictionary.Type getBiomeDictionaryType(String name) { - return BiomeDictionary.Type.getType(name); - } - - @Override - public BiomeBuilder whitelistBiome(@Nonnull Biome biome) { - this.biomeWhitelist.add(new BiomeLocationSingle(biome)); - return this; - } - - @Override - public BiomeBuilder whitelistBiomeByName(@Nonnull String biomeName) { - Biome b = getBiomeByName(biomeName); - BiomeLocation bL = new BiomeLocationSingle(b); - - if (!this.biomeWhitelist.contains(bL)) { - this.biomeWhitelist.add(bL); - } - - return this; - } - - @Override - public BiomeBuilder whitelistBiomeByDictionary(@Nonnull String biomeDictionaryName) { - this.biomeWhitelist.add(new BiomeLocationDictionary(getBiomeDictionaryType(biomeDictionaryName))); - return this; - } - - @Override - public BiomeBuilder blacklistBiome(@Nonnull Biome biome) { - this.biomeBlacklist.add(new BiomeLocationSingle(biome)); - return this; - } - - @Override - public BiomeBuilder blacklistBiomeByName(@Nonnull String biomeName) { - this.biomeBlacklist.add(new BiomeLocationSingle(getBiomeByName(biomeName))); - return this; - } - - @Override - public BiomeBuilder blacklistBiomeByDictionary(@Nonnull String biomeDictionaryName) { - this.biomeBlacklist.add(new BiomeLocationDictionary(getBiomeDictionaryType(biomeDictionaryName))); - return this; - } - - @Override - public BiomeLocation getBiomes() { - if (this.loc != null) { - return this.loc; - } - - if (!this.biomeBlacklist.isEmpty()) { - this.loc = new BiomeLocationComposition(ImmutableSet.copyOf(this.biomeWhitelist), - ImmutableSet.copyOf(this.biomeBlacklist)); - } else { - if (this.biomeWhitelist.size() == 1) { - this.loc = this.biomeWhitelist.toArray(new BiomeLocation[0])[0]; - } else { - this.loc = new BiomeLocationList(ImmutableSet.copyOf(this.biomeWhitelist)); - } - } - - return this.loc; - } - - @Override - public BiomeBuilder setFromBiomeLocation(@Nonnull BiomeLocation biomes) { - this.loc = biomes; - return this; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/BuilderLogicImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/BuilderLogicImpl.java deleted file mode 100644 index 26ebd491..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/BuilderLogicImpl.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import com.google.common.collect.ImmutableMap; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.os3.BuilderLogic; -import com.mcmoddev.orespawn.api.os3.DimensionBuilder; -import com.mcmoddev.orespawn.data.Constants; - -public class BuilderLogicImpl implements BuilderLogic { - private final Map dimensions; - - public BuilderLogicImpl() { - this.dimensions = new HashMap<>(); - } - - @Override - public DimensionBuilder newDimensionBuilder(String name) { - int id = this.dimensionNameToId(name); - - if (id == OreSpawn.API.dimensionWildcard()) { - return this.newDimensionBuilder(); - } - - return this.newDimensionBuilder(id); - } - - @Override - public DimensionBuilder newDimensionBuilder(int id) { - if (dimensions.containsKey(id)) { - return this.getDimension(id); - } - - DimensionBuilder db = new DimensionBuilderImpl(); - dimensions.put(id, db); - return db; - } - - @Override - public DimensionBuilder newDimensionBuilder() { - return this.newDimensionBuilder(OreSpawn.API.dimensionWildcard()); - } - - @Override - public BuilderLogic create(DimensionBuilder... dimensions) { - // for future expansion/orthagonality - return this; - } - - @Override - public DimensionBuilder getDimension(String name) { - Integer id = this.dimensionNameToId(name); - return this.getDimension(id); - } - - @Override - public DimensionBuilder getDimension(int id) { - if (dimensions.containsKey(id)) { - return dimensions.get(id); - } - - return null; - } - - @Override - public ImmutableMap getAllDimensions() { - return ImmutableMap.copyOf(dimensions); - } - - - private int dimensionNameToId(String name) { - switch (name.toLowerCase(Locale.ROOT)) { - case Constants.OVERWORLD: - case Constants.THE_OVERWORLD: - return 0; - - case Constants.NETHER: - case Constants.THE_NETHER: - return -1; - - case Constants.END: - case Constants.THE_END: - return 1; - - case "+": - default: - return OreSpawn.API.dimensionWildcard(); - } - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/DimensionBuilderImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/DimensionBuilderImpl.java deleted file mode 100644 index 68cb7fc6..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/DimensionBuilderImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.annotation.Nullable; - -import com.google.common.collect.ImmutableList; -import com.mcmoddev.orespawn.api.os3.DimensionBuilder; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; - -public class DimensionBuilderImpl implements DimensionBuilder { - private static final String UNNAMED = "unnamed"; - private Map> spawns; - - public DimensionBuilderImpl() { - this.spawns = new HashMap<>(); - } - - @Override - public SpawnBuilder newSpawnBuilder(@Nullable String name) { - String entName = (name == null) ? UNNAMED : name; - spawns.computeIfAbsent(entName, tempName -> new ArrayList()); - SpawnBuilder sb = new SpawnBuilderImpl(); - spawns.get(entName).add(sb); - return sb; - } - - @Override - public DimensionBuilder create(SpawnBuilder... addedSpawns) { - return this; - } - - @Override - public ImmutableList getSpawnByName(String name) { - if (spawns.containsKey(name)) { - return ImmutableList.copyOf(spawns.get(name)); - } - - return null; - } - - @Override - public ImmutableList getAllSpawns() { - return ImmutableList.copyOf(spawns.values().stream().collect(Collectors.toList()).get(0)); - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/DimensionListImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/DimensionListImpl.java deleted file mode 100644 index 043b8c05..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/DimensionListImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import com.mcmoddev.orespawn.api.os3.DimensionList; - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -public class DimensionListImpl implements DimensionList { - private List whitelist; - private List blacklist; - - DimensionListImpl() { - this.whitelist = new LinkedList<> (); - this.blacklist = new LinkedList<> (); - } - - @Override - public boolean match(int dimensionId) { - return (whitelist.isEmpty() && blacklist.isEmpty() && dimensionId != -1 && dimensionId != 1) || - whitelist.contains(dimensionId) || !blacklist.contains(dimensionId); - } - - @Override - public void create(int[] whitelist, int[] blacklist) { - Arrays.stream(whitelist).map(Integer::new).forEach(this.whitelist::add); - Arrays.stream(blacklist).map(Integer::new).forEach(this.blacklist::add); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/FeatureBuilderImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/FeatureBuilderImpl.java deleted file mode 100644 index 51356609..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/FeatureBuilderImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.api.os3.FeatureBuilder; -import com.mcmoddev.orespawn.impl.features.DefaultFeatureGenerator; - -public class FeatureBuilderImpl implements FeatureBuilder { - private String featureName; - private JsonObject parameters; - private IFeature feature; - - public FeatureBuilderImpl() { - this.featureName = "default"; - this.feature = new DefaultFeatureGenerator(); - this.parameters = new JsonObject(); - } - - @Override - public FeatureBuilder setGenerator(String name) { - if (OreSpawn.FEATURES.hasFeature(name)) { - this.featureName = name; - this.feature = OreSpawn.FEATURES.getFeature(name); - } - - return this; - } - - @Override - public FeatureBuilder addParameter(String name, boolean value) { - this.parameters.addProperty(name, value); - return this; - } - - @Override - public FeatureBuilder addParameter(String name, int value) { - this.parameters.addProperty(name, value); - return this; - } - - @Override - public FeatureBuilder addParameter(String name, float value) { - this.parameters.addProperty(name, value); - return this; - } - - @Override - public FeatureBuilder addParameter(String name, String value) { - this.parameters.addProperty(name, value); - return this; - } - - @Override - public FeatureBuilder setParameters(JsonObject parameters) { - this.parameters = parameters; - return this; - } - - @Override - public FeatureBuilder setDefaultParameters() { - this.parameters = this.feature.getDefaultParameters(); - return this; - } - - @Override - public IFeature getGenerator() { - return this.feature; - } - - @Override - public JsonObject getParameters() { - return this.parameters; - } - - @Override - public String getFeatureName() { - return this.featureName; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/OS3APIImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/OS3APIImpl.java deleted file mode 100644 index c4b8fc9c..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/OS3APIImpl.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Map.Entry; - -import com.google.common.collect.ImmutableMap; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.api.os3.BuilderLogic; -import com.mcmoddev.orespawn.api.os3.DimensionBuilder; -import com.mcmoddev.orespawn.api.os3.OS3API; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.data.ReplacementsRegistry; -import com.mcmoddev.orespawn.util.OS3V2PresetStorage; -import com.mcmoddev.orespawn.worldgen.OreSpawnWorldGen; - -import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; -import net.minecraftforge.fml.common.registry.GameRegistry; - -public class OS3APIImpl implements OS3API { - private final Map logic; - private OreSpawnWorldGen generator; - private OS3V2PresetStorage presets; - - public OS3APIImpl() { - this.logic = new HashMap<>(); - this.presets = new OS3V2PresetStorage(); - } - - @Override - public void registerReplacementBlock(String name, Block itemBlock) { - this.registerReplacementBlock(name, itemBlock.getDefaultState()); - } - - @Override - public void registerReplacementBlock(String name, IBlockState itemBlock) { - ReplacementsRegistry.addBlock(name, itemBlock); - } - - @Override - public void registerFeatureGenerator(String name, String className) { - OreSpawn.FEATURES.addFeature(name, className); - } - - @Override - public void registerFeatureGenerator(String name, IFeature feature) { - this.registerFeatureGenerator(name, feature.getClass().getName()); - } - - @Override - public void registerFeatureGenerator(String name, Class feature) { - this.registerFeatureGenerator(name, feature.getName()); - } - - @Override - public BuilderLogic getLogic(String name) { - if (logic.containsKey(name)) { - return logic.get(name); - } else { - BuilderLogic bl = new BuilderLogicImpl(); - logic.put(name, bl); - return bl; - } - } - - @Override - public void registerLogic(BuilderLogic logic) { - // we do nothing - this is here for orthogonality, really - } - - @Override - public int dimensionWildcard() { - return 0xCAFEBABE; - } - - @Override - public int biomeWildcard() { - return 0xF00DF00D; - } - - @Override - public ImmutableMap getSpawns() { - return ImmutableMap.copyOf(logic); - } - - @Override - public void registerSpawns() { - Map> spawns = OreSpawn.getSpawns(); - - // build a proper tracking of data for the spawner - for (Entry ent : logic.entrySet()) { - for (Entry dL : ent.getValue().getAllDimensions().entrySet()) { - if (spawns.containsKey(dL.getKey())) { - spawns.get(dL.getKey()).addAll(dL.getValue().getAllSpawns()); - } else { - spawns.put(dL.getKey(), new ArrayList<>()); - spawns.get(dL.getKey()).addAll(dL.getValue().getAllSpawns()); - } - } - - OreSpawn.LOGGER.info(String.format("Registered spawn logic from data-file (maybe mod) %s", ent.getKey())); - } - - Random random = new Random(); - - this.generator = new OreSpawnWorldGen(spawns, random.nextLong()); - - GameRegistry.registerWorldGenerator(generator, 100); - - } - - @Override - public OreSpawnWorldGen getGenerator() { - return this.generator; - } - - @Override - public OS3V2PresetStorage getPresets() { - return this.presets; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/OreBuilderImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/OreBuilderImpl.java deleted file mode 100644 index 264f9c51..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/OreBuilderImpl.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.os3.OreBuilder; -import com.mcmoddev.orespawn.util.StateUtil; - -import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.common.registry.ForgeRegistries; - -public class OreBuilderImpl implements OreBuilder { - private IBlockState ore; - private int chance; - - public OreBuilderImpl() { - this.ore = null; - this.chance = 100; - } - @Override - public OreBuilder setOre(String name) { - Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(name)); - - if (block == null) { - OreSpawn.LOGGER.warn("Block {} not found!", name); - return this; - } - - this.ore = block.getDefaultState(); - return this; - } - - @Override - public OreBuilder setOre(String name, String serializedState) { - this.setOre(name); - - if (this.ore == null) { - return this; - } - - this.ore = StateUtil.deserializeState(this.ore.getBlock(), serializedState); - return this; - } - - @SuppressWarnings("deprecation") - @Override - public OreBuilder setOre(String name, int metaData) { - Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(name)); - - if (block == null) { - return this; - } - - this.ore = block.getStateFromMeta(metaData); - return this; - } - - @Override - public OreBuilder setOre(Block base) { - this.ore = base.getDefaultState(); - return this; - } - - @Override - public OreBuilder setOre(Block base, String serializedState) { - this.ore = StateUtil.deserializeState(base, serializedState); - return this; - } - - @SuppressWarnings("deprecation") - @Override - public OreBuilder setOre(Item base, int metaData) { - this.ore = Block.getBlockFromItem(base).getStateFromMeta(metaData); - return this; - } - - @Override - public OreBuilder setOre(ItemStack item) { - return this.setOre(Block.getBlockFromItem(item.getItem())); - } - - @Override - public OreBuilder setOre(String name, String serializedState, int chance) { - return this.setOre(name, serializedState).setChance(chance); - } - - @Override - public OreBuilder setOre(String name, int metaData, int chance) { - return this.setOre(name, metaData).setChance(chance); - } - - @Override - public OreBuilder setOre(Block base, String serializedState, int chance) { - return this.setOre(base, serializedState).setChance(chance); - } - - @Override - public OreBuilder setOre(Item base, int metaData, int chance) { - return this.setOre(base, metaData).setChance(chance); - } - - @Override - public OreBuilder setOre(ItemStack item, int chance) { - return this.setOre(item).setChance(chance); - } - - @Override - public OreBuilder setChance(int chance) { - this.chance = chance; - return this; - } - - - @Override - public IBlockState getOre() { - return this.ore; - } - - @Override - public int getChance() { - return this.chance; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/impl/os3/SpawnBuilderImpl.java b/src/main/java/com/mcmoddev/orespawn/impl/os3/SpawnBuilderImpl.java deleted file mode 100644 index 9c41e2d7..00000000 --- a/src/main/java/com/mcmoddev/orespawn/impl/os3/SpawnBuilderImpl.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.mcmoddev.orespawn.impl.os3; - -import com.google.common.collect.ImmutableList; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.os3.*; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.util.OreList; -import net.minecraft.block.state.IBlockState; -import org.apache.commons.lang3.ArrayUtils; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.*; - -public class SpawnBuilderImpl implements SpawnBuilder { - private BiomeLocation biomeLocs; - private FeatureBuilder featureGen; - private List replacementBlocks; - private List myOres; - private OreList oreList; - private boolean enabled = true; - private boolean retrogen = false; - private boolean extendedDimensionsBool = false; - private DimensionList extendedDimensions; - - SpawnBuilderImpl() { - this.biomeLocs = null; - this.featureGen = null; - this.replacementBlocks = new ArrayList<>(); - this.myOres = new ArrayList<>(); - } - - @Override - public FeatureBuilder newFeatureBuilder(@Nullable String featureName) { - String featName; - this.featureGen = new FeatureBuilderImpl(); - - if (OreSpawn.FEATURES.getFeature(featureName) == null || featureName == null) { - featName = "default"; - } else { - featName = featureName; - } - - this.featureGen.setGenerator(featName); - return this.featureGen; - } - - @Override - public BiomeBuilder newBiomeBuilder() { - return new BiomeBuilderImpl(); - } - - @Override - public OreBuilder newOreBuilder() { - return new OreBuilderImpl(); - } - - @Override - public SpawnBuilder create(@Nonnull BiomeBuilder biomes, @Nonnull FeatureBuilder feature, - @Nonnull List replacements, @Nonnull OreBuilder... ores) { - this.biomeLocs = biomes.getBiomes(); - this.featureGen = feature; - this.replacementBlocks.addAll(replacements); - - if (ores.length > 1) { - this.myOres.addAll(Arrays.asList(ores)); - } else { - this.myOres.add(ores[0]); - } - - return this; - } - - @Override - public SpawnBuilder create(@Nonnull BiomeBuilder biomes, @Nonnull FeatureBuilder feature, - @Nonnull List replacements, JsonObject exDim, - @Nonnull OreBuilder... ores) { - this.create(biomes, feature, replacements, ores); - - this.setupDimensionWhitelist(exDim); - return this; - } - - private void setupDimensionWhitelist(JsonObject exDim) { - JsonArray whitelist = exDim.getAsJsonArray(Constants.ConfigNames.DimensionStuff.INCLUDE); - JsonArray blacklist = exDim.getAsJsonArray(Constants.ConfigNames.DimensionStuff.EXCLUDE); - List tempW = new ArrayList<> (); - List tempB = new ArrayList<> (); - - if (whitelist != null) { - whitelist.forEach(it -> tempW.add(it.getAsInt())); - } - - if (blacklist != null) { - blacklist.forEach(it -> tempB.add(it.getAsInt())); - } - - this.extendedDimensionsBool = true; - this.extendedDimensions = new DimensionListImpl(); - this.extendedDimensions.create(ArrayUtils.toPrimitive(tempW.toArray(new Integer[0])), - ArrayUtils.toPrimitive(tempB.toArray(new Integer[0]))); - } - - @Override - public BiomeLocation getBiomes() { - return this.biomeLocs; - } - - @Override - public ImmutableList getOres() { - return ImmutableList.copyOf(this.myOres); - } - - @Override - public ImmutableList getReplacementBlocks() { - return ImmutableList.copyOf(this.replacementBlocks); - } - - @Override - public FeatureBuilder getFeatureGen() { - return this.featureGen; - } - - private void buildSpawnList() { - if (this.oreList != null) { - return; - } - - this.oreList = new OreList(); - - this.oreList.build(Collections.unmodifiableList(this.myOres)); - } - - @Override - public boolean enabled() { - return this.enabled; - } - - @Override - public void enabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public boolean retrogen() { - return this.retrogen; - } - - @Override - public void retrogen(boolean enabled) { - this.retrogen = enabled; - } - - @Override - public boolean hasExtendedDimensions() { - return this.extendedDimensionsBool; - } - - @Override - public boolean extendedDimensionsMatch(int dimension) { - return !this.extendedDimensionsBool || this.extendedDimensions.match(dimension); - } - - @Override - public OreBuilder getRandomOre(Random rand) { - if (this.oreList == null) { - this.buildSpawnList(); - } - - return this.oreList.getRandomOre(rand); - } - - @Override - public OreList getOreSpawns() { - if (this.oreList == null) { - this.buildSpawnList(); - } - - return this.oreList; - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/OS3Reader.java b/src/main/java/com/mcmoddev/orespawn/json/OS3Reader.java deleted file mode 100644 index d547044c..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/OS3Reader.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.mcmoddev.orespawn.json; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; - -import com.google.common.base.Charsets; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.os3.*; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.data.ReplacementsRegistry; -import com.mcmoddev.orespawn.json.os3.IOS3Reader; -import com.mcmoddev.orespawn.json.os3.readers.*; - -import net.minecraft.crash.CrashReport; -import net.minecraft.item.ItemStack; -import net.minecraft.util.NonNullList; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.common.registry.ForgeRegistries; -import net.minecraftforge.oredict.OreDictionary; -import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; - -public class OS3Reader { - - private OS3Reader() { - - } - private static void loadFeatures(File file) { - OreSpawn.FEATURES.loadFeaturesFile(file); - } - - public static void loadEntries() { - File directory = new File(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3); - JsonParser parser = new JsonParser(); - - if (!directory.exists()) { - directory.mkdirs(); - return; - } - - if (!directory.isDirectory()) { - OreSpawn.LOGGER.fatal("OreSpawn data directory inaccessible - " + directory + " is not a directory!"); - return; - } - - File[] files = directory.listFiles(); - - if (files.length == 0) { - // nothing to load - return; - } - - Path presets = Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF, Constants.FileBits.PRESETS); - loadPresets(presets); - - if (Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF).toFile().exists() && Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF).toFile().isDirectory()) { - Arrays.stream(Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF).toFile().listFiles()) - .filter(file -> "json".equals(FilenameUtils.getExtension(file.getName()))) - .forEach(file -> { - String filename = file.getName(); - - if (FilenameUtils.getBaseName(filename).matches("features-.+")) { - loadFeatures(file); - } else if (FilenameUtils.getBaseName(filename).matches("replacements-.+")) { - Replacements.load(file); - } - }); - } - - Arrays.stream(files).filter(file -> file.getName().endsWith(".json")).forEach( - file -> { - try { - String rawData = FileUtils.readFileToString(file, Charset.defaultCharset()); - - if (rawData.isEmpty()) { - return; - } - - JsonElement full = parser.parse(rawData); - JsonObject parsed = full.getAsJsonObject(); - - String version = parsed.get("version").getAsString(); - IOS3Reader reader = getReader(version); - - if (reader != null) { - finallyParse(reader.parseJson(parsed, FilenameUtils.getBaseName(file.getName())), FilenameUtils.getBaseName(file.getName())); - } - } catch (Exception e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed reading config " + file.getName()); - report.getCategory().addCrashSection(Constants.ORESPAWN_VERSION_CRASH_MESSAGE, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - }); - - } - - private static void loadPresets(Path presets) { - if (presets.toFile().exists()) { - try { - JsonParser parser = new JsonParser(); - String rawJson = FileUtils.readFileToString(presets.toFile(), Charsets.UTF_8); - JsonObject top = parser.parse(rawJson).getAsJsonObject(); - top.entrySet() - .forEach(entry -> { - String section = entry.getKey(); - entry.getValue().getAsJsonObject().entrySet() - .forEach(pres -> OreSpawn.API.getPresets().setSymbolSection(section, pres.getKey(), pres.getValue())); - }); - } catch (IOException exc) { - CrashReport report = CrashReport.makeCrashReport(exc, "Failed reading presets " + presets.toFile().getName()); - report.getCategory().addCrashSection(Constants.ORESPAWN_VERSION_CRASH_MESSAGE, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } catch (JsonParseException ex) { - CrashReport report = CrashReport.makeCrashReport(ex, "Failed loading or parsing " + presets.toFile().getName()); - report.getCategory().addCrashSection(Constants.ORESPAWN_VERSION_CRASH_MESSAGE, Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - } - } - - /** - * Actually parse the normalized data - * @param parseJson normalized data returned by the file loader/normalizer - */ - private static void finallyParse(JsonObject parseJson, String filename) { - JsonObject work = parseJson.getAsJsonObject("dimensions"); - BuilderLogic logic = OreSpawn.API.getLogic(filename); - - // at the top-most level we have the dimension sets - work.entrySet().forEach(entry -> { - int dimension = Integer.parseInt(entry.getKey()); - DimensionBuilder builder = logic.newDimensionBuilder(dimension); - - entry.getValue().getAsJsonArray().forEach(ore -> { - try { - JsonObject nw = ore.getAsJsonObject(); - SpawnBuilder spawn = builder.newSpawnBuilder(null); - // load the "ores" as "OreBuilder" - we should always have a "blocks" here, so... - List blocks = Helpers.loadOres(nw.getAsJsonArray(ConfigNames.BLOCKS), spawn); - List replacements = getReplacements(nw.get(ConfigNames.V2.REPLACES).getAsString(), dimension); - BiomeBuilder biomes = spawn.newBiomeBuilder(); - - if (nw.get(ConfigNames.BIOMES).isJsonObject()) { - biomes.setFromBiomeLocation(Helpers.deserializeBiomeLocationComposition(nw.getAsJsonObject(ConfigNames.BIOMES))); - } - - FeatureBuilder gen = spawn.newFeatureBuilder(nw.get(ConfigNames.FEATURE).getAsString()); - gen.setDefaultParameters(); - handleParameterFixes(nw); - gen.setParameters(nw.getAsJsonObject(ConfigNames.PARAMETERS)); - spawn.enabled(nw.get(ConfigNames.V2.ENABLED).getAsBoolean()); - spawn.retrogen(nw.get(ConfigNames.V2.RETROGEN).getAsBoolean()); - - if (nw.has(ConfigNames.DIMENSION)) { - OreSpawn.LOGGER.fatal("Entry has %s tag with contents: %s", ConfigNames.DIMENSION, nw.getAsJsonObject(ConfigNames.DIMENSION)); - spawn.create(biomes, gen, replacements, nw.getAsJsonObject(ConfigNames.DIMENSION), blocks.toArray(new OreBuilder[0])); - } else { - spawn.create(biomes, gen, replacements, blocks.toArray(new OreBuilder[0])); - } - - builder.create(spawn); - } catch (JsonParseException ex) { - OreSpawn.LOGGER.error(String.format("Error parsing entry %s : %s", ore.getAsJsonObject().get("name").getAsString(), ex)); - } catch (NullPointerException npe) { - OreSpawn.LOGGER.error(String.format("Exception parsing entry %s : possibly mis-named or missing item ?", ore.getAsJsonObject().get("name").getAsString())); - } - }); - logic.create(builder); - }); - } - - private static void handleParameterFixes(JsonObject nw) { - JsonObject p = nw.getAsJsonObject(ConfigNames.PARAMETERS); - - if (p.has(Constants.FormatBits.ATTEMPTS)) { - if (p.get(Constants.FormatBits.ATTEMPTS).isJsonObject()) { - p.add(Constants.FormatBits.ATTEMPTS_MIN, p.getAsJsonObject(Constants.FormatBits.ATTEMPTS).get(ConfigNames.V2.MINIMUM)); - p.add(Constants.FormatBits.ATTEMPTS_MAX, p.getAsJsonObject(Constants.FormatBits.ATTEMPTS).get(ConfigNames.V2.MAXIMUM)); - } else { - p.addProperty(Constants.FormatBits.ATTEMPTS_MIN, p.get(Constants.FormatBits.ATTEMPTS).getAsInt()); - p.addProperty(Constants.FormatBits.ATTEMPTS_MAX, p.get(Constants.FormatBits.ATTEMPTS).getAsInt()); - } - - p.remove(Constants.FormatBits.ATTEMPTS); - nw.remove(ConfigNames.PARAMETERS); - nw.add(ConfigNames.PARAMETERS, p); - } - } - - private static List getReplacements(String configField, int dimension) { - String work = configField.toLowerCase(); - - if (work.equals(ConfigNames.DEFAULT)) { - return ReplacementsRegistry.getDimensionDefault(dimension); - } else if (work.startsWith("ore:")) { - NonNullList ores = OreDictionary.getOres(work.substring(4)); - List reps = new ArrayList<>(); - ores.forEach(ore -> reps.add(Block.getBlockFromItem(ore.getItem()).getDefaultState())); - return reps; - } else if (!work.contains(":")) { // probably a "replacements registry" entry - return Arrays.asList(ReplacementsRegistry.getBlock(work)); - } else { - return Arrays.asList(ForgeRegistries.BLOCKS.getValue(new ResourceLocation(configField)).getDefaultState()); - } - } - - public static IOS3Reader getReader(String version) { - switch (version) { - case "1": - case "1.1": - case "1.2": - return new OS3V1Reader(); - - case "2.0": - return new OS3V2Reader(); - - default: - OreSpawn.LOGGER.error("Unknown version %s", version); - return null; - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/OS3Writer.java b/src/main/java/com/mcmoddev/orespawn/json/OS3Writer.java index f8fe60c9..397dbeff 100644 --- a/src/main/java/com/mcmoddev/orespawn/json/OS3Writer.java +++ b/src/main/java/com/mcmoddev/orespawn/json/OS3Writer.java @@ -1,227 +1,4 @@ package com.mcmoddev.orespawn.json; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map.Entry; - -import org.apache.commons.io.FileUtils; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.os3.BuilderLogic; -import com.mcmoddev.orespawn.api.os3.DimensionBuilder; -import com.mcmoddev.orespawn.api.os3.OreBuilder; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.impl.location.*; -import com.mcmoddev.orespawn.util.StateUtil; - -import net.minecraft.crash.CrashReport; -import net.minecraftforge.fml.common.registry.ForgeRegistries; - -public class OS3Writer { - private void writeFeatures(String base) { - File file = new File(Paths.get(base, Constants.FileBits.SYSCONF).toString(), "features-default.json"); - OreSpawn.FEATURES.writeFeatures(file); - } - - private void writeReplacements(String base) { - File file = new File(Paths.get(base, Constants.FileBits.SYSCONF).toString(), "replacements-default.json"); - Replacements.save(file); - } - - public void writeSpawnEntries() { - String basePath = Paths.get("config", "orespawn3", "force-written").toString(); - writeFeatures(basePath); - writeReplacements(basePath); - - OreSpawn.API.getSpawns().entrySet().forEach(ent -> { - JsonArray dimensions = new JsonArray(); - - for (Entry dim : ent.getValue().getAllDimensions().entrySet()) { - JsonObject dimension = new JsonObject(); - - if (dim.getKey() != OreSpawn.API.dimensionWildcard()) { - dimension.addProperty(ConfigNames.DIMENSION, String.format("%d", dim.getKey())); - } - - JsonArray spawns = this.genSpawns(dim.getValue().getAllSpawns()); - - if (spawns.size() > 0) { - dimension.add(ConfigNames.ORES, spawns); - dimensions.add(dimension); - } - } - - if (countOres(dimensions) > 0) { - File file = new File(basePath, String.format("%s.json", ent.getKey())); - JsonObject wrapper = new JsonObject(); - wrapper.addProperty(ConfigNames.FILE_VERSION, "1.2"); - wrapper.add(ConfigNames.DIMENSIONS, dimensions); - this.writeFile(file, wrapper); - } - }); - } - - private JsonArray genSpawns(ImmutableList allSpawns) { - JsonArray rv = new JsonArray(); - - for (SpawnBuilder spawn : allSpawns) { - if (spawn.getOres().isEmpty() || - spawn.getOres().get(0).getOre() == null || - "minecraft:air".equals(spawn.getOres().get(0).getOre().getBlock().getRegistryName().toString())) { - continue; - } - - rv.add(this.genSpawn(spawn)); - } - - return rv; - } - - private void writeFile(File file, JsonObject wrapper) { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - try { - FileUtils.writeStringToFile(file, gson.toJson(wrapper), "UTF8", false); - } catch (IOException e) { - CrashReport report = CrashReport.makeCrashReport(e, String.format("Failed in config %s", file.getName())); - report.getCategory().addCrashSection("OreSpawn Version", Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - } - - private JsonObject genSpawn(SpawnBuilder spawn) { - JsonObject ore = new JsonObject(); - - ore.add(ConfigNames.BLOCKS, genBlocks(spawn.getOres())); - ore.add(ConfigNames.PARAMETERS, spawn.getFeatureGen().getParameters()); - ore.addProperty(ConfigNames.FEATURE, spawn.getFeatureGen().getFeatureName()); - ore.addProperty(ConfigNames.REPLACEMENT, ConfigNames.DEFAULT); - ore.add(ConfigNames.BIOMES, biomeLocationToJsonObject(spawn.getBiomes())); - return ore; - } - - private JsonArray genBlocks(ImmutableList ores) { - JsonArray retval = new JsonArray(); - - ores.forEach(ore -> { - JsonObject obj = new JsonObject(); - obj.addProperty(ConfigNames.BLOCK, ore.getOre().getBlock().getRegistryName().toString()); - obj.addProperty(ConfigNames.STATE, StateUtil.serializeState(ore.getOre())); - obj.addProperty(ConfigNames.CHANCE, ore.getChance()); - retval.add(obj); - }); - return retval; - } - - private int countOres(JsonArray dims) { - int count = 0; - - for (JsonElement dim : dims) { - count += dim.getAsJsonObject().get(ConfigNames.ORES).getAsJsonArray().size(); - } - - return count; - } - - private JsonElement biomeLocationToJsonObject(BiomeLocation value) { - if ((value instanceof BiomeLocationSingle) || (value instanceof BiomeLocationDictionary)) { - return getString(value); - } else if (value instanceof BiomeLocationList) { - return getList(value); - } else if (value instanceof BiomeLocationComposition) { - return getComposition(value); - } - - return null; - } - - private JsonElement getComposition(BiomeLocation value) { - JsonObject rv = new JsonObject(); - rv.add(ConfigNames.BiomeStuff.WHITELIST, getList(new BiomeLocationList(((BiomeLocationComposition)value).getInclusions()))); - rv.add(ConfigNames.BiomeStuff.BLACKLIST, getList(new BiomeLocationList(((BiomeLocationComposition)value).getExclusions()))); - return rv; - } - - private JsonArray getList(BiomeLocation value) { - JsonArray rv = new JsonArray(); - ((BiomeLocationList)value).getLocations().forEach(loc -> { - if ((loc instanceof BiomeLocationSingle) || (loc instanceof BiomeLocationDictionary)) { - rv.add(getString(loc)); - } else if (loc instanceof BiomeLocationComposition) { - rv.add(getComposition(loc)); - } - }); - return rv; - } - - private JsonElement getString(BiomeLocation value) { - String val = null; - - if (value instanceof BiomeLocationSingle) { - val = ForgeRegistries.BIOMES.getKey(((BiomeLocationSingle)value).getBiome()).toString(); - } else { - val = ((BiomeLocationDictionary)value).getType().toString(); - } - - return new JsonPrimitive(val); - } - - public void writeSysconfIfNonexistent() { - String base = String.format(".%1$sconfig%1$sorespawn3", File.separator); - - if (!Paths.get(base, Constants.FileBits.SYSCONF, "features-default.json").toFile().exists()) { - writeFeatures(base); - } - - if (!Paths.get(base, Constants.FileBits.SYSCONF, "replacements-default.json").toFile().exists()) { - writeReplacements(base); - } - } - - public void writeAddOreEntry(String fileName) { - BuilderLogic ent = OreSpawn.API.getLogic(fileName); - - JsonArray dimensions = new JsonArray(); - - for (Entry dim : ent.getAllDimensions().entrySet()) { - JsonObject dimension = new JsonObject(); - - if (dim.getKey() != OreSpawn.API.dimensionWildcard()) { - dimension.addProperty(ConfigNames.DIMENSION, String.format("%d", dim.getKey())); - } - - JsonArray spawns = this.genSpawns(dim.getValue().getAllSpawns()); - - if (spawns.size() > 0) { - dimension.add(ConfigNames.ORES, spawns); - dimensions.add(dimension); - } - } - - if (countOres(dimensions) > 0) { - Path p = Paths.get("config", "orespawn3", "force-written"); - - if (!p.toFile().exists()) { - p.toFile().mkdirs(); - } - - File file = Paths.get(p.toString(), String.format("%s-addOre.json", fileName)).toFile(); - JsonObject wrapper = new JsonObject(); - wrapper.addProperty(ConfigNames.FILE_VERSION, "1.2"); - wrapper.add(ConfigNames.DIMENSIONS, dimensions); - this.writeFile(file, wrapper); - } - } -} \ No newline at end of file +/** Deprecated marker retained for the public OS3 3.2 facade field. */ +public class OS3Writer { public OS3Writer() { } } diff --git a/src/main/java/com/mcmoddev/orespawn/json/OreSpawnReader.java b/src/main/java/com/mcmoddev/orespawn/json/OreSpawnReader.java deleted file mode 100644 index dad12301..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/OreSpawnReader.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.mcmoddev.orespawn.json; - -import java.io.File; -import java.nio.charset.Charset; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.json.os3.IOS3Reader; - -import net.minecraft.crash.CrashReport; - -public class OreSpawnReader { - private List spawns; - - public OreSpawnReader() { - this.spawns = new LinkedList<>(); - } - - public void loadSpawnData() { - // first we parse the OS3 format spawns, as we prefer them over other versions - parseSpawnsV3(); - - } - - private void parseSpawnsV3() { - File directory = new File(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3); - File[] files; - - if (!directory.exists()) { - return; - } - - if (!directory.isDirectory()) { - OreSpawn.LOGGER.fatal("OreSpawn data directory inaccessible - " + directory + " is not a directory!"); - return; - } - - files = directory.listFiles(); - - if (files.length == 0) { - // nothing to load - return; - } - - loadFeaturesAndReplacements(); - loadSpawns(files); - } - - private void loadSpawns(File[] files) { - JsonParser parser = new JsonParser(); - Arrays.stream(files).filter(file -> file.getName().endsWith(".json")).forEach( - file -> { - try { - String rawData = FileUtils.readFileToString(file, Charset.defaultCharset()); - - if (rawData.isEmpty()) { - return; - } - - JsonElement full = parser.parse(rawData); - JsonObject parsed = full.getAsJsonObject(); - - String version = parsed.get("version").getAsString(); - IOS3Reader reader = OS3Reader.getReader(version); - - spawns.add(reader.parseJson(parsed, file.getName().substring(0, file.getName().lastIndexOf('.')))); - } catch (Exception e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed reading config " + file.getName()); - report.getCategory().addCrashSection("OreSpawn Version", Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - } - }); - } - - private void loadFeaturesAndReplacements() { - if (Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF).toFile().exists() && Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF).toFile().isDirectory()) { - Arrays.stream(Paths.get(Constants.FileBits.CONFIG_DIR, Constants.FileBits.OS3, Constants.FileBits.SYSCONF).toFile().listFiles()) - .filter(file -> "json".equals(FilenameUtils.getExtension(file.getName()))) - .forEach(file -> { - String filename = file.getName(); - - if (FilenameUtils.getBaseName(filename).matches("features-.+")) { - OreSpawn.FEATURES.loadFeaturesFile(file); - } else if (FilenameUtils.getBaseName(filename).matches("replacements-.+")) { - Replacements.load(file); - } - }); - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/Replacements.java b/src/main/java/com/mcmoddev/orespawn/json/Replacements.java deleted file mode 100644 index 32f8c034..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/Replacements.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.mcmoddev.orespawn.json; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.Map; -import java.util.Map.Entry; - -import org.apache.commons.codec.CharEncoding; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringEscapeUtils; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.data.ReplacementsRegistry; -import com.mcmoddev.orespawn.util.StateUtil; - -import net.minecraft.block.state.IBlockState; -import net.minecraft.crash.CrashReport; - -public class Replacements { - private Replacements() { - - } - public static void load(File file) { - JsonParser parser = new JsonParser(); - String rawJson = "[]"; - JsonArray elements; - - try { - rawJson = FileUtils.readFileToString(file, Charset.defaultCharset()); - } catch (IOException e) { - CrashReport report = CrashReport.makeCrashReport(e, "Failed reading config " + file.getName()); - report.getCategory().addCrashSection("OreSpawn Version", Constants.VERSION); - OreSpawn.LOGGER.info(report.getCompleteReport()); - return; - } - - elements = parser.parse(rawJson).getAsJsonArray(); - - for (JsonElement elem : elements) { - JsonObject obj = elem.getAsJsonObject(); - String name = obj.get("name").getAsString(); - String blockName = obj.get("blockName").getAsString(); - String blockState = obj.get("blockState").getAsString(); - ReplacementsRegistry.addBlock(name, blockName, blockState); - } - } - - public static void save(File file) { - Map blocks = ReplacementsRegistry.getBlocks(); - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - - if (blocks != null) { - JsonArray root = new JsonArray(); - - for (Entry block : blocks.entrySet()) { - JsonObject entry = new JsonObject(); - entry.addProperty("name", block.getKey()); - entry.addProperty("blockName", block.getValue().getBlock().getRegistryName().toString()); - entry.addProperty("blockState", StateUtil.serializeState(block.getValue())); - root.add(entry); - } - - String json = gson.toJson(root); - - try { - FileUtils.writeStringToFile(file, StringEscapeUtils.unescapeJson(json), CharEncoding.UTF_8); - } catch (IOException e) { - OreSpawn.LOGGER.fatal("Error writing " + file.toString() + " - " + e.getLocalizedMessage()); - } - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/os3/IOS3Reader.java b/src/main/java/com/mcmoddev/orespawn/json/os3/IOS3Reader.java deleted file mode 100644 index 995cb738..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/os3/IOS3Reader.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.mcmoddev.orespawn.json.os3; - -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.Map.Entry; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.util.OS3V2PresetStorage; - -public interface IOS3Reader { - -default OS3V2PresetStorage getStorage() { - return null; - } - - JsonObject parseJson(JsonObject entries, String fileName); - - -default void copyOverSingleBlock(JsonObject ore, JsonObject oreOut) { - JsonObject blockEntry = new JsonObject(); - - for (Entry prop : ore.entrySet()) { - switch (prop.getKey()) { - case ConfigNames.BLOCK: - case ConfigNames.BLOCKID: - blockEntry.addProperty(ConfigNames.BLOCK_V2, prop.getValue().getAsString()); - break; - - case ConfigNames.METADATA: - blockEntry.addProperty(ConfigNames.METADATA, prop.getValue().getAsNumber()); - break; - - case ConfigNames.STATE: - blockEntry.addProperty(ConfigNames.STATE, prop.getValue().getAsString()); - break; - - case ConfigNames.CHANCE: - blockEntry.addProperty(ConfigNames.CHANCE, prop.getValue().getAsNumber()); - break; - - case ConfigNames.REPLACEMENT: - oreOut.addProperty(ConfigNames.REPLACEMENT_V2, prop.getValue().getAsString()); - break; - - default: - if (!oreOut.has(prop.getKey())) { - oreOut.add(prop.getKey(), prop.getValue()); - } - } - } - - if (!blockEntry.has(ConfigNames.CHANCE)) { - blockEntry.addProperty(ConfigNames.CHANCE, 100); - } - - if (!oreOut.has(ConfigNames.BLOCKS)) { - oreOut.add(ConfigNames.BLOCKS, new JsonArray()); - } - - JsonArray temp = oreOut.getAsJsonArray(ConfigNames.BLOCKS); - temp.add(blockEntry); - oreOut.add(ConfigNames.BLOCKS, temp); - } - -default void normalizeBlockData(JsonObject ore, JsonObject oreOut) { - JsonObject blockEntry = new JsonObject(); - String blockName; - - if (ore.has(ConfigNames.STATE)) { - blockEntry.addProperty(ConfigNames.STATE, ore.get(ConfigNames.STATE).getAsString()); - } else if (ore.has(ConfigNames.METADATA)) { - blockEntry.addProperty(ConfigNames.METADATA, ore.get(ConfigNames.METADATA).getAsInt()); - } - - if (ore.has(ConfigNames.BLOCKID)) { - blockName = ore.get(ConfigNames.BLOCKID).getAsString(); - } else if (ore.has(ConfigNames.BLOCK)) { - blockName = ore.get(ConfigNames.BLOCK).getAsString(); - } else { - blockName = "i_am_a_dumbass"; - } - - blockEntry.addProperty(ConfigNames.BLOCK_V2, blockName); - - oreOut.add(ConfigNames.BLOCKS, blockEntry); - } - -default JsonArray getDimensionData(JsonObject retVal, int dimension) { - if (!retVal.has("dimensions")) { - retVal.add("dimensions", new JsonObject()); - } - - if (!retVal.getAsJsonObject("dimensions").has(String.format("%d", dimension))) { - retVal.getAsJsonObject("dimensions").add(String.format("%d", dimension), new JsonArray()); - } - - return retVal.getAsJsonObject("dimensions").getAsJsonArray(String.format("%d", dimension)); - } - - /** - * called with a JsonObject that has to be iterated, copied and all variable references replaced with the data they - * refer to - * @param objReplace object to do variable replacement on - * @return copy of objReplace with all variables interpolated in - */ -default JsonObject replaceVariables(JsonObject objReplace) { - JsonObject retVal = new JsonObject(); - - objReplace.entrySet().forEach(entry -> retVal.add(entry.getKey(), replaceVariablesBase(entry.getValue()))); - - return retVal; - } - -default JsonElement replaceVariablesBase(JsonElement value) { - if (value.isJsonPrimitive() && value.getAsString().startsWith("$.")) { - return replaceVariablePrimitive(value); - } else if (value.isJsonPrimitive()) { - return value; - } else if (value.isJsonArray()) { - return replaceVariableArray(value); - } else if (value.isJsonObject()) { - return replaceVariables(value.getAsJsonObject()); - } else { - return value; - } - } - -default JsonElement replaceVariableArray(JsonElement value) { - JsonArray retVal = new JsonArray(); - - value.getAsJsonArray().forEach(item -> retVal.add(replaceVariablesBase(item))); - return retVal; - } - - // variable looks like: "$.
." -default JsonElement replaceVariablePrimitive(JsonElement value) { - String rawVal = value.getAsString().substring(2); - String[] bits = rawVal.split("\\."); - String section = bits[0]; - String item = bits[1]; - return getStorage().getSymbolSection(section, item); - } - -default String getBlockName(JsonObject ore) { - String key; - - if (ore.has(Constants.ConfigNames.BLOCK)) { - key = Constants.ConfigNames.BLOCK; - } else if (ore.has(Constants.ConfigNames.BLOCKID)) { - key = Constants.ConfigNames.BLOCKID; - } else { - return String.format("ore-%d", new Random().nextInt()); - } - - return ore.get(key).getAsString(); - } - -default String getBlockNameMulti(JsonObject ore) { - List rv = new LinkedList<>(); - JsonArray ores = ore.get(Constants.ConfigNames.BLOCKS).getAsJsonArray(); - - for (JsonElement o : ores) { - rv.add(o.getAsJsonObject().get(Constants.ConfigNames.BLOCK_V2).getAsString()); - } - - return String.join("-", rv.toArray(new String[0])); - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/os3/readers/Helpers.java b/src/main/java/com/mcmoddev/orespawn/json/os3/readers/Helpers.java deleted file mode 100644 index d61ccbb1..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/os3/readers/Helpers.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.mcmoddev.orespawn.json.os3.readers; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import com.google.common.collect.ImmutableSet; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.api.BiomeLocation; -import com.mcmoddev.orespawn.api.os3.OreBuilder; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.impl.location.BiomeLocationComposition; -import com.mcmoddev.orespawn.impl.location.BiomeLocationDictionary; -import com.mcmoddev.orespawn.impl.location.BiomeLocationList; -import com.mcmoddev.orespawn.impl.location.BiomeLocationSingle; - -import net.minecraft.item.ItemStack; -import net.minecraft.util.NonNullList; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.common.BiomeDictionary; -import net.minecraftforge.fml.common.registry.ForgeRegistries; -import net.minecraftforge.oredict.OreDictionary; - -public class Helpers { - - private Helpers() {} - - private static BiomeLocation deserializeSingleEntry(String in) { - if (in.contains(":")) { - String[] parts = in.split(":"); - return new BiomeLocationSingle(ForgeRegistries.BIOMES.getValue(new ResourceLocation(parts[0], parts[1]))); - } else { - return new BiomeLocationDictionary(BiomeDictionary.Type.getType(in)); - } - } - - private static BiomeLocation deserializeBiomeLocationList(JsonArray in) { - List myData = new ArrayList<>(); - - if (in.size() == 0) { - return new BiomeLocationList(ImmutableSet.copyOf(Collections.emptySet())); - } - - in.forEach(elem -> { - if (elem.isJsonPrimitive()) { - myData.add(deserializeSingleEntry(elem.getAsString())); - } else if (elem.isJsonObject()) { - myData.add(deserializeBiomeLocationComposition(elem.getAsJsonObject())); - } - }); - - return new BiomeLocationList(ImmutableSet.copyOf(myData)); - } - - public static BiomeLocationComposition deserializeBiomeLocationComposition(JsonObject in) { - JsonArray includeArr = in.getAsJsonArray(ConfigNames.BiomeStuff.WHITELIST); - JsonArray excludeArr = in.getAsJsonArray(ConfigNames.BiomeStuff.BLACKLIST); - - if (includeArr == null) { - includeArr = new JsonArray(); - } - - if (excludeArr == null) { - excludeArr = new JsonArray(); - } - - BiomeLocation includes = null; - BiomeLocation excludes = null; - - if (includeArr.size() > 0) { - includes = deserializeBiomeLocationList(includeArr); - } - - if (excludeArr.size() > 0) { - excludes = deserializeBiomeLocationList(excludeArr); - } - - return new BiomeLocationComposition((includes == null) ? ImmutableSet.copyOf(Collections.emptySet()) : ImmutableSet.of(includes), - (excludes == null) ? ImmutableSet.copyOf(Collections.emptySet()) : ImmutableSet.of(excludes)); - } - - private static void handleState(JsonObject ore, OreBuilder oreB, String oreName) { - if (ore.has(ConfigNames.STATE)) { - String stateString = ore.get(ConfigNames.STATE).getAsString(); - - if (ConfigNames.STATE_NORMAL.equals(stateString)) { - oreB.setOre(oreName); - } else { - oreB.setOre(oreName, stateString); - } - } else { - if (ore.has(ConfigNames.METADATA)) { - oreB.setOre(oreName, ore.get(ConfigNames.METADATA).getAsInt()); - } else { - oreB.setOre(oreName); - } - } - } - - private static OreBuilder parseOreEntry(JsonObject oreSpawn, SpawnBuilder spawn) { - String blockName = oreSpawn.has(ConfigNames.BLOCK) ? ConfigNames.BLOCK : ConfigNames.BLOCK_V2; - String oreName = oreSpawn.get(blockName).getAsString(); - int chance = oreSpawn.has(ConfigNames.CHANCE) ? oreSpawn.get(ConfigNames.CHANCE).getAsInt() : 100; - - OreBuilder thisOre = spawn.newOreBuilder(); - - handleState(oreSpawn, thisOre, oreName); - - thisOre.setChance(chance); - - return thisOre; - } - - private static List loadOreDict(JsonObject oreObj, SpawnBuilder spawn) { - String oreName = oreObj.get(ConfigNames.BLOCK).getAsString().split(":")[1]; - int chance = oreObj.has(ConfigNames.CHANCE) ? oreObj.get(ConfigNames.CHANCE).getAsInt() : 100; - List retval = new ArrayList<>(); - - NonNullList ores = OreDictionary.getOres(oreName); - - for (ItemStack ore : ores) { - OreBuilder thisOre = spawn.newOreBuilder(); - thisOre.setOre(ore.getItem(), ore.getMetadata()); - thisOre.setChance(chance); - retval.add(thisOre); - } - - return retval; - } - - public static List loadOres(JsonArray oresArray, SpawnBuilder spawn) { - List rV = new LinkedList<>(); - - oresArray.forEach(oreEntry -> { - JsonObject work = oreEntry.getAsJsonObject(); - String oreName = work.get(ConfigNames.BLOCK_V2).getAsString(); - OreBuilder ores = spawn.newOreBuilder(); - - if (work.has(ConfigNames.STATE) || work.has(ConfigNames.METADATA)) { - Helpers.handleState(work, ores, oreName); - rV.add(ores); - } else { - if (oreName.toLowerCase().startsWith("ore:")) { - rV.addAll(Helpers.loadOreDict(work, spawn)); - } else { - rV.add(Helpers.parseOreEntry(work, spawn)); - } - } - }); - - return rV; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/os3/readers/OS3V1Reader.java b/src/main/java/com/mcmoddev/orespawn/json/os3/readers/OS3V1Reader.java deleted file mode 100644 index 13129a6f..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/os3/readers/OS3V1Reader.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.mcmoddev.orespawn.json.os3.readers; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.json.os3.IOS3Reader; - -public final class OS3V1Reader implements IOS3Reader { - - @Override - public JsonObject parseJson(JsonObject entries, String fileName) { - JsonArray elements = entries.get(ConfigNames.DIMENSIONS).getAsJsonArray(); - - JsonObject retVal = new JsonObject(); - - for (JsonElement element : elements) { - JsonObject object = element.getAsJsonObject(); - JsonArray dimData; - int dimension = object.has(ConfigNames.DIMENSION) ? object.get(ConfigNames.DIMENSION).getAsInt() : OreSpawn.API.dimensionWildcard(); - - dimData = getDimensionData(retVal, dimension); - - JsonArray ores = object.get(ConfigNames.ORES).getAsJsonArray(); - - for (JsonElement oresEntry : ores) { - JsonObject ore = oresEntry.getAsJsonObject(); - ore.addProperty("retrogen", true); - ore.addProperty("enabled", true); - - JsonObject oreOut = handleVersionDifferences(ore, entries.get(ConfigNames.FILE_VERSION).getAsString()); - dimData.add(oreOut); - } - - retVal.getAsJsonObject("dimensions").add(Integer.toString(dimension), dimData); - } - - return retVal; - } - - private JsonObject handleVersionDifferences(JsonObject ore, String version) { - JsonObject returnValue = new JsonObject(); - - if ("1".equals(version) || "1.1".equals(version)) { - copyOverSingleBlock(ore, returnValue); - } - - switch (version) { - case "1.2": - ore.entrySet().forEach(prop -> returnValue.add(prop.getKey(), prop.getValue())); - break; - - case "1.1": - JsonArray biomes = ore.has(ConfigNames.BIOMES) ? ore.getAsJsonArray(ConfigNames.BIOMES) : new JsonArray(); - JsonObject biomeObj = new JsonObject(); - biomeObj.add(biomes.size() < 1 ? ConfigNames.BiomeStuff.BLACKLIST : ConfigNames.BiomeStuff.WHITELIST, biomes); - returnValue.add(ConfigNames.BIOMES, biomeObj); - returnValue.add(ConfigNames.BIOMES, new JsonObject()); - returnValue.addProperty(ConfigNames.BLOCK_V2, getBlockName(ore)); - break; - - case "1": - returnValue.add(ConfigNames.BIOMES, new JsonObject()); - returnValue.addProperty(ConfigNames.BLOCK_V2, getBlockName(ore)); - break; - - default: - break; - } - - return returnValue; - } - -} diff --git a/src/main/java/com/mcmoddev/orespawn/json/os3/readers/OS3V2Reader.java b/src/main/java/com/mcmoddev/orespawn/json/os3/readers/OS3V2Reader.java deleted file mode 100644 index 0062896d..00000000 --- a/src/main/java/com/mcmoddev/orespawn/json/os3/readers/OS3V2Reader.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.mcmoddev.orespawn.json.os3.readers; - -import java.util.Map.Entry; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.data.Constants.ConfigNames; -import com.mcmoddev.orespawn.json.os3.IOS3Reader; -import com.mcmoddev.orespawn.util.OS3V2PresetStorage; - -public class OS3V2Reader implements IOS3Reader { - private OS3V2PresetStorage storage = new OS3V2PresetStorage(); - - @Override - public OS3V2PresetStorage getStorage() { - return this.storage; - } - - @Override - public JsonObject parseJson(JsonObject entries, String fileName) { - // do we have any presets ? - JsonObject spawns = entries.getAsJsonObject("spawns"); - JsonObject retVal = new JsonObject(); - - storage.clear(); - OreSpawn.API.getPresets().copy(storage); - - loadLocalPresets(entries); - - // 'work' contains the reduced, no-variables-here data - JsonObject work = new JsonObject(); - spawns.entrySet().forEach(entry -> work.add(entry.getKey(), replaceVariables(entry.getValue().getAsJsonObject()))); - - work.entrySet().forEach(entry -> { - JsonObject lw = setHandleDimensions(entry.getValue().getAsJsonObject()); - - lw.getAsJsonArray(ConfigNames.DIMENSIONS).getAsJsonArray().forEach( - dim -> { - JsonObject nw = new JsonObject(); - nw.addProperty("name", entry.getKey()); - - if (lw.has(ConfigNames.DIMENSION)) - nw.add(ConfigNames.DIMENSION, lw.getAsJsonObject(ConfigNames.DIMENSION)); - - entry.getValue().getAsJsonObject().entrySet().stream() - .filter(e -> !e.getKey().equals(ConfigNames.DIMENSIONS)) - .forEach(ent -> nw.add(ent.getKey(), ent.getValue())); - JsonArray thisDim = getDimensionData(retVal, dim.getAsInt()); - thisDim.add(nw); - - JsonObject dimStore; - if (retVal.has(ConfigNames.DIMENSIONS)) { - dimStore = retVal.getAsJsonObject(ConfigNames.DIMENSIONS); - } else { - dimStore = new JsonObject(); - } - dimStore.add(dim.getAsString(), thisDim); - retVal.add(ConfigNames.DIMENSIONS, dimStore); - }); - }); - - return retVal; - } - - private JsonObject setHandleDimensions(JsonObject spawnEntry) { - JsonObject lw = spawnEntry; - - if (lw.get(ConfigNames.DIMENSIONS).isJsonArray()) { - if (lw.getAsJsonArray(ConfigNames.DIMENSIONS).size() < 1) { - JsonArray temp = lw.getAsJsonArray(ConfigNames.DIMENSIONS); - temp.add(new JsonPrimitive(OreSpawn.API.dimensionWildcard())); - lw.remove(ConfigNames.DIMENSIONS); - lw.add(ConfigNames.DIMENSIONS, temp); - } - } else { - JsonObject dimSet = lw.getAsJsonObject(ConfigNames.DIMENSIONS); - lw.add(ConfigNames.DIMENSION, dimSet); - lw.remove(ConfigNames.DIMENSIONS); - JsonArray temp = new JsonArray(); - temp.add(new JsonPrimitive(OreSpawn.API.dimensionWildcard())); - lw.add(ConfigNames.DIMENSIONS, temp); - } - - return lw; - } - - private void loadLocalPresets(JsonObject entries) { - boolean hasPresets = entries.has("presets"); - - if (hasPresets) { - for (Entry preset : entries.get("presets").getAsJsonObject().entrySet()) { - String sectionName = preset.getKey(); - JsonObject entry = preset.getValue().getAsJsonObject(); - - for (Entry variables : entry.entrySet()) { - String itemName = variables.getKey(); - JsonElement varValue = variables.getValue(); - storage.setSymbolSection(sectionName, itemName, varValue); - } - } - } - } -} diff --git a/src/main/java/com/mcmoddev/orespawn/util/Collectors2.java b/src/main/java/com/mcmoddev/orespawn/util/Collectors2.java index e71d3aeb..ff9a3d32 100644 --- a/src/main/java/com/mcmoddev/orespawn/util/Collectors2.java +++ b/src/main/java/com/mcmoddev/orespawn/util/Collectors2.java @@ -5,22 +5,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +/** Deprecated OS3 collection helpers retained for binary consumers. */ +@Deprecated public final class Collectors2 { - private Collectors2() {} - + private Collectors2() { } public static Collector, ImmutableList> toImmutableList() { - return Collector.of( - ImmutableList.Builder::new, ImmutableList.Builder::add, - (left, right) -> left.addAll(right.build()), - ImmutableList.Builder::build - ); + return Collector.of(ImmutableList.Builder::new, ImmutableList.Builder::add, + (left, right) -> left.addAll(right.build()), ImmutableList.Builder::build); } - public static Collector, ImmutableSet> toImmutableSet() { - return Collector.of( - ImmutableSet.Builder::new, ImmutableSet.Builder::add, - (left, right) -> left.addAll(right.build()), - ImmutableSet.Builder::build - ); + return Collector.of(ImmutableSet.Builder::new, ImmutableSet.Builder::add, + (left, right) -> left.addAll(right.build()), ImmutableSet.Builder::build); } } diff --git a/src/main/java/com/mcmoddev/orespawn/util/OS3V2PresetStorage.java b/src/main/java/com/mcmoddev/orespawn/util/OS3V2PresetStorage.java index 0f577b4b..5d4b4a8e 100644 --- a/src/main/java/com/mcmoddev/orespawn/util/OS3V2PresetStorage.java +++ b/src/main/java/com/mcmoddev/orespawn/util/OS3V2PresetStorage.java @@ -1,42 +1,20 @@ package com.mcmoddev.orespawn.util; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -import java.util.TreeMap; import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; +import com.google.gson.JsonParser; +/** Deprecated OS3 3.2 preset container. */ public class OS3V2PresetStorage { - private final Map> storage; - - public OS3V2PresetStorage() { - storage = new TreeMap<>(); - } - - public void setSymbolSection(String sectionName, String itemName, JsonElement value) { - Map temp = storage.getOrDefault(sectionName, new HashMap()); - temp.put(itemName, value); - storage.put(sectionName, temp); - } - - public JsonElement getSymbolSection(String sectionName, String itemName) { - if (storage.containsKey(sectionName) && storage.get(sectionName).containsKey(itemName)) { - return storage.get(sectionName).get(itemName); - } else { - return new JsonPrimitive(itemName); - } - } - - public void copy(OS3V2PresetStorage dest) { - storage.entrySet().stream() - .forEach(ensm -> { - String section = ensm.getKey(); - ensm.getValue().entrySet().forEach(ensje -> dest.setSymbolSection(section, ensje.getKey(), ensje.getValue())); - }); + private final Map> storage = new LinkedHashMap<>(); + public void setSymbolSection(String symbol, String section, JsonElement value) { + storage.computeIfAbsent(symbol, key -> new LinkedHashMap<>()).put(section, new JsonParser().parse(value.toString())); } - - public void clear() { - this.storage.clear(); + public JsonElement getSymbolSection(String symbol, String section) { + Map values = storage.get(symbol); return values == null ? null : values.get(section); } + public void copy(OS3V2PresetStorage source) { clear(); source.storage.forEach((s, values) -> values.forEach((k, v) -> setSymbolSection(s, k, v))); } + public void clear() { storage.clear(); } } diff --git a/src/main/java/com/mcmoddev/orespawn/util/OreList.java b/src/main/java/com/mcmoddev/orespawn/util/OreList.java index 1ee99ac9..b496ac80 100644 --- a/src/main/java/com/mcmoddev/orespawn/util/OreList.java +++ b/src/main/java/com/mcmoddev/orespawn/util/OreList.java @@ -1,70 +1,40 @@ package com.mcmoddev.orespawn.util; -import java.util.Comparator; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Random; -import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import com.mcmoddev.orespawn.api.os3.OreBuilder; + import net.minecraft.block.state.IBlockState; +/** OS3 3.2 weighted output container retained for unchanged feature binaries. */ public class OreList { - private List myChanceList; - private List myCopy; - - private Integer listCount = 0; - - public OreList() { - this.myChanceList = new LinkedList<>(); - this.myCopy = new LinkedList<>(); - } + private final List values = new ArrayList<>(); + private int total; public void build(List ores) { - ores.stream().sorted(Comparator.comparingInt(OreBuilder::getChance)) - .forEach(ob -> { myChanceList.add(ob.getChance()); myCopy.add(ob); }); - - this.listCount = myChanceList.stream().mapToInt(Integer::intValue).max().getAsInt(); - } - - public OreBuilder getRandomOre(Random rand) { - int v = rand.nextInt(this.listCount); - - int c = 0; - - for (Integer i : this.myChanceList) { - c += i; - - if (c > v) { - OreBuilder rv = this.getOreWithChance(i); - - if (rv == null) { - break; - } else { - return rv; - } + values.clear(); total = 0; + for (OreBuilder ore : ores) { + if (ore != null && ore.getOre() != null && ore.getChance() > 0) { + values.add(ore); total += ore.getChance(); } } - - return this.getMaxChanceOre(); } - private OreBuilder getMaxChanceOre() { - return this.getOreWithChance(this.myChanceList.stream().mapToInt(Integer::intValue).max().getAsInt()); - } - - private OreBuilder getOreWithChance(int intValue) { - for (OreBuilder o : this.myCopy) { - if (o.getChance() == intValue) { - return o; - } + public OreBuilder getRandomOre(Random random) { + if (values.isEmpty()) return null; + int selected = random.nextInt(Math.max(1, total)); + for (OreBuilder value : values) { + selected -= value.getChance(); if (selected < 0) return value; } - - return null; + return values.get(values.size() - 1); } public ImmutableList getOres() { - return ImmutableList.copyOf(this.myCopy.stream().map(OreBuilder::getOre).distinct().collect(Collectors.toList())); + ImmutableList.Builder result = ImmutableList.builder(); + for (OreBuilder value : values) result.add(value.getOre()); + return result.build(); } } diff --git a/src/main/java/com/mcmoddev/orespawn/util/StateUtil.java b/src/main/java/com/mcmoddev/orespawn/util/StateUtil.java index 348d5628..ebe4c5bb 100644 --- a/src/main/java/com/mcmoddev/orespawn/util/StateUtil.java +++ b/src/main/java/com/mcmoddev/orespawn/util/StateUtil.java @@ -1,41 +1,36 @@ package com.mcmoddev.orespawn.util; -import com.mcmoddev.orespawn.OreSpawn; +import com.google.common.base.Optional; +import com.mcmoddev.orespawn.api.exceptions.BadStateValueException; import net.minecraft.block.Block; +import net.minecraft.block.properties.IProperty; import net.minecraft.block.state.IBlockState; -public class StateUtil { - private StateUtil() { - throw new InstantiationError("This class cannot be instantiated!"); - } +/** Deprecated metadata-state parser used by published OS3 integrations. */ +@Deprecated +public final class StateUtil { + private StateUtil() { throw new InstantiationError("This class cannot be instantiated"); } public static String serializeState(IBlockState state) { - String string = state.toString(); - string = string.substring(string.indexOf('[') + 1, string.length() - (string.endsWith("]") ? 1 : 0)); - - if (string.equals(state.getBlock().getRegistryName().toString())) { - string = "normal"; - } - - OreSpawn.LOGGER.fatal("State is %s (for block %s)", string, state.getBlock().getRegistryName()); - return string; + String value = state.toString(); + int start = value.indexOf('['); + return start < 0 ? "normal" : value.substring(start + 1, value.endsWith("]") ? value.length() - 1 : value.length()); } - public static IBlockState deserializeState(Block block, String state) { - for (IBlockState validState : block.getBlockState().getValidStates()) { - String string = validState.toString(); - string = string.substring(string.indexOf('[') + 1, string.length() - (string.endsWith("]") ? 1 : 0)); - - if (string.equals(block.getRegistryName().toString())) { - string = ""; - } - - if (state.equals(string)) { - return validState; - } + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static IBlockState deserializeState(Block block, String serialized) throws BadStateValueException { + if (serialized == null || serialized.isEmpty() || "normal".equals(serialized)) return block.getDefaultState(); + IBlockState state = block.getDefaultState(); + for (String assignment : serialized.split(",")) { + String[] parts = assignment.trim().split("=", 2); + if (parts.length != 2) throw new BadStateValueException("Malformed block state: " + assignment); + IProperty property = block.getBlockState().getProperty(parts[0]); + if (property == null) throw new BadStateValueException(parts[0] + " is not a known property of " + block.getRegistryName()); + Optional value = property.parseValue(parts[1]); + if (!value.isPresent()) throw new BadStateValueException(parts[1] + " is not valid for " + parts[0]); + state = state.withProperty(property, value.get()); } - - return null; + return state; } } diff --git a/src/main/java/com/mcmoddev/orespawn/worldgen/FlatBedrock.java b/src/main/java/com/mcmoddev/orespawn/worldgen/FlatBedrock.java index 68101b65..9be4424f 100644 --- a/src/main/java/com/mcmoddev/orespawn/worldgen/FlatBedrock.java +++ b/src/main/java/com/mcmoddev/orespawn/worldgen/FlatBedrock.java @@ -2,81 +2,18 @@ import java.util.Random; -import com.mcmoddev.orespawn.data.Config; -import com.mcmoddev.orespawn.data.Constants; - -import net.minecraft.block.Block; -import net.minecraft.init.Blocks; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; import net.minecraft.world.World; -import net.minecraft.world.WorldType; import net.minecraft.world.chunk.IChunkProvider; import net.minecraft.world.chunk.IChunkGenerator; import net.minecraftforge.fml.common.IWorldGenerator; +/** + * Deprecated OS3 ABI shell. OreSpawn 4's coordinator owns flat-bedrock and + * retrogen scheduling, so this class deliberately cannot create a second pass. + */ +@Deprecated public class FlatBedrock implements IWorldGenerator { - - @Override - public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, - IChunkProvider chunkProvider) { - // no need to do flat-bedrock on a "FLAT" world - if (world.getWorldType() != WorldType.FLAT) { - if (world.provider.getDimension() == -1) { - genTopPlate(world, new ChunkPos(chunkX, chunkZ), Blocks.NETHERRACK); - genBottomPlate(world, new ChunkPos(chunkX, chunkZ), Blocks.NETHERRACK); - } else if (world.provider.getDimension() >= 0 && world.provider.getDimension() != 1) { - genBottomPlate(world, new ChunkPos(chunkX, chunkZ), Blocks.STONE); - } - } - } - - public void retrogen(World world, int chunkX, int chunkZ) { - if (world.getWorldType() != WorldType.FLAT) { - if (world.provider.getDimension() == -1) { - genTopPlate(world, new ChunkPos(chunkX, chunkZ), Blocks.NETHERRACK); - genBottomPlate(world, new ChunkPos(chunkX, chunkZ), Blocks.NETHERRACK); - } else if (world.provider.getDimension() >= 0 && world.provider.getDimension() != 1) { - genBottomPlate(world, new ChunkPos(chunkX, chunkZ), Blocks.STONE); - } - } - } - - private void genBottomPlate(World world, ChunkPos chunkPos, Block repBlock) { - int plateThickness = Config.getInt(Constants.BEDROCK_LAYERS); - - for (int xP = 0; xP < 16; xP++) { - for (int zP = 0; zP < 16; zP++) { - for (int yP = 5; yP > 0; yP--) { - BlockPos target = new BlockPos(chunkPos.x * 16 + xP, yP, chunkPos.z * 16 + zP); - - if (yP < plateThickness && !world.getBlockState(target).getBlock().equals(Blocks.BEDROCK)) { - world.setBlockState(target, Blocks.BEDROCK.getDefaultState(), 26); - } else if (yP >= plateThickness && world.getBlockState(target).getBlock().equals(Blocks.BEDROCK)) { - world.setBlockState(target, repBlock.getDefaultState(), 26); - } - } - } - } - } - - private void genTopPlate(World world, ChunkPos chunkPos, Block repBlock) { - int plateThickness = Config.getInt(Constants.BEDROCK_LAYERS); - int thickness = 127 - plateThickness; // layer where the flat for the top starts - - for (int xP = 0; xP < 16; xP++) { - for (int zP = 0; zP < 16; zP++) { - for (int yP = 126; yP > 121; yP--) { - BlockPos target = new BlockPos(chunkPos.x * 16 + xP, yP, chunkPos.z * 16 + zP); - - if (yP > thickness && !world.getBlockState(target).getBlock().equals(Blocks.BEDROCK)) { - world.setBlockState(target, Blocks.BEDROCK.getDefaultState(), 26); - } else if (yP <= thickness && world.getBlockState(target).getBlock().equals(Blocks.BEDROCK)) { - world.setBlockState(target, repBlock.getDefaultState(), 26); - } - } - } - } - } - + @Override public void generate(Random random, int chunkX, int chunkZ, World world, + IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) { } + public void retrogen(World world, int chunkX, int chunkZ) { } } diff --git a/src/main/java/com/mcmoddev/orespawn/worldgen/OreSpawnWorldGen.java b/src/main/java/com/mcmoddev/orespawn/worldgen/OreSpawnWorldGen.java index 195d7dbe..0c87b477 100644 --- a/src/main/java/com/mcmoddev/orespawn/worldgen/OreSpawnWorldGen.java +++ b/src/main/java/com/mcmoddev/orespawn/worldgen/OreSpawnWorldGen.java @@ -1,75 +1,26 @@ package com.mcmoddev.orespawn.worldgen; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; import com.google.common.collect.ImmutableList; -import com.mcmoddev.orespawn.OreSpawn; -import com.mcmoddev.orespawn.api.GeneratorParameters; -import com.mcmoddev.orespawn.api.IFeature; -import com.mcmoddev.orespawn.api.os3.SpawnBuilder; -import com.mcmoddev.orespawn.data.Config; -import com.mcmoddev.orespawn.data.Constants; -import com.mcmoddev.orespawn.data.ReplacementsRegistry; import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; -import net.minecraft.init.Blocks; -import net.minecraft.item.ItemBlock; -import net.minecraft.util.math.ChunkPos; import net.minecraft.world.World; -import net.minecraft.world.chunk.IChunkGenerator; import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.IChunkGenerator; import net.minecraftforge.fml.common.IWorldGenerator; -import net.minecraftforge.oredict.OreDictionary; +/** + * Binary shell for OS3 3.2. It is deliberately never registered: translated + * entries run through OreSpawn 4's single compatibility scheduler. + */ public class OreSpawnWorldGen implements IWorldGenerator { - - private final Map> dimensions; - private static final List SPAWN_BLOCKS = new ArrayList<>(); - - public OreSpawnWorldGen(Map> allDimensions, long nextLong) { - this.dimensions = Collections.unmodifiableMap(allDimensions); - - if (SPAWN_BLOCKS.isEmpty()) { - SPAWN_BLOCKS.add(Blocks.STONE); - SPAWN_BLOCKS.add(Blocks.NETHERRACK); - SPAWN_BLOCKS.add(Blocks.END_STONE); - SPAWN_BLOCKS.addAll(OreDictionary.getOres("stone").stream().filter(stack -> stack.getItem() instanceof ItemBlock).map(stack -> ((ItemBlock) stack.getItem()).getBlock()).collect(Collectors.toList())); - } - } - - public static ImmutableList getSpawnBlocks() { - return ImmutableList.copyOf(SPAWN_BLOCKS); - } - - @Override - public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, - IChunkProvider chunkProvider) { - - int thisDim = world.provider.getDimension(); - List entries = new ArrayList<> (this.dimensions.getOrDefault(thisDim, new ArrayList<> ())); - - if (!this.dimensions.getOrDefault(OreSpawn.API.dimensionWildcard(), new ArrayList<>()).isEmpty()) - entries.addAll(this.dimensions.get(OreSpawn.API.dimensionWildcard()).stream() - .filter(ent -> (!ent.hasExtendedDimensions() && thisDim > 0 && thisDim != 1) || - ent.extendedDimensionsMatch(thisDim)) - .collect(Collectors.toList())); - - entries.stream() - .filter(SpawnBuilder::enabled) - .filter(sb -> !Config.getBoolean(Constants.RETROGEN_KEY) || (sb.retrogen() || Config.getBoolean(Constants.FORCE_RETROGEN_KEY))) - .forEach(sE -> { - IFeature currentFeatureGen = sE.getFeatureGen().getGenerator(); - List replacement = sE.getReplacementBlocks(); - replacement = replacement.isEmpty() ? ReplacementsRegistry.getDimensionDefault(thisDim) : replacement; - - GeneratorParameters parameters = new GeneratorParameters(new ChunkPos(chunkX, chunkZ), sE.getOreSpawns(), replacement, sE.getBiomes(), sE.getFeatureGen().getParameters()); - - currentFeatureGen.setRandom(random); - currentFeatureGen.generate(world, chunkGenerator, chunkProvider, parameters); - }); - } + public OreSpawnWorldGen() { } + public OreSpawnWorldGen(Map> dimensions, long seed) { } + public static ImmutableList getSpawnBlocks() { return ImmutableList.copyOf(Collections.emptyList()); } + @Override public void generate(Random random, int chunkX, int chunkZ, World world, + IChunkGenerator generator, IChunkProvider provider) { } } - - diff --git a/src/main/java/com/mojang/serialization/Codec.java b/src/main/java/com/mojang/serialization/Codec.java new file mode 100644 index 00000000..34002bd9 --- /dev/null +++ b/src/main/java/com/mojang/serialization/Codec.java @@ -0,0 +1,30 @@ +package com.mojang.serialization; + +import java.util.Objects; +import java.util.function.Function; + +import com.google.gson.JsonElement; + +/** + * Small 1.12 compatibility surface for OreSpawn's codec-backed public pattern + * contract. Minecraft 1.11 predates Mojang's serialization package, so this + * target supplies only the JSON decode operation used by OreSpawn profiles. + */ +public abstract class Codec { + public abstract DataResult parse(JsonOps operations, JsonElement input); + + public static Codec of(Function decoder) { + Objects.requireNonNull(decoder, "decoder"); + return new Codec() { + @Override + public DataResult parse(JsonOps operations, JsonElement input) { + try { + return DataResult.success(decoder.apply(input)); + } catch (RuntimeException exception) { + return DataResult.error(exception.getMessage() == null + ? exception.getClass().getSimpleName() : exception.getMessage()); + } + } + }; + } +} diff --git a/src/main/java/com/mojang/serialization/DataResult.java b/src/main/java/com/mojang/serialization/DataResult.java new file mode 100644 index 00000000..7053caac --- /dev/null +++ b/src/main/java/com/mojang/serialization/DataResult.java @@ -0,0 +1,43 @@ +package com.mojang.serialization; + +import java.util.Optional; + +/** Minimal result type paired with the 1.12 OreSpawn codec adapter. */ +public final class DataResult { + private final A value; + private final PartialResult error; + + private DataResult(A value, PartialResult error) { + this.value = value; + this.error = error; + } + + public static DataResult success(A value) { + return new DataResult<>(value, null); + } + + public static DataResult error(String message) { + return new DataResult<>(null, new PartialResult<>(message)); + } + + public Optional result() { + return Optional.ofNullable(value); + } + + public Optional> error() { + return Optional.ofNullable(error); + } + + public static final class PartialResult { + private final String message; + + private PartialResult(String message) { + this.message = message == null ? "unknown codec error" : message; + } + + @Override + public String toString() { + return message; + } + } +} diff --git a/src/main/java/com/mojang/serialization/JsonOps.java b/src/main/java/com/mojang/serialization/JsonOps.java new file mode 100644 index 00000000..e16fc858 --- /dev/null +++ b/src/main/java/com/mojang/serialization/JsonOps.java @@ -0,0 +1,9 @@ +package com.mojang.serialization; + +/** JSON operations marker used by the 1.12 OreSpawn codec adapter. */ +public final class JsonOps { + public static final JsonOps INSTANCE = new JsonOps(); + + private JsonOps() { + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java new file mode 100644 index 00000000..cc00dbd5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -0,0 +1,236 @@ +package zone.moddev.mc.orespawn; + +import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; +import zone.moddev.mc.orespawn.init.OreSpawnPatterns; +import zone.moddev.mc.orespawn.worldgen.OreSpawnOreGeneration; +import zone.moddev.mc.orespawn.worldgen.GeomeConfig; +import zone.moddev.mc.orespawn.worldgen.GeomeDistributionSampler; +import zone.moddev.mc.orespawn.worldgen.FluidDepositFeature; +import zone.moddev.mc.orespawn.worldgen.StoneReplacer; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; +import zone.moddev.mc.orespawn.worldgen.FormationSettings.Preset; +import zone.moddev.mc.orespawn.OreSpawnConfig.GeologyMode; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; +import zone.moddev.mc.orespawn.worldgen.WorldgenBenchmark; +import zone.moddev.mc.orespawn.worldgen.FlatBedrockFeature; +import zone.moddev.mc.orespawn.worldgen.OreRetrogenManager; +import zone.moddev.mc.orespawn.worldgen.BiomeSurfaceFeature; +import zone.moddev.mc.orespawn.worldgen.BiomeWorldgenBootstrap; +import zone.moddev.mc.orespawn.worldgen.WorldMaterialWeather; +import zone.moddev.mc.orespawn.worldgen.OreSpawnWorldGenerator; +import zone.moddev.mc.orespawn.commands.OreSpawnCommands; +import zone.moddev.mc.orespawn.documentation.DocumentationExporter; +import com.mcmoddev.orespawn.compat.LegacyOs3Bridge; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLInterModComms; +import net.minecraftforge.fml.common.event.FMLLoadCompleteEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.FMLServerAboutToStartEvent; +import net.minecraftforge.fml.common.event.FMLServerStartingEvent; +import net.minecraftforge.fml.common.event.FMLServerStoppedEvent; +import net.minecraftforge.fml.common.registry.GameRegistry; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.world.ChunkDataEvent; +import net.minecraftforge.event.world.ChunkEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.fml.common.registry.ForgeRegistries; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Mod(modid = OreSpawn.MODID, name = OreSpawn.NAME, version = OreSpawn.VERSION, + acceptedMinecraftVersions = "[1.11.2]") +public class OreSpawn { + @Mod.Instance(OreSpawn.MODID) + public static OreSpawn instance; + + public static final String MODID = "orespawn"; + public static final String NAME = "OreSpawn"; + public static final String VERSION = "4.0.16.111021"; + + private static final Logger LOGGER = LogManager.getLogger(); + + private static String getVersion() { + Package metadata = OreSpawn.class.getPackage(); + String version = metadata == null ? null : metadata.getImplementationVersion(); + return version == null ? "DEV" : version; + } + + public OreSpawn() { + instance = this; + } + + @EventHandler + public void preInit(FMLPreInitializationEvent event) { + OreSpawnConfig.load(event.getSuggestedConfigurationFile()); + OreSpawnPatterns.register(); + LegacyOs3Bridge.initialize(event); + MinecraftForge.EVENT_BUS.register(RuntimeEvents.INSTANCE); + // Forge 1.11 posts DecorateBiomeEvent.Pre on EVENT_BUS even though the + // event's own documentation names TERRAIN_GEN_BUS. Register the + // deduplicated coordinator on both native buses so the early surface and + // geology pass runs before ores, structures, and vegetation. + MinecraftForge.EVENT_BUS.register(OreSpawnWorldGenerator.INSTANCE); + MinecraftForge.ORE_GEN_BUS.register(OreSpawnWorldGenerator.INSTANCE); + MinecraftForge.TERRAIN_GEN_BUS.register(OreSpawnWorldGenerator.INSTANCE); + GameRegistry.registerWorldGenerator(OreSpawnWorldGenerator.INSTANCE, 0); + if (event.getSide().isClient()) zone.moddev.mc.orespawn.client.ClientSetup.initialize(); + } + + @EventHandler + public void init(FMLInitializationEvent event) { + WorldgenIntegrationManager.initialize(); + GeomeConfig.bake(); + logGeomeSampler(); + DocumentationExporter.exportBundledGuide(); + StoneReplacer.registerConfiguredFeature(); + OreSpawnOreGeneration.registerConfiguredFeatures(); + FluidDepositFeature.registerConfiguredFeature(); + FlatBedrockFeature.registerConfiguredFeature(); + BiomeSurfaceFeature.registerConfiguredFeature(); + } + + @EventHandler + public void processInterMod(FMLInterModComms.IMCEvent event) { + WorldgenIntegrationManager.processImcMessages(); + } + + @EventHandler + public void postInit(FMLPostInitializationEvent event) { + WorldgenIntegrationManager.processImcMessages(); + WorldgenIntegrationManager.freeze(); + GeomeConfig.bake(); + refreshGenerationConfig(); + WorldgenIntegrationManager.markFeatureReady(); + } + + @EventHandler + public void loadComplete(final FMLLoadCompleteEvent event) { + WorldgenIntegrationManager.freeze(); + GeomeConfig.bake(); + refreshGenerationConfig(); + WorldgenIntegrationManager.markFeatureReady(); + } + + @EventHandler + public void serverAboutToStart(FMLServerAboutToStartEvent event) { + WorldGeologyProfileManager.onServerAboutToStart(event); + WorldgenBenchmark.onServerAboutToStart(event); + } + + @EventHandler + public void serverStarting(FMLServerStartingEvent event) { + OreSpawnCommands.register(event); + WorldgenBenchmark.onServerStarted(event); + } + + @EventHandler + public void serverStopped(FMLServerStoppedEvent event) { + WorldGeologyProfileManager.onServerStopped(event); + OreSpawnWorldGenerator.INSTANCE.clear(); + } + + private static void refreshGenerationConfig() { + StoneReplacer.refreshWorldConfig(); + OreSpawnOreGeneration.refreshWorldConfig(); + FluidDepositFeature.refreshWorldConfig(); + FlatBedrockFeature.refreshWorldConfig(); + OreRetrogenManager.refreshWorldConfig(); + } + + private static void logGeomeSampler() { + if (!Boolean.getBoolean("orespawn.geomeSampler")) { + return; + } + + WorldGeologyProfile original = GeomeConfig.globalProfile(); + String defaultSeed = Long.toString(Long.getLong("orespawn.geomeSamplerSeed", 19780401L)); + String[] samplerSeeds = System.getProperty("orespawn.geomeSamplerSeeds", defaultSeed).split(","); + String profileFilter = System.getProperty("orespawn.geomeSamplerProfiles", "all"); + boolean includeBiomeAudit = Boolean.parseBoolean( + System.getProperty("orespawn.geomeSamplerBiomeAudit", "true")); + try { + for (String seedText : samplerSeeds) { + long samplerSeed = Long.parseLong(seedText.trim()); + for (Preset preset : new Preset[] { + Preset.TINY, Preset.SMALL, Preset.AVERAGE, Preset.LARGE, Preset.HUGE }) { + if (!samplerProfileEnabled(profileFilter, preset.configName())) { + continue; + } + WorldGeologyProfile profile = original + .withSelection(GeologyMode.GEOME, preset, preset, preset, preset, preset, + original.placeFluidDeposits()); + logSamplerProfile("Sky " + preset.configName(), samplerSeed, profile, includeBiomeAudit); + } + if (samplerProfileEnabled(profileFilter, "mixed_huge")) { + WorldGeologyProfile mixedHuge = original + .withSelection(GeologyMode.GEOME, Preset.AVERAGE, Preset.HUGE, Preset.HUGE, + Preset.HUGE, Preset.HUGE, original.placeFluidDeposits()); + logSamplerProfile("Sky mixed-huge", samplerSeed, mixedHuge, includeBiomeAudit); + } + } + } finally { + GeomeConfig.applyWorldProfile(original); + } + } + + private static boolean samplerProfileEnabled(String filter, String profile) { + if ("all".equalsIgnoreCase(filter.trim())) { + return true; + } + for (String configured : filter.split(",")) { + if (profile.equalsIgnoreCase(configured.trim())) { + return true; + } + } + return false; + } + + private static void logSamplerProfile(String label, long seed, WorldGeologyProfile profile, + boolean includeBiomeAudit) { + GeomeConfig.applyWorldProfile(profile); + String terrainSample = System.getProperty("orespawn.geomeSamplerTerrain"); + if (terrainSample == null || terrainSample.trim().isEmpty()) { + LOGGER.info("\n{} sampler\n{}", label, + GeomeDistributionSampler.sample(seed, ForgeRegistries.BIOMES.getValues(), 8, 8, + includeBiomeAudit)); + return; + } + try { + LOGGER.info("\n{} sampler\n{}", label, + GeomeDistributionSampler.sampleTerrain(seed, java.nio.file.Paths.get(terrainSample))); + } catch (java.io.IOException e) { + LOGGER.error("Could not replay OreSpawn terrain sample '{}'", terrainSample, e); + } + } + + private static final class RuntimeEvents { + static final RuntimeEvents INSTANCE = new RuntimeEvents(); + + @SubscribeEvent public void worldLoad(WorldEvent.Load event) { + WorldGeologyProfileManager.onWorldLoad(event); + } + @SubscribeEvent public void chunkLoad(ChunkEvent.Load event) { + WorldMaterialWeather.onChunkLoad(event); + } + @SubscribeEvent public void worldTick(TickEvent.WorldTickEvent event) { + WorldMaterialWeather.onWorldTick(event); + } + @SubscribeEvent public void retrogenLoad(ChunkDataEvent.Load event) { + OreRetrogenManager.onChunkLoad(event); + } + @SubscribeEvent public void retrogenSave(ChunkDataEvent.Save event) { + OreRetrogenManager.onChunkSave(event); + } + @SubscribeEvent public void serverTick(TickEvent.ServerTickEvent event) { + OreRetrogenManager.onServerTick(event); + } + } + +} diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawnConfig.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawnConfig.java new file mode 100644 index 00000000..a88a0a44 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawnConfig.java @@ -0,0 +1,72 @@ +package zone.moddev.mc.orespawn; + +import java.io.File; + +import net.minecraftforge.common.config.Configuration; + +/** Small bootstrap fallback; the JSON profile is the authoritative worldgen configuration. */ +public final class OreSpawnConfig { + private static volatile boolean placeTerrain = true; + private static volatile GeologyMode geologyMode = GeologyMode.GEOME; + private static volatile int geomeSize = 256; + private static volatile double rockLayerNoise = 32.0D; + private static volatile int layerThickness = 8; + + private OreSpawnConfig() { + } + + /** Loads the small global fallback file. Per-world JSON remains authoritative. */ + public static synchronized void load(File file) { + Configuration config = new Configuration(file); + try { + config.load(); + String category = "worldgen"; + placeTerrain = config.getBoolean("place_terrain", category, true, + "Master switch for configured terrain replacement."); + String mode = config.getString("fallback_geology_mode", category, + GeologyMode.GEOME.name(), "Fallback geology mode.", + new String[] { GeologyMode.GEOME.name(), GeologyMode.LEGACY.name() }); + try { + geologyMode = GeologyMode.valueOf(mode.toUpperCase(java.util.Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + geologyMode = GeologyMode.GEOME; + } + geomeSize = config.getInt("cyano_region_size", category, 256, 4, + Short.MAX_VALUE, "Fallback Cyano region size."); + rockLayerNoise = config.getFloat("cyano_layer_reach", category, 32.0F, + 1.0F, Short.MAX_VALUE, "Fallback Cyano layer reach."); + layerThickness = config.getInt("cyano_layer_thickness", category, 8, 1, + 255, "Fallback Cyano layer thickness."); + } finally { + if (config.hasChanged()) config.save(); + } + } + + public static boolean placeOreSpawnRock() { return placeTerrain; } + public static GeologyMode geologyMode() { return geologyMode; } + public static int geomeSize() { return geomeSize; } + public static double rockLayerNoise() { return rockLayerNoise; } + public static int geomLayerThickness() { return layerThickness; } + /** Legacy fallback input retained for source compatibility; JSON deposits are authoritative. */ + @Deprecated + public static boolean placeCrudeOil() { return false; } + + public static final class OreGenerationSettings { + private final int minY; + private final int maxY; + private final double frequency; + private final int quantity; + public OreGenerationSettings(int minY, int maxY, double frequency, int quantity) { + this.minY = minY; this.maxY = maxY; this.frequency = frequency; this.quantity = quantity; + } + public int minY() { return minY; } + public int maxY() { return maxY; } + public double frequency() { return frequency; } + public int quantity() { return quantity; } + } + + public enum GeologyMode { + GEOME, + LEGACY + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/BiomePlacementMode.java b/src/main/java/zone/moddev/mc/orespawn/api/BiomePlacementMode.java new file mode 100644 index 00000000..0a80ff71 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/BiomePlacementMode.java @@ -0,0 +1,13 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Locale; + +/** Controls whether a palette supplements or replaces matching source biomes. */ +public enum BiomePlacementMode { + AUGMENT, + REPLACE; + + public String configName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/BiomeRegionSize.java b/src/main/java/zone/moddev/mc/orespawn/api/BiomeRegionSize.java new file mode 100644 index 00000000..fb2b14c4 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/BiomeRegionSize.java @@ -0,0 +1,26 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Locale; + +/** Stable presets for broad biome-overlay regions, expressed in blocks. */ +public enum BiomeRegionSize { + TINY(128), + SMALL(256), + AVERAGE(512), + LARGE(1024), + HUGE(2048); + + private final int blocks; + + BiomeRegionSize(int blocks) { + this.blocks = blocks; + } + + public int blocks() { + return blocks; + } + + public String configName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/BiomeReplacementScope.java b/src/main/java/zone/moddev/mc/orespawn/api/BiomeReplacementScope.java new file mode 100644 index 00000000..a594dd3d --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/BiomeReplacementScope.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Locale; + +/** Selects which source-biome namespaces a palette may transform. */ +public enum BiomeReplacementScope { + ALL, + MINECRAFT_ONLY, + SELECTED_NAMESPACES; + + public String configName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/CompiledOrePattern.java b/src/main/java/zone/moddev/mc/orespawn/api/CompiledOrePattern.java new file mode 100644 index 00000000..5805de01 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/CompiledOrePattern.java @@ -0,0 +1,11 @@ +package zone.moddev.mc.orespawn.api; + +/** + * Immutable, pre-decoded ore pattern used during chunk generation. + * Implementations must be thread-safe and must not allocate or access registries, + * configuration files, tags, or logging from {@link #place(OrePlacementContext)}. + */ +@FunctionalInterface +public interface CompiledOrePattern { + boolean place(OrePlacementContext context); +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/FormationPreset.java b/src/main/java/zone/moddev/mc/orespawn/api/FormationPreset.java new file mode 100644 index 00000000..a241872b --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/FormationPreset.java @@ -0,0 +1,21 @@ +package zone.moddev.mc.orespawn.api; + +/** Named formation scales accepted by the world profile. */ +public enum FormationPreset { + TINY("tiny"), + SMALL("small"), + AVERAGE("average"), + LARGE("large"), + HUGE("huge"), + CUSTOM("custom"); + + private final String configName; + + FormationPreset(String configName) { + this.configName = configName; + } + + public String configName() { + return configName; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologyAlgorithm.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologyAlgorithm.java new file mode 100644 index 00000000..669d0edd --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologyAlgorithm.java @@ -0,0 +1,17 @@ +package zone.moddev.mc.orespawn.api; + +/** Supported terrain formation algorithms. */ +public enum GeologyAlgorithm { + STABLE_LAYERS("stable_layers"), + SKY_V1("sky_v1"); + + private final String configName; + + GeologyAlgorithm(String configName) { + this.configName = configName; + } + + public String configName() { + return configName; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologyColumn.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologyColumn.java new file mode 100644 index 00000000..d17b4b3d --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologyColumn.java @@ -0,0 +1,18 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Optional; + +import net.minecraft.util.ResourceLocation; +import net.minecraft.block.state.IBlockState; + +/** A single classified geology column returned by {@link GeologySampler}. */ +public interface GeologyColumn { + ResourceLocation dimension(); + ResourceLocation biome(); + ResourceLocation geome(); + int blockX(); + int blockZ(); + int surfaceY(); + IBlockState rockAt(int y); + Optional familyAt(int y); +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologyFamily.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologyFamily.java new file mode 100644 index 00000000..e2cb7793 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologyFamily.java @@ -0,0 +1,19 @@ +package zone.moddev.mc.orespawn.api; + +/** Geological families understood by OreSpawn's declarative engines. */ +public enum GeologyFamily { + SEDIMENTARY("sedimentary"), + METAMORPHIC("metamorphic"), + IGNEOUS_INTRUSIVE("igneous_intrusive"), + IGNEOUS_VOLCANIC("igneous_volcanic"); + + private final String configName; + + GeologyFamily(String configName) { + this.configName = configName; + } + + public String configName() { + return configName; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologyProfileView.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologyProfileView.java new file mode 100644 index 00000000..0efe2af2 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologyProfileView.java @@ -0,0 +1,89 @@ +package zone.moddev.mc.orespawn.api; + +import zone.moddev.mc.orespawn.util.JsonCopies; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import net.minecraft.util.ResourceLocation; + +/** Immutable view of the effective geology profile for the active server. */ +public final class GeologyProfileView { + private final JsonObject root; + + GeologyProfileView(JsonObject root) { + this.root = JsonCopies.copy(root); + } + + public int schemaVersion() { + return root.has("schema_version") ? root.get("schema_version").getAsInt() : 0; + } + + public String geologyMode() { + return root.has("geology_mode") ? root.get("geology_mode").getAsString() : "geome"; + } + + public Optional selectedTemplate() { + if (!root.has("selected_template")) { + return Optional.empty(); + } + try { + return Optional.of(new ResourceLocation(root.get("selected_template").getAsString())); + } catch (RuntimeException ignored) { + return Optional.empty(); + } + } + + public Set rockIds() { + return keys("rocks"); + } + + public Set oreIds() { + return keys("ores"); + } + + public Set fluidDepositIds() { + return keys("fluid_deposits"); + } + + public Set geomeIds() { + return keys("geomes"); + } + + public Set terrainDimensions() { + return keys("terrain_dimensions"); + } + + public Set biomePaletteIds() { + return keys("biome_palettes"); + } + + public Set dimensionMaterialIds() { + return keys("dimension_materials"); + } + + /** Returns a defensive copy suitable for diagnostics or tooling. */ + public JsonObject toJson() { + return JsonCopies.copy(root); + } + + private Set keys(String section) { + if (!root.has(section) || !root.get(section).isJsonObject()) { + return Collections.emptySet(); + } + Set values = new LinkedHashSet<>(); + for (String value : JsonCopies.keys(root.getAsJsonObject(section))) { + try { + values.add(new ResourceLocation(value)); + } catch (RuntimeException ignored) { + // Invalid user data is omitted from the typed view. + } + } + return Collections.unmodifiableSet(values); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java new file mode 100644 index 00000000..f029d69f --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/GeologySampler.java @@ -0,0 +1,10 @@ +package zone.moddev.mc.orespawn.api; + +/** Reusable, read-only sampler for the active world's baked geology. */ +public interface GeologySampler { + /** + * Classifies one column. The returned column reuses that biome/geome + * classification for all subsequent Y queries. + */ + GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY); +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreDimensionSelector.java b/src/main/java/zone/moddev/mc/orespawn/api/OreDimensionSelector.java new file mode 100644 index 00000000..701a469d --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreDimensionSelector.java @@ -0,0 +1,34 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Locale; + +import net.minecraft.util.ResourceLocation; + +/** Built-in dimension policies available to declarative ore providers. */ +public enum OreDimensionSelector { + ALL_EXCEPT_NETHER_AND_END("all_except_nether_end"); + + private final ResourceLocation id; + + OreDimensionSelector(String path) { + this.id = new ResourceLocation("orespawn", path); + } + + public ResourceLocation id() { + return id; + } + + public static OreDimensionSelector fromId(ResourceLocation id) { + for (OreDimensionSelector selector : values()) { + if (selector.id.equals(id)) return selector; + } + throw new IllegalArgumentException("Unknown ore dimension selector: " + id); + } + + public static OreDimensionSelector fromName(String value) { + ResourceLocation id = value.indexOf(':') >= 0 + ? new ResourceLocation(value) + : new ResourceLocation("orespawn", value.toLowerCase(Locale.ROOT)); + return fromId(id); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreHeightDistribution.java b/src/main/java/zone/moddev/mc/orespawn/api/OreHeightDistribution.java new file mode 100644 index 00000000..54b8c2df --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreHeightDistribution.java @@ -0,0 +1,19 @@ +package zone.moddev.mc.orespawn.api; + +/** Vertical distributions supported by OreSpawn-managed ores. */ +public enum OreHeightDistribution { + UNIFORM("uniform"), + TRIANGLE("triangle"), + BOTTOM_TRIANGLE("bottom_triangle"), + UNIFORM_BOTTOM_TRIANGLE("uniform_bottom_triangle"); + + private final String configName; + + OreHeightDistribution(String configName) { + this.configName = configName; + } + + public String configName() { + return configName; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OrePattern.java b/src/main/java/zone/moddev/mc/orespawn/api/OrePattern.java new file mode 100644 index 00000000..fb6d0071 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OrePattern.java @@ -0,0 +1,28 @@ +package zone.moddev.mc.orespawn.api; + +/** Shapes supported by OreSpawn's bounded ore generator. */ +public enum OrePattern { + DEFAULT("default"), + VEIN("vein"), + NORMAL_CLOUD("normal_cloud"), + PRECISION("precision"), + CLUSTERS("clusters"), + UNDERFLUIDS("underfluids"); + + /** @deprecated Use {@link #CLUSTERS}. */ + @Deprecated + public static final OrePattern CLUSTER = CLUSTERS; + /** @deprecated Use {@link #NORMAL_CLOUD}. */ + @Deprecated + public static final OrePattern CLOUD = NORMAL_CLOUD; + + private final String configName; + + OrePattern(String configName) { + this.configName = configName; + } + + public String configName() { + return configName; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OrePatternType.java b/src/main/java/zone/moddev/mc/orespawn/api/OrePatternType.java new file mode 100644 index 00000000..9fb496e0 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OrePatternType.java @@ -0,0 +1,53 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Objects; +import java.util.function.Function; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; + +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; + +/** + * Forge-registered ore pattern type. Its codec is evaluated once while a + * geology profile is baked; only the resulting compiled pattern reaches the + * generation loop. + */ +public final class OrePatternType extends IForgeRegistryEntry.Impl { + private final Codec codec; + private final Function compiler; + + private OrePatternType(Codec codec, Function compiler) { + this.codec = Objects.requireNonNull(codec, "codec"); + Objects.requireNonNull(compiler, "compiler"); + this.compiler = value -> compiler.apply(cast(value)); + } + + public static OrePatternType create(Codec codec, + Function compiler) { + return new OrePatternType(codec, compiler); + } + + public Codec codec() { + return codec; + } + + public CompiledOrePattern decode(JsonElement configuration) { + DataResult result = codec.parse(JsonOps.INSTANCE, configuration); + Object value = result.result().orElseThrow(() -> new IllegalArgumentException( + "Invalid settings for ore pattern " + getRegistryName() + ": " + + result.error().map(Object::toString).orElse("unknown codec error"))); + return compile(value); + } + + private CompiledOrePattern compile(Object configuration) { + return Objects.requireNonNull(compiler.apply(configuration), "compiled pattern"); + } + + @SuppressWarnings("unchecked") + private static C cast(Object value) { + return (C) value; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OrePlacementContext.java b/src/main/java/zone/moddev/mc/orespawn/api/OrePlacementContext.java new file mode 100644 index 00000000..0b8f98ec --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OrePlacementContext.java @@ -0,0 +1,30 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Random; + +import net.minecraftforge.fluids.Fluid; + +/** Allocation-free view supplied to a compiled ore pattern for one attempt. */ +public interface OrePlacementContext { + Random random(); + + int originX(); + int originY(); + int originZ(); + int minY(); + int maxY(); + int quantity(); + int spread(); + int verticalSpread(); + int nodeSize(); + + /** + * Returns whether this attempt may inspect or replace the position. During initial + * generation this includes Minecraft's already-loaded writable worldgen region, so + * deposits can cross chunk borders. Retrogen deliberately limits it to the chunk + * being updated. + */ + boolean inside(int x, int y, int z); + boolean isFluid(int x, int y, int z, Fluid fluid); + boolean tryPlace(int x, int y, int z); +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnApi.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnApi.java new file mode 100644 index 00000000..b3b88361 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnApi.java @@ -0,0 +1,55 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Optional; + +import zone.moddev.mc.orespawn.OreSpawn; +import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; +import zone.moddev.mc.orespawn.worldgen.GeomeConfig; +import zone.moddev.mc.orespawn.worldgen.WorldIds; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; + +/** Entry point for OreSpawn API version 1. */ +public final class OreSpawnApi { + public static final int API_VERSION = 1; + public static final String IMC_WORLDGEN_PROVIDER = "worldgen_provider_v1"; + + private OreSpawnApi() { + } + + /** + * Enqueues a provider before OreSpawn freezes discovery. Call this during + * the provider mod's Forge initialization phase. + */ + public static boolean enqueue(WorldgenProvider provider) { + if (provider == null) { + throw new IllegalArgumentException("provider cannot be null"); + } + return WorldgenIntegrationManager.submitApiProvider(provider); + } + + public static ProviderStatus getProviderStatus(String providerModId) { + return WorldgenIntegrationManager.getProviderStatus(providerModId); + } + + public static boolean isOreTakeoverActive(String providerModId) { + return WorldgenIntegrationManager.isOreTakeoverActive(providerModId); + } + + public static Optional getActiveProfile(MinecraftServer server) { + if (server == null || WorldGeologyProfileManager.activeServer() != server) { + return Optional.empty(); + } + return Optional.of(new GeologyProfileView(WorldGeologyProfileManager.activeProfile().toJson())); + } + + public static Optional createSampler(WorldServer level) { + if (level == null || WorldGeologyProfileManager.activeServer() != level.getMinecraftServer() + || GeomeConfig.baked(WorldIds.dimension(level)) == null) { + return Optional.empty(); + } + return Optional.of(OreSpawnGeologySampler.create(level)); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnBiomes.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnBiomes.java new file mode 100644 index 00000000..faa0cbe6 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnBiomes.java @@ -0,0 +1,194 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import net.minecraft.entity.EnumCreatureType; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeDecorator; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import net.minecraftforge.fml.common.registry.IForgeRegistry; + +/** + * Forge 1.11 biome registration helpers for provider mods. The registrar keeps + * the same deferred declaration semantics as later OreSpawn ports while using + * Forge 13 registry events and {@link Biome.BiomeProperties}. + */ +public final class OreSpawnBiomes { + private OreSpawnBiomes() { + } + + public static BiomeRegistrar registrar(String modId) { + return new BiomeRegistrar(modId, true); + } + + static BiomeRegistrar registrarForTesting(String modId) { + return new BiomeRegistrar(modId, false); + } + + public static BiomeReference copyAndRegister(BiomeRegistrar registrar, + String name, Supplier source, + Consumer edit) { + Objects.requireNonNull(registrar, "registrar"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(edit, "edit"); + return registrar.register(name, () -> { + Biome sourceBiome = Objects.requireNonNull(source.get(), "source biome"); + Biome.BiomeProperties properties = copiedProperties(name, sourceBiome); + edit.accept(properties); + ProviderBiome result = new ProviderBiome(properties); + result.copyContents(sourceBiome); + return result; + }); + } + + public static BiomeReference blankAndRegister(BiomeRegistrar registrar, + String name, Consumer configure) { + Objects.requireNonNull(registrar, "registrar"); + Objects.requireNonNull(configure, "configure"); + return registrar.register(name, () -> { + Biome.BiomeProperties properties = new Biome.BiomeProperties(name); + configure.accept(properties); + return new ProviderBiome(properties); + }); + } + + private static Biome.BiomeProperties copiedProperties(String name, Biome source) { + Biome.BiomeProperties properties = new Biome.BiomeProperties(name) + .setBaseHeight(source.getBaseHeight()) + .setHeightVariation(source.getHeightVariation()) + .setTemperature(source.getTemperature()) + .setRainfall(source.getRainfall()) + .setWaterColor(source.getWaterColor()); + if (!source.canRain()) properties.setRainDisabled(); + if (source.getEnableSnow()) properties.setSnowEnabled(); + if (source.getRegistryName() != null) { + properties.setBaseBiome(source.getRegistryName().toString()); + } + return properties; + } + + public static final class BiomeRegistrar { + private final String modId; + private final Map> entries = new LinkedHashMap<>(); + private boolean registering; + + private BiomeRegistrar(String modId, boolean attach) { + this.modId = Objects.requireNonNull(modId, "modId"); + new ResourceLocation(modId, "registrar_probe"); + if (attach) MinecraftForge.EVENT_BUS.register(this); + } + + private synchronized BiomeReference register(String name, + Supplier factory) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(factory, "factory"); + if (registering) { + throw new IllegalStateException("Biome declarations are closed for " + modId); + } + ResourceLocation id = new ResourceLocation(modId, name); + if (entries.containsKey(id)) { + throw new IllegalArgumentException("Duplicate biome declaration: " + id); + } + entries.put(id, factory); + return new BiomeReference(id); + } + + @SubscribeEvent + public synchronized void registerBiomes(RegistryEvent.Register event) { + registerRegistry(event.getRegistry()); + } + + synchronized void registerForTesting(IForgeRegistry registry) { + registerRegistry(registry); + } + + private void registerRegistry(IForgeRegistry registry) { + if (registering) { + throw new IllegalStateException("Biome registrar invoked more than once for " + modId); + } + registering = true; + for (Map.Entry> entry : entries.entrySet()) { + Biome biome = Objects.requireNonNull(entry.getValue().get(), + "Biome factory returned null for " + entry.getKey()); + if (biome.getRegistryName() != null + && !entry.getKey().equals(biome.getRegistryName())) { + throw new IllegalStateException("Biome factory returned an already named biome: " + + biome.getRegistryName()); + } + registry.register(biome.setRegistryName(entry.getKey())); + } + } + } + + public static final class BiomeReference implements Supplier { + private final ResourceLocation id; + + private BiomeReference(ResourceLocation id) { + this.id = id; + } + + public ResourceLocation getId() { + return id; + } + + @Override + public Biome get() { + return get(ForgeRegistries.BIOMES); + } + + Biome get(IForgeRegistry registry) { + Biome biome = registry.getValue(id); + if (biome == null) throw new IllegalStateException("Biome is not registered yet: " + id); + return biome; + } + } + + private static final class ProviderBiome extends Biome { + ProviderBiome(Biome.BiomeProperties properties) { + super(properties); + } + + void copyContents(Biome source) { + topBlock = source.topBlock; + fillerBlock = source.fillerBlock; + decorator = copyDecorator(source.decorator); + for (EnumCreatureType type : EnumCreatureType.values()) { + getSpawnableList(type).clear(); + getSpawnableList(type).addAll(source.getSpawnableList(type)); + } + } + + private static BiomeDecorator copyDecorator(BiomeDecorator source) { + BiomeDecorator copy = new BiomeDecorator(); + copy.clayGen = source.clayGen; copy.sandGen = source.sandGen; + copy.gravelGen = source.gravelGen; copy.dirtGen = source.dirtGen; + copy.gravelOreGen = source.gravelOreGen; copy.graniteGen = source.graniteGen; + copy.dioriteGen = source.dioriteGen; copy.andesiteGen = source.andesiteGen; + copy.coalGen = source.coalGen; copy.ironGen = source.ironGen; + copy.goldGen = source.goldGen; copy.redstoneGen = source.redstoneGen; + copy.diamondGen = source.diamondGen; copy.lapisGen = source.lapisGen; + copy.flowerGen = source.flowerGen; copy.mushroomBrownGen = source.mushroomBrownGen; + copy.mushroomRedGen = source.mushroomRedGen; copy.bigMushroomGen = source.bigMushroomGen; + copy.reedGen = source.reedGen; copy.cactusGen = source.cactusGen; + copy.waterlilyGen = source.waterlilyGen; + copy.waterlilyPerChunk = source.waterlilyPerChunk; + copy.treesPerChunk = source.treesPerChunk; copy.extraTreeChance = source.extraTreeChance; + copy.flowersPerChunk = source.flowersPerChunk; copy.grassPerChunk = source.grassPerChunk; + copy.deadBushPerChunk = source.deadBushPerChunk; copy.mushroomsPerChunk = source.mushroomsPerChunk; + copy.reedsPerChunk = source.reedsPerChunk; copy.cactiPerChunk = source.cactiPerChunk; + copy.gravelPatchesPerChunk = source.gravelPatchesPerChunk; + copy.sandPatchesPerChunk = source.sandPatchesPerChunk; + copy.clayPerChunk = source.clayPerChunk; copy.bigMushroomsPerChunk = source.bigMushroomsPerChunk; + copy.generateFalls = source.generateFalls; + return copy; + } + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java new file mode 100644 index 00000000..5ba48d47 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnGeologySampler.java @@ -0,0 +1,135 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Locale; +import java.util.Optional; + +import zone.moddev.mc.orespawn.OreSpawnConfig.GeologyMode; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig; +import zone.moddev.mc.orespawn.worldgen.Geology; +import zone.moddev.mc.orespawn.worldgen.GeomeConfig; +import zone.moddev.mc.orespawn.worldgen.GeomeGeology; +import zone.moddev.mc.orespawn.worldgen.RockFamily; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; +import zone.moddev.mc.orespawn.worldgen.WorldIds; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.biome.Biome; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.world.WorldServer; + +final class OreSpawnGeologySampler implements GeologySampler { + private static final ResourceLocation CYANO_GEOME = new ResourceLocation("orespawn", "cyano"); + + private final WorldServer level; + private final ResourceLocation dimension; + private final BakedGeomeConfig config; + private final GeologyMode mode; + private final GeomeGeology sky; + private final Geology cyano; + + private OreSpawnGeologySampler(WorldServer level) { + this.level = level; + dimension = WorldIds.dimension(level); + config = GeomeConfig.baked(dimension); + WorldGeologyProfile profile = WorldGeologyProfileManager.activeProfile(); + mode = profile.geologyMode(); + if (mode == GeologyMode.LEGACY) { + cyano = new Geology(level.getSeed(), profile.cyanoGeomeSize(), profile.cyanoRockLayerNoise(), + profile.cyanoLayerThickness(), config); + sky = null; + } else { + sky = new GeomeGeology(level.getSeed(), config); + cyano = null; + } + } + + static GeologySampler create(WorldServer level) { + if (level == null || WorldGeologyProfileManager.activeServer() != level.getMinecraftServer()) { + throw new IllegalStateException("The level is not part of OreSpawn's active server"); + } + return new OreSpawnGeologySampler(level); + } + + @Override + public GeologyColumn sampleColumn(int blockX, int blockZ, int surfaceY) { + BlockPos position = new BlockPos(blockX, surfaceY, blockZ); + Biome biome = level.getBiome(position); + ResourceLocation biomeId = WorldIds.biome(biome); + if (biomeId == null) biomeId = new ResourceLocation("orespawn", "unregistered_biome"); + if (mode == GeologyMode.LEGACY) { + return new CyanoColumn(biomeId, blockX, blockZ, surfaceY); + } + GeomeGeology.ColumnSample sample = sky.sampleColumn(biome, biomeId, blockX, blockZ); + return new SkyColumn(biomeId, blockX, blockZ, surfaceY, sample); + } + + private abstract class BaseColumn implements GeologyColumn { + private final ResourceLocation biome; + private final int x; + private final int z; + private final int surfaceY; + + BaseColumn(ResourceLocation biome, int x, int z, int surfaceY) { + this.biome = biome; + this.x = x; + this.z = z; + this.surfaceY = surfaceY; + } + + @Override public ResourceLocation dimension() { return dimension; } + @Override public ResourceLocation biome() { return biome; } + @Override public int blockX() { return x; } + @Override public int blockZ() { return z; } + @Override public int surfaceY() { return surfaceY; } + } + + private final class SkyColumn extends BaseColumn { + private final GeomeGeology.ColumnSample sample; + + SkyColumn(ResourceLocation biome, int x, int z, int surfaceY, GeomeGeology.ColumnSample sample) { + super(biome, x, z, surfaceY); + this.sample = sample; + } + + @Override public ResourceLocation geome() { return geomeId(sample.geomeName()); } + @Override public IBlockState rockAt(int y) { return sample.rockAt(y); } + @Override public Optional familyAt(int y) { return family(sample.familyAt(y)); } + } + + private final class CyanoColumn extends BaseColumn { + CyanoColumn(ResourceLocation biome, int x, int z, int surfaceY) { + super(biome, x, z, surfaceY); + } + + @Override public ResourceLocation geome() { return CYANO_GEOME; } + @Override public IBlockState rockAt(int y) { return cyano.getStoneAt(blockX(), y, blockZ()).getDefaultState(); } + @Override public Optional familyAt(int y) { + Block block = rockAt(y).getBlock(); + for (RockFamily candidate : RockFamily.values()) { + for (IBlockState state : config.statesForFamily(candidate)) { + if (state.getBlock() == block) { + return family(candidate); + } + } + } + return Optional.empty(); + } + } + + private static Optional family(RockFamily family) { + return family == null ? Optional.empty() + : Optional.of(GeologyFamily.valueOf(family.name())); + } + + private static ResourceLocation geomeId(String name) { + try { + return name.indexOf(':') >= 0 ? new ResourceLocation(name) + : new ResourceLocation("orespawn", name.toLowerCase(Locale.ROOT)); + } catch (RuntimeException ignored) { + return new ResourceLocation("orespawn", "unknown"); + } + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnOreIntegration.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnOreIntegration.java new file mode 100644 index 00000000..f58aff59 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnOreIntegration.java @@ -0,0 +1,51 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.Set; + +import com.google.gson.JsonObject; +import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; + +/** + * Compatibility facade for the initial ore-provider status API. + * + * @deprecated Use {@link OreSpawnApi}. This class remains available so + * existing provider mods do not need an immediate source change. + */ +@Deprecated +public final class OreSpawnOreIntegration { + /** @deprecated Use {@link ProviderStatus}. */ + @Deprecated + public enum ProviderStatus { + PENDING, + ACTIVE, + INACTIVE + } + + private OreSpawnOreIntegration() { + } + + public static void initialize() { + WorldgenIntegrationManager.initialize(); + } + + public static ProviderStatus getProviderStatus(String providerModId) { + return ProviderStatus.valueOf(OreSpawnApi.getProviderStatus(providerModId).name()); + } + + public static boolean isProviderActive(String providerModId) { + return OreSpawnApi.isOreTakeoverActive(providerModId); + } + + public static void markFeatureReady() { + WorldgenIntegrationManager.markFeatureReady(); + } + + /** Merge all provider contributions while retaining the historical method name. */ + public static boolean mergeProviderOres(JsonObject target) { + return WorldgenIntegrationManager.mergeProviderDefinitions(target); + } + + public static Set activeProviderIds() { + return WorldgenIntegrationManager.activeProviderIds(); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnPatternRegistry.java b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnPatternRegistry.java new file mode 100644 index 00000000..3a2cf50a --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/OreSpawnPatternRegistry.java @@ -0,0 +1,25 @@ +package zone.moddev.mc.orespawn.api; + +import java.util.function.Supplier; + +import zone.moddev.mc.orespawn.OreSpawn; + +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.registry.IForgeRegistry; + +/** Stable entry point for mods that register codec-backed ore patterns. */ +public final class OreSpawnPatternRegistry { + public static final ResourceLocation REGISTRY_NAME = + new ResourceLocation(OreSpawn.MODID, "ore_pattern_types"); + + private OreSpawnPatternRegistry() { + } + + public static IForgeRegistry registry() { + return zone.moddev.mc.orespawn.init.OreSpawnPatterns.registry(); + } + + public static Supplier> registrySupplier() { + return zone.moddev.mc.orespawn.init.OreSpawnPatterns.registrySupplier(); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/ProviderStatus.java b/src/main/java/zone/moddev/mc/orespawn/api/ProviderStatus.java new file mode 100644 index 00000000..546d07f2 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/ProviderStatus.java @@ -0,0 +1,8 @@ +package zone.moddev.mc.orespawn.api; + +/** Lifecycle state of a OreSpawn world-generation provider. */ +public enum ProviderStatus { + PENDING, + ACTIVE, + INACTIVE +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/StandardPatternSettings.java b/src/main/java/zone/moddev/mc/orespawn/api/StandardPatternSettings.java new file mode 100644 index 00000000..f39b5977 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/StandardPatternSettings.java @@ -0,0 +1,48 @@ +package zone.moddev.mc.orespawn.api; + +import com.mojang.serialization.Codec; + +import com.google.gson.JsonObject; + +/** Shared bounded settings understood by OreSpawn's six built-in patterns. */ +public final class StandardPatternSettings { + public static final Codec CODEC = Codec.of(element -> { + JsonObject json = element == null || !element.isJsonObject() + ? new JsonObject() : element.getAsJsonObject(); + return new StandardPatternSettings(integer(json, "spread", 8), + integer(json, "vertical_spread", 4), integer(json, "node_size", 4), + integer(json, "length", 16), string(json, "fluid", "minecraft:water")); + }); + + private final int spread; + private final int verticalSpread; + private final int nodeSize; + private final int length; + private final String fluid; + + public StandardPatternSettings(int spread, int verticalSpread, int nodeSize, int length, String fluid) { + this.spread = bounded(spread, 0, 64); + this.verticalSpread = bounded(verticalSpread, 0, 64); + this.nodeSize = bounded(nodeSize, 1, 32); + this.length = bounded(length, 1, 64); + this.fluid = fluid; + } + + public int spread() { return spread; } + public int verticalSpread() { return verticalSpread; } + public int nodeSize() { return nodeSize; } + public int length() { return length; } + public String fluid() { return fluid; } + + private static int bounded(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private static int integer(JsonObject json, String key, int fallback) { + return json.has(key) ? json.get(key).getAsInt() : fallback; + } + + private static String string(JsonObject json, String key, String fallback) { + return json.has(key) ? json.get(key).getAsString() : fallback; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java new file mode 100644 index 00000000..6d037627 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java @@ -0,0 +1,1753 @@ +package zone.moddev.mc.orespawn.api; + +import zone.moddev.mc.orespawn.util.JsonCopies; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import net.minecraft.util.ResourceLocation; + +/** + * Immutable declarative world-generation contribution submitted to OreSpawn. + * All registry objects are represented by IDs and resolved only when OreSpawn + * freezes and bakes the active provider set. + */ +public final class WorldgenProvider { + private final String modId; + private final int revision; + private final JsonObject definition; + + private WorldgenProvider(String modId, int revision, JsonObject definition) { + this.modId = modId; + this.revision = revision; + this.definition = JsonCopies.copy(definition); + } + + public static Builder builder(String modId, int revision) { + return new Builder(modId, revision); + } + + public String modId() { + return modId; + } + + public int revision() { + return revision; + } + + /** Returns a defensive JSON representation matching provider schema 4. */ + public JsonObject toJson() { + return JsonCopies.copy(definition); + } + + public static final class Builder { + private final String modId; + private final int revision; + private final LinkedHashMap rocks = new LinkedHashMap<>(); + private final LinkedHashMap ores = new LinkedHashMap<>(); + private final LinkedHashMap fluidDeposits = + new LinkedHashMap<>(); + private final LinkedHashMap geomes = new LinkedHashMap<>(); + private final LinkedHashMap biomeRules = new LinkedHashMap<>(); + private final LinkedHashMap terrainDimensions = + new LinkedHashMap<>(); + private final LinkedHashMap biomePalettes = + new LinkedHashMap<>(); + private final LinkedHashMap dimensionMaterials = + new LinkedHashMap<>(); + private final LinkedHashMap templates = new LinkedHashMap<>(); + + private Builder(String modId, int revision) { + this.modId = requireModId(modId); + if (revision < 1) { + throw new IllegalArgumentException("Provider revision must be at least 1"); + } + this.revision = revision; + } + + public Builder rock(RockDefinition rock) { + putUnique(rocks, rock.id(), rock, "rock"); + return this; + } + + public Builder rock(ResourceLocation block, GeologyFamily family, Consumer edit) { + RockDefinition.Builder builder = RockDefinition.builder(ownedId("rock", block), block, family); + edit.accept(builder); + return rock(builder.build()); + } + + public Builder rock(ResourceLocation id, ResourceLocation block, GeologyFamily family, + Consumer edit) { + RockDefinition.Builder builder = RockDefinition.builder(id, block, family); + edit.accept(builder); + return rock(builder.build()); + } + + public Builder ore(OreDefinition ore) { + putUnique(ores, ore.id(), ore, "ore"); + return this; + } + + public Builder ore(ResourceLocation block, Consumer edit) { + OreDefinition.Builder builder = OreDefinition.builder(ownedId("ore", block), block); + edit.accept(builder); + return ore(builder.build()); + } + + public Builder ore(ResourceLocation id, ResourceLocation block, + Consumer edit) { + OreDefinition.Builder builder = OreDefinition.builder(id, block); + edit.accept(builder); + return ore(builder.build()); + } + + public Builder fluidDeposit(FluidDepositDefinition deposit) { + putUnique(fluidDeposits, deposit.id(), deposit, "fluid deposit"); + return this; + } + + public Builder fluidDeposit(ResourceLocation id, ResourceLocation block, + Consumer edit) { + FluidDepositDefinition.Builder builder = FluidDepositDefinition.builder(id, block); + edit.accept(builder); + return fluidDeposit(builder.build()); + } + + public Builder geome(GeomeDefinition geome) { + putUnique(geomes, geome.id(), geome, "geome"); + return this; + } + + public Builder geome(ResourceLocation id, Consumer edit) { + GeomeDefinition.Builder builder = GeomeDefinition.builder(id); + edit.accept(builder); + return geome(builder.build()); + } + + public Builder biome(BiomeRule biome) { + putUnique(biomeRules, biome.biome(), biome, "biome rule"); + return this; + } + + public Builder biome(ResourceLocation biome, Map geomeWeights) { + return biome(new BiomeRule(biome, geomeWeights)); + } + + public Builder terrainDimension(TerrainDimensionDefinition dimension) { + putUnique(terrainDimensions, dimension.dimension(), dimension, "terrain dimension"); + return this; + } + + public Builder terrainDimension(ResourceLocation dimension, + Consumer edit) { + TerrainDimensionDefinition.Builder builder = TerrainDimensionDefinition.builder(dimension); + edit.accept(builder); + return terrainDimension(builder.build()); + } + + public Builder biomePalette(BiomePaletteDefinition palette) { + putUnique(biomePalettes, palette.id(), palette, "biome palette"); + return this; + } + + public Builder biomePalette(ResourceLocation id, ResourceLocation dimension, + Consumer edit) { + BiomePaletteDefinition.Builder builder = BiomePaletteDefinition.builder(id, dimension); + edit.accept(builder); + return biomePalette(builder.build()); + } + + public Builder dimensionMaterials(DimensionMaterialsDefinition materials) { + putUnique(dimensionMaterials, materials.id(), materials, "dimension materials"); + return this; + } + + public Builder dimensionMaterials(ResourceLocation id, ResourceLocation dimension, + Consumer edit) { + DimensionMaterialsDefinition.Builder builder = + DimensionMaterialsDefinition.builder(id, dimension); + edit.accept(builder); + return dimensionMaterials(builder.build()); + } + + public Builder template(GeologyTemplate template) { + putUnique(templates, template.id(), template, "template"); + return this; + } + + public Builder template(ResourceLocation id, Consumer edit) { + GeologyTemplate.Builder builder = GeologyTemplate.builder(id); + edit.accept(builder); + return template(builder.build()); + } + + public WorldgenProvider build() { + if (rocks.isEmpty() && ores.isEmpty() && fluidDeposits.isEmpty() + && geomes.isEmpty() && biomeRules.isEmpty() + && terrainDimensions.isEmpty() && biomePalettes.isEmpty() + && dimensionMaterials.isEmpty() && templates.isEmpty()) { + throw new IllegalStateException("A provider must declare at least one contribution"); + } + requireOwned(rocks.keySet(), "rock"); + requireOwned(ores.keySet(), "ore"); + requireOwned(fluidDeposits.keySet(), "fluid deposit"); + requireOwned(geomes.keySet(), "geome"); + // Terrain-dimension keys identify Minecraft dimensions. Unlike provider-owned + // rocks, ores, palettes and templates, they are intentionally not namespaced + // to the provider (for example minecraft:overworld or minecraft:the_end). + requireOwned(biomePalettes.keySet(), "biome palette"); + requireOwned(dimensionMaterials.keySet(), "dimension materials"); + requireOwned(templates.keySet(), "template"); + JsonObject root = new JsonObject(); + root.addProperty("schema_version", 4); + root.addProperty("provider_modid", modId); + root.addProperty("provider_revision", revision); + root.add("rocks", object(rocks)); + root.add("ores", object(ores)); + root.add("fluid_deposits", object(fluidDeposits)); + root.add("geomes", object(geomes)); + root.add("biome_rules", object(biomeRules)); + root.add("terrain_dimensions", object(terrainDimensions)); + root.add("biome_palettes", object(biomePalettes)); + root.add("dimension_materials", object(dimensionMaterials)); + root.add("templates", object(templates)); + return new WorldgenProvider(modId, revision, root); + } + + private void requireOwned(Collection ids, String type) { + for (ResourceLocation id : ids) { + if (!modId.equals(id.getResourceDomain())) { + throw new IllegalStateException("Provider " + modId + " does not own " + type + " " + id); + } + } + } + + private ResourceLocation ownedId(String kind, ResourceLocation output) { + return new ResourceLocation(modId, kind + "/" + output.getResourceDomain() + "/" + output.getResourcePath()); + } + } + + public static final class RockDefinition implements JsonDefinition { + private final ResourceLocation id; + private final ResourceLocation block; + private final boolean enabled; + private final GeologyFamily family; + private final int depthPeak; + private final int depthSpread; + private final int minY; + private final int maxY; + private final double weight; + private final boolean oreReplaceable; + private final Map geomes; + private final Set dimensions; + + private RockDefinition(Builder builder) { + id = builder.id; + block = builder.block; + enabled = builder.enabled; + family = builder.family; + depthPeak = builder.depthPeak; + depthSpread = builder.depthSpread; + minY = builder.minY; + maxY = builder.maxY; + weight = builder.weight; + oreReplaceable = builder.oreReplaceable; + geomes = immutableMap(builder.geomes); + dimensions = immutableSet(builder.dimensions); + } + + public static Builder builder(ResourceLocation block, GeologyFamily family) { + return new Builder(block, block, family); + } + + public static Builder builder(ResourceLocation id, ResourceLocation block, GeologyFamily family) { + return new Builder(id, block, family); + } + + public ResourceLocation id() { return id; } + public ResourceLocation block() { return block; } + public boolean enabled() { return enabled; } + public GeologyFamily family() { return family; } + public int depthPeak() { return depthPeak; } + public int depthSpread() { return depthSpread; } + public int minY() { return minY; } + public int maxY() { return maxY; } + public double weight() { return weight; } + public boolean oreReplaceable() { return oreReplaceable; } + public Map geomes() { return geomes; } + public Set dimensions() { return dimensions; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("block", block.toString()); + json.addProperty("enabled", enabled); + json.addProperty("family", family.configName()); + json.addProperty("depth_peak", depthPeak); + json.addProperty("depth_spread", depthSpread); + json.addProperty("min_y", minY); + json.addProperty("max_y", maxY); + json.addProperty("weight", weight); + json.addProperty("ore_replaceable", oreReplaceable); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "geomes", weights(geomes)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "dimensions", ids(dimensions)); + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private final ResourceLocation block; + private final GeologyFamily family; + private boolean enabled = true; + private int depthPeak = 48; + private int depthSpread = 40; + private int minY = 0; + private int maxY = 255; + private double weight = 1.0D; + private boolean oreReplaceable = true; + private final Map geomes = new LinkedHashMap<>(); + private final Set dimensions = new LinkedHashSet<>(); + + private Builder(ResourceLocation id, ResourceLocation block, GeologyFamily family) { + this.id = Objects.requireNonNull(id, "id"); + this.block = Objects.requireNonNull(block, "block"); + this.family = Objects.requireNonNull(family, "family"); + dimensions.add(new ResourceLocation("minecraft", "overworld")); + } + + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder depth(int peak, int spread) { depthPeak = peak; depthSpread = spread; return this; } + public Builder yRange(int min, int max) { minY = min; maxY = max; return this; } + public Builder weight(double value) { weight = value; return this; } + public Builder oreReplaceable(boolean value) { oreReplaceable = value; return this; } + public Builder geomeWeight(ResourceLocation geome, double value) { geomes.put(geome, value); return this; } + public Builder dimensions(Collection values) { dimensions.clear(); dimensions.addAll(values); return this; } + public Builder dimension(ResourceLocation value) { dimensions.add(value); return this; } + + public RockDefinition build() { + requireRange(minY, maxY, "rock Y range"); + if (depthSpread < 1 || weight < 0.0D || !Double.isFinite(weight) || dimensions.isEmpty()) { + throw new IllegalStateException("Invalid rock depth, weight, or dimensions for " + block); + } + return new RockDefinition(this); + } + } + } + + public static final class OreDefinition implements JsonDefinition { + private final ResourceLocation id; + private final ResourceLocation block; + private final boolean enabled; + private final boolean nativeGeneration; + private final ResourceLocation deepOutput; + private final int deepOutputMaxY; + private final List outputs; + private final boolean suppressVanilla; + private final boolean retrogen; + private final Map dimensions; + private final Map dimensionSelectors; + + private OreDefinition(Builder builder) { + id = builder.id; + block = builder.block; + enabled = builder.enabled; + nativeGeneration = builder.nativeGeneration; + deepOutput = builder.deepOutput; + deepOutputMaxY = builder.deepOutputMaxY; + outputs = Collections.unmodifiableList(new ArrayList<>(builder.outputs)); + suppressVanilla = builder.suppressVanilla; + retrogen = builder.retrogen; + dimensions = Collections.unmodifiableMap(new LinkedHashMap<>(builder.dimensions)); + dimensionSelectors = Collections.unmodifiableMap(new LinkedHashMap<>(builder.dimensionSelectors)); + } + + public static Builder builder(ResourceLocation block) { return new Builder(block, block); } + public static Builder builder(ResourceLocation id, ResourceLocation block) { return new Builder(id, block); } + public ResourceLocation id() { return id; } + public ResourceLocation block() { return block; } + public boolean enabled() { return enabled; } + public List outputs() { return outputs; } + public boolean suppressVanilla() { return suppressVanilla; } + public boolean retrogen() { return retrogen; } + public Map dimensions() { return dimensions; } + public Map dimensionSelectors() { return dimensionSelectors; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("block", block.toString()); + json.addProperty("enabled", enabled); + json.addProperty("native_generation", nativeGeneration); + json.addProperty("suppress_vanilla", suppressVanilla); + json.addProperty("retrogen", retrogen); + if (!outputs.isEmpty()) { + JsonArray values = new JsonArray(); + for (OreOutputDefinition output : outputs) zone.moddev.mc.orespawn.util.JsonCopies.add(values, output.toJson()); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "outputs", values); + } + if (deepOutput != null) { + json.addProperty("deep_output", deepOutput.toString()); + json.addProperty("deep_output_max_y", deepOutputMaxY); + } + if (!dimensions.isEmpty()) zone.moddev.mc.orespawn.util.JsonCopies.add(json, "dimensions", object(dimensions)); + JsonObject selectors = new JsonObject(); + for (Entry entry : dimensionSelectors.entrySet()) { + selectors.add(entry.getKey().id().toString(), entry.getValue().toJson()); + } + if (zone.moddev.mc.orespawn.util.JsonCopies.size(selectors) > 0) zone.moddev.mc.orespawn.util.JsonCopies.add(json, "dimension_selectors", selectors); + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private final ResourceLocation block; + private boolean enabled = true; + private boolean nativeGeneration; + private ResourceLocation deepOutput; + private int deepOutputMaxY = -1; + private final List outputs = new ArrayList<>(); + private boolean suppressVanilla; + private boolean retrogen = true; + private final LinkedHashMap dimensions = new LinkedHashMap<>(); + private final LinkedHashMap dimensionSelectors = + new LinkedHashMap<>(); + + private Builder(ResourceLocation id, ResourceLocation block) { + this.id = Objects.requireNonNull(id, "id"); + this.block = Objects.requireNonNull(block, "block"); + } + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder nativeGeneration(boolean value) { nativeGeneration = value; return this; } + public Builder suppressVanilla(boolean value) { suppressVanilla = value; return this; } + public Builder retrogen(boolean value) { retrogen = value; return this; } + public Builder deepOutput(ResourceLocation value, int maxY) { deepOutput = value; deepOutputMaxY = maxY; return this; } + public Builder output(ResourceLocation value, double weight) { + return output(value, weight, -2048, 2048); + } + public Builder output(ResourceLocation value, double weight, int minY, int maxY) { + outputs.add(new OreOutputDefinition(value, weight, minY, maxY)); + return this; + } + public Builder dimension(OreDimensionDefinition value) { + putUnique(dimensions, value.dimension(), value, "ore dimension"); + return this; + } + public Builder dimension(ResourceLocation id, Consumer edit) { + OreDimensionDefinition.Builder builder = OreDimensionDefinition.builder(id); + edit.accept(builder); + return dimension(builder.build()); + } + public Builder dimensionSelector(OreDimensionSelector selector, OreDimensionDefinition value) { + Objects.requireNonNull(selector, "selector"); + if (!selector.id().equals(value.dimension())) { + throw new IllegalArgumentException("Selector rule ID does not match " + selector.id()); + } + if (dimensionSelectors.putIfAbsent(selector, value) != null) { + throw new IllegalStateException("Duplicate ore dimension selector: " + selector.id()); + } + return this; + } + /** Adds a built-in fallback policy used when no explicit dimension rule exists. */ + public Builder dimensionSelector(OreDimensionSelector selector, + Consumer edit) { + OreDimensionDefinition.Builder builder = OreDimensionDefinition.builder(selector.id()); + edit.accept(builder); + return dimensionSelector(selector, builder.build()); + } + + public OreDefinition build() { + if (dimensions.isEmpty() && dimensionSelectors.isEmpty()) { + throw new IllegalStateException("Ore has no dimensions or dimension selectors: " + block); + } + return new OreDefinition(this); + } + } + } + + /** One weighted output choice for an ore rule, optionally restricted by Y. */ + public static final class OreOutputDefinition implements JsonDefinition { + private final ResourceLocation block; + private final double weight; + private final int minY; + private final int maxY; + + private OreOutputDefinition(ResourceLocation block, double weight, int minY, int maxY) { + this.block = Objects.requireNonNull(block, "block"); + if (!Double.isFinite(weight) || weight <= 0.0D) { + throw new IllegalArgumentException("Output weight must be positive for " + block); + } + requireRange(minY, maxY, "output Y range"); + this.weight = weight; + this.minY = minY; + this.maxY = maxY; + } + + public ResourceLocation block() { return block; } + public double weight() { return weight; } + public int minY() { return minY; } + public int maxY() { return maxY; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("block", block.toString()); + json.addProperty("weight", weight); + json.addProperty("min_y", minY); + json.addProperty("max_y", maxY); + return json; + } + } + + public static final class OreDimensionDefinition implements JsonDefinition { + private final ResourceLocation dimension; + private final boolean enabled; + private final int minY; + private final int maxY; + private final double frequency; + private final int minQuantity; + private final int maxQuantity; + private final OrePattern pattern; + private final ResourceLocation patternType; + private final JsonObject patternSettings; + private final OreHeightDistribution heightDistribution; + private final double discardChanceOnAirExposure; + private final int spread; + private final int verticalSpread; + private final int nodeSize; + private final Set hostFamilies; + private final Map geomes; + private final Set hostBlocks; + private final Set hostTags; + private final Set biomeIds; + private final Set excludedBiomeIds; + private final Set biomeDictionary; + private final Set excludedBiomeDictionary; + private final Map hostBlockWeights; + private final Map hostTagWeights; + + private OreDimensionDefinition(Builder builder) { + dimension = builder.dimension; + enabled = builder.enabled; + minY = builder.minY; + maxY = builder.maxY; + frequency = builder.frequency; + minQuantity = builder.minQuantity; + maxQuantity = builder.maxQuantity; + pattern = builder.pattern; + patternType = builder.patternType; + patternSettings = JsonCopies.copy(builder.patternSettings); + heightDistribution = builder.heightDistribution; + discardChanceOnAirExposure = builder.discardChanceOnAirExposure; + spread = builder.spread; + verticalSpread = builder.verticalSpread; + nodeSize = builder.nodeSize; + hostFamilies = Collections.unmodifiableSet(new LinkedHashSet<>(builder.hostFamilies)); + geomes = immutableMap(builder.geomes); + hostBlocks = immutableSet(builder.hostBlocks); + hostTags = immutableSet(builder.hostTags); + biomeIds = immutableSet(builder.biomeIds); + excludedBiomeIds = immutableSet(builder.excludedBiomeIds); + biomeDictionary = Collections.unmodifiableSet(new LinkedHashSet<>(builder.biomeDictionary)); + excludedBiomeDictionary = Collections.unmodifiableSet( + new LinkedHashSet<>(builder.excludedBiomeDictionary)); + hostBlockWeights = immutableMap(builder.hostBlockWeights); + hostTagWeights = immutableMap(builder.hostTagWeights); + } + + public static Builder builder(ResourceLocation dimension) { return new Builder(dimension); } + public ResourceLocation dimension() { return dimension; } + public boolean enabled() { return enabled; } + public int minY() { return minY; } + public int maxY() { return maxY; } + public double attempts() { return frequency; } + /** Compatibility value for consumers that only understand a fixed quantity. */ + public int quantity() { return (minQuantity + maxQuantity + 1) / 2; } + public int minQuantity() { return minQuantity; } + public int maxQuantity() { return maxQuantity; } + public OrePattern pattern() { return pattern; } + public ResourceLocation patternType() { return patternType; } + public JsonObject patternSettings() { return JsonCopies.copy(patternSettings); } + public OreHeightDistribution heightDistribution() { return heightDistribution; } + public double discardChanceOnAirExposure() { return discardChanceOnAirExposure; } + public int spread() { return spread; } + public int verticalSpread() { return verticalSpread; } + public int nodeSize() { return nodeSize; } + public Set hostFamilies() { return hostFamilies; } + public Map geomes() { return geomes; } + public Set hostBlocks() { return hostBlocks; } + public Set hostTags() { return hostTags; } + public Set biomeIds() { return biomeIds; } + public Set excludedBiomeIds() { return excludedBiomeIds; } + public Set biomeDictionary() { return biomeDictionary; } + public Set excludedBiomeDictionary() { return excludedBiomeDictionary; } + public Map hostBlockWeights() { return hostBlockWeights; } + public Map hostTagWeights() { return hostTagWeights; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("enabled", enabled); + json.addProperty("min_y", minY); + json.addProperty("max_y", maxY); + json.addProperty("frequency", frequency); + if (minQuantity == maxQuantity) { + json.addProperty("quantity", minQuantity); + } else { + json.addProperty("min_quantity", minQuantity); + json.addProperty("max_quantity", maxQuantity); + } + if (patternType == null) { + json.addProperty("pattern", pattern.configName()); + } else { + JsonObject configuredPattern = new JsonObject(); + configuredPattern.addProperty("type", patternType.toString()); + configuredPattern.add("settings", JsonCopies.copy(patternSettings)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "pattern", configuredPattern); + } + json.addProperty("height_distribution", heightDistribution.configName()); + json.addProperty("discard_chance_on_air_exposure", discardChanceOnAirExposure); + json.addProperty("spread", spread); + json.addProperty("vertical_spread", verticalSpread); + json.addProperty("node_size", nodeSize); + JsonArray families = new JsonArray(); + for (GeologyFamily family : hostFamilies) { zone.moddev.mc.orespawn.util.JsonCopies.add(families, family.configName()); } + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_families", families); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "geomes", weights(geomes)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_blocks", weightedIds(hostBlocks, hostBlockWeights, "block")); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_tags", weightedIds(hostTags, hostTagWeights, "tag")); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biome_ids", ids(biomeIds)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "excluded_biome_ids", ids(excludedBiomeIds)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biome_dictionary", strings(biomeDictionary)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "excluded_biome_dictionary", strings(excludedBiomeDictionary)); + return json; + } + + public static final class Builder { + private final ResourceLocation dimension; + private boolean enabled = true; + private int minY = 0; + private int maxY = 255; + private double frequency = 1.0D; + private int minQuantity = 8; + private int maxQuantity = 8; + private OrePattern pattern = OrePattern.VEIN; + private ResourceLocation patternType; + private JsonObject patternSettings = new JsonObject(); + private OreHeightDistribution heightDistribution = OreHeightDistribution.UNIFORM; + private double discardChanceOnAirExposure; + private int spread = 8; + private int verticalSpread = 4; + private int nodeSize = 4; + private final Set hostFamilies = new LinkedHashSet<>(); + private final Map geomes = new LinkedHashMap<>(); + private final Set hostBlocks = new LinkedHashSet<>(); + private final Set hostTags = new LinkedHashSet<>(); + private final Set biomeIds = new LinkedHashSet<>(); + private final Set excludedBiomeIds = new LinkedHashSet<>(); + private final Set biomeDictionary = new LinkedHashSet<>(); + private final Set excludedBiomeDictionary = new LinkedHashSet<>(); + private final Map hostBlockWeights = new LinkedHashMap<>(); + private final Map hostTagWeights = new LinkedHashMap<>(); + + private Builder(ResourceLocation dimension) { this.dimension = Objects.requireNonNull(dimension, "dimension"); } + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder yRange(int min, int max) { minY = min; maxY = max; return this; } + public Builder attempts(double value) { frequency = value; return this; } + public Builder quantity(int value) { minQuantity = value; maxQuantity = value; return this; } + /** Selects an inclusive random block budget for each placement attempt. */ + public Builder quantityRange(int min, int max) { minQuantity = min; maxQuantity = max; return this; } + public Builder pattern(OrePattern value) { + pattern = Objects.requireNonNull(value); + patternType = null; + patternSettings = new JsonObject(); + return this; + } + /** Uses a codec-backed pattern registered through {@link OreSpawnPatternRegistry}. */ + public Builder pattern(ResourceLocation type, JsonObject settings) { + patternType = Objects.requireNonNull(type, "type"); + patternSettings = JsonCopies.copy(Objects.requireNonNull(settings, "settings")); + return this; + } + public Builder heightDistribution(OreHeightDistribution value) { heightDistribution = Objects.requireNonNull(value); return this; } + public Builder discardChanceOnAirExposure(double value) { discardChanceOnAirExposure = value; return this; } + public Builder spread(int horizontal, int vertical) { spread = horizontal; verticalSpread = vertical; return this; } + public Builder nodeSize(int value) { nodeSize = value; return this; } + public Builder hostFamily(GeologyFamily value) { hostFamilies.add(value); return this; } + public Builder geomeWeight(ResourceLocation geome, double value) { geomes.put(geome, value); return this; } + public Builder hostBlock(ResourceLocation value) { hostBlocks.add(value); return this; } + public Builder hostTag(ResourceLocation value) { hostTags.add(value); return this; } + public Builder biome(ResourceLocation value) { biomeIds.add(value); return this; } + public Builder excludeBiome(ResourceLocation value) { excludedBiomeIds.add(value); return this; } + public Builder biomeDictionary(String value) { biomeDictionary.add(nonBlank(value)); return this; } + public Builder excludeBiomeDictionary(String value) { + excludedBiomeDictionary.add(nonBlank(value)); return this; + } + public Builder hostBlock(ResourceLocation value, double weight) { + hostBlocks.add(value); + hostBlockWeights.put(value, replacementWeight(weight)); + return this; + } + public Builder hostTag(ResourceLocation value, double weight) { + hostTags.add(value); + hostTagWeights.put(value, replacementWeight(weight)); + return this; + } + + public OreDimensionDefinition build() { + requireRange(minY, maxY, "ore Y range"); + if (frequency < 0.0D || frequency > 64.0D + || minQuantity < 1 || minQuantity > maxQuantity || maxQuantity > 64 + || !Double.isFinite(discardChanceOnAirExposure) + || discardChanceOnAirExposure < 0.0D || discardChanceOnAirExposure > 1.0D + || spread < 0 || spread > 64 || verticalSpread < 0 || verticalSpread > 64 + || nodeSize < 1 || nodeSize > 32) { + throw new IllegalStateException("Invalid ore placement values for " + dimension); + } + if (enabled && hostFamilies.isEmpty() && hostBlocks.isEmpty() && hostTags.isEmpty()) { + throw new IllegalStateException("Enabled ore dimension has no hosts: " + dimension); + } + return new OreDimensionDefinition(this); + } + } + } + + /** A provider-owned underground deposit made from a registered fluid block. */ + public static final class FluidDepositDefinition implements JsonDefinition { + private final ResourceLocation id; + private final ResourceLocation block; + private final boolean enabled; + private final Map dimensions; + + private FluidDepositDefinition(Builder builder) { + id = builder.id; + block = builder.block; + enabled = builder.enabled; + dimensions = Collections.unmodifiableMap(new LinkedHashMap<>(builder.dimensions)); + } + + public static Builder builder(ResourceLocation id, ResourceLocation block) { + return new Builder(id, block); + } + + public ResourceLocation id() { return id; } + public ResourceLocation block() { return block; } + public boolean enabled() { return enabled; } + public Map dimensions() { return dimensions; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("block", block.toString()); + json.addProperty("enabled", enabled); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "dimensions", object(dimensions)); + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private final ResourceLocation block; + private boolean enabled = true; + private final LinkedHashMap dimensions = + new LinkedHashMap<>(); + + private Builder(ResourceLocation id, ResourceLocation block) { + this.id = Objects.requireNonNull(id, "id"); + this.block = Objects.requireNonNull(block, "block"); + } + + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder dimension(FluidDepositDimensionDefinition value) { + putUnique(dimensions, value.dimension(), value, "fluid deposit dimension"); + return this; + } + public Builder dimension(ResourceLocation id, + Consumer edit) { + FluidDepositDimensionDefinition.Builder builder = FluidDepositDimensionDefinition.builder(id); + edit.accept(builder); + return dimension(builder.build()); + } + + public FluidDepositDefinition build() { + if (dimensions.isEmpty()) { + throw new IllegalStateException("Fluid deposit has no dimensions: " + id); + } + return new FluidDepositDefinition(this); + } + } + } + + /** Placement and host rules for one fluid deposit in one dimension. */ + public static final class FluidDepositDimensionDefinition implements JsonDefinition { + private final ResourceLocation dimension; + private final boolean enabled; + private final int minY; + private final int maxY; + private final double frequency; + private final int minRadius; + private final int maxRadius; + private final int minVerticalRadius; + private final int maxVerticalRadius; + private final int maxLobes; + private final int minSolidCover; + private final int minSolidShell; + private final Set hostFamilies; + private final Set hostBlocks; + private final Set hostTags; + private final Set biomeIds; + private final Set excludedBiomeIds; + private final Set biomeDictionary; + private final Set excludedBiomeDictionary; + private final Map geomes; + + private FluidDepositDimensionDefinition(Builder builder) { + dimension = builder.dimension; + enabled = builder.enabled; + minY = builder.minY; + maxY = builder.maxY; + frequency = builder.frequency; + minRadius = builder.minRadius; + maxRadius = builder.maxRadius; + minVerticalRadius = builder.minVerticalRadius; + maxVerticalRadius = builder.maxVerticalRadius; + maxLobes = builder.maxLobes; + minSolidCover = builder.minSolidCover; + minSolidShell = builder.minSolidShell; + hostFamilies = Collections.unmodifiableSet(new LinkedHashSet<>(builder.hostFamilies)); + hostBlocks = immutableSet(builder.hostBlocks); + hostTags = immutableSet(builder.hostTags); + biomeIds = immutableSet(builder.biomeIds); + excludedBiomeIds = immutableSet(builder.excludedBiomeIds); + biomeDictionary = Collections.unmodifiableSet(new LinkedHashSet<>(builder.biomeDictionary)); + excludedBiomeDictionary = Collections.unmodifiableSet( + new LinkedHashSet<>(builder.excludedBiomeDictionary)); + geomes = immutableMap(builder.geomes); + } + + public static Builder builder(ResourceLocation dimension) { return new Builder(dimension); } + public ResourceLocation dimension() { return dimension; } + public boolean enabled() { return enabled; } + public int minY() { return minY; } + public int maxY() { return maxY; } + public double attempts() { return frequency; } + public int minRadius() { return minRadius; } + public int maxRadius() { return maxRadius; } + public int minVerticalRadius() { return minVerticalRadius; } + public int maxVerticalRadius() { return maxVerticalRadius; } + public int maxLobes() { return maxLobes; } + public int minSolidCover() { return minSolidCover; } + public int minSolidShell() { return minSolidShell; } + public Set hostFamilies() { return hostFamilies; } + public Set hostBlocks() { return hostBlocks; } + public Set hostTags() { return hostTags; } + public Set biomeIds() { return biomeIds; } + public Set excludedBiomeIds() { return excludedBiomeIds; } + public Set biomeDictionary() { return biomeDictionary; } + public Set excludedBiomeDictionary() { return excludedBiomeDictionary; } + public Map geomes() { return geomes; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("enabled", enabled); + json.addProperty("min_y", minY); + json.addProperty("max_y", maxY); + json.addProperty("frequency", frequency); + json.addProperty("min_radius", minRadius); + json.addProperty("max_radius", maxRadius); + json.addProperty("min_vertical_radius", minVerticalRadius); + json.addProperty("max_vertical_radius", maxVerticalRadius); + json.addProperty("max_lobes", maxLobes); + json.addProperty("min_solid_cover", minSolidCover); + json.addProperty("min_solid_shell", minSolidShell); + JsonArray families = new JsonArray(); + for (GeologyFamily family : hostFamilies) zone.moddev.mc.orespawn.util.JsonCopies.add(families, family.configName()); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_families", families); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_blocks", ids(hostBlocks)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_tags", ids(hostTags)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biome_ids", ids(biomeIds)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "excluded_biome_ids", ids(excludedBiomeIds)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biome_dictionary", strings(biomeDictionary)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "excluded_biome_dictionary", strings(excludedBiomeDictionary)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "geomes", weights(geomes)); + return json; + } + + public static final class Builder { + private final ResourceLocation dimension; + private boolean enabled = true; + private int minY = 0; + private int maxY = 48; + private double frequency = 0.08D; + private int minRadius = 5; + private int maxRadius = 12; + private int minVerticalRadius = 2; + private int maxVerticalRadius = 5; + private int maxLobes = 4; + private int minSolidCover = 2; + private int minSolidShell = 1; + private final Set hostFamilies = new LinkedHashSet<>(); + private final Set hostBlocks = new LinkedHashSet<>(); + private final Set hostTags = new LinkedHashSet<>(); + private final Set biomeIds = new LinkedHashSet<>(); + private final Set excludedBiomeIds = new LinkedHashSet<>(); + private final Set biomeDictionary = new LinkedHashSet<>(); + private final Set excludedBiomeDictionary = new LinkedHashSet<>(); + private final Map geomes = new LinkedHashMap<>(); + + private Builder(ResourceLocation dimension) { + this.dimension = Objects.requireNonNull(dimension, "dimension"); + } + + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder yRange(int min, int max) { minY = min; maxY = max; return this; } + public Builder attempts(double value) { frequency = value; return this; } + public Builder radius(int min, int max) { minRadius = min; maxRadius = max; return this; } + public Builder verticalRadius(int min, int max) { + minVerticalRadius = min; maxVerticalRadius = max; return this; + } + public Builder maxLobes(int value) { maxLobes = value; return this; } + public Builder minSolidCover(int value) { minSolidCover = value; return this; } + public Builder minSolidShell(int value) { minSolidShell = value; return this; } + public Builder hostFamily(GeologyFamily value) { hostFamilies.add(value); return this; } + public Builder hostBlock(ResourceLocation value) { hostBlocks.add(value); return this; } + public Builder hostTag(ResourceLocation value) { hostTags.add(value); return this; } + public Builder biome(ResourceLocation value) { biomeIds.add(value); return this; } + public Builder excludeBiome(ResourceLocation value) { excludedBiomeIds.add(value); return this; } + public Builder biomeDictionary(String value) { biomeDictionary.add(nonBlank(value)); return this; } + public Builder excludeBiomeDictionary(String value) { + excludedBiomeDictionary.add(nonBlank(value)); return this; + } + public Builder geomeWeight(ResourceLocation geome, double value) { + if (!Double.isFinite(value) || value < 0.0D) { + throw new IllegalArgumentException("Geome weight must be finite and non-negative"); + } + geomes.put(geome, value); + return this; + } + + public FluidDepositDimensionDefinition build() { + requireRange(minY, maxY, "fluid deposit Y range"); + if (!Double.isFinite(frequency) || frequency < 0.0D || frequency > 64.0D + || minRadius < 1 || minRadius > maxRadius || maxRadius > 64 + || minVerticalRadius < 1 || minVerticalRadius > maxVerticalRadius + || maxVerticalRadius > 64 || maxLobes < 1 || maxLobes > 16 + || minSolidCover < 0 || minSolidCover > 64 + || minSolidShell < 0 || minSolidShell > 64) { + throw new IllegalStateException("Invalid fluid deposit placement values for " + dimension); + } + if (enabled && hostFamilies.isEmpty() && hostBlocks.isEmpty() && hostTags.isEmpty()) { + throw new IllegalStateException("Enabled fluid deposit dimension has no hosts: " + dimension); + } + return new FluidDepositDimensionDefinition(this); + } + } + } + + public static final class GeomeDefinition implements JsonDefinition { + private final ResourceLocation id; + private final double baseWeight; + private final Map familyWeights; + + private GeomeDefinition(Builder builder) { + id = builder.id; + baseWeight = builder.baseWeight; + familyWeights = Collections.unmodifiableMap(new LinkedHashMap<>(builder.familyWeights)); + } + + public static Builder builder(ResourceLocation id) { return new Builder(id); } + public ResourceLocation id() { return id; } + public double baseWeight() { return baseWeight; } + public Map familyWeights() { return familyWeights; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("base", baseWeight); + for (GeologyFamily family : GeologyFamily.values()) { + json.addProperty(family.configName(), familyWeights.getOrDefault(family, 1.0D)); + } + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private double baseWeight = 1.0D; + private final Map familyWeights = new LinkedHashMap<>(); + private Builder(ResourceLocation id) { this.id = Objects.requireNonNull(id, "id"); } + public Builder baseWeight(double value) { baseWeight = value; return this; } + public Builder familyWeight(GeologyFamily family, double value) { familyWeights.put(family, value); return this; } + public GeomeDefinition build() { + if (!Double.isFinite(baseWeight) || baseWeight < 0.0D) { + throw new IllegalStateException("Invalid geome base weight for " + id); + } + return new GeomeDefinition(this); + } + } + } + + public static final class BiomeRule implements JsonDefinition { + private final ResourceLocation biome; + private final Map geomeWeights; + public BiomeRule(ResourceLocation biome, Map geomeWeights) { + this.biome = Objects.requireNonNull(biome, "biome"); + this.geomeWeights = immutableMap(geomeWeights); + } + public ResourceLocation biome() { return biome; } + public Map geomeWeights() { return geomeWeights; } + @Override public JsonObject toJson() { return weights(geomeWeights); } + } + + public static final class TerrainDimensionDefinition implements JsonDefinition { + private final ResourceLocation dimension; + private final boolean enabled; + private final Set biomeIds; + private final Set biomeNamespaces; + private final Set hostBlocks; + private final Set hostTags; + + private TerrainDimensionDefinition(Builder builder) { + dimension = builder.dimension; + enabled = builder.enabled; + biomeIds = immutableSet(builder.biomeIds); + biomeNamespaces = Collections.unmodifiableSet(new LinkedHashSet<>(builder.biomeNamespaces)); + hostBlocks = immutableSet(builder.hostBlocks); + hostTags = immutableSet(builder.hostTags); + } + + public static Builder builder(ResourceLocation id) { return new Builder(id); } + public ResourceLocation dimension() { return dimension; } + public boolean enabled() { return enabled; } + public Set biomeIds() { return biomeIds; } + public Set biomeNamespaces() { return biomeNamespaces; } + public Set hostBlocks() { return hostBlocks; } + public Set hostTags() { return hostTags; } + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("enabled", enabled); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biome_ids", ids(biomeIds)); + JsonArray namespaces = new JsonArray(); + for (String namespace : biomeNamespaces) { zone.moddev.mc.orespawn.util.JsonCopies.add(namespaces, namespace); } + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biome_namespaces", namespaces); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_blocks", ids(hostBlocks)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "host_tags", ids(hostTags)); + return json; + } + + public static final class Builder { + private final ResourceLocation dimension; + private boolean enabled = true; + private final Set biomeIds = new LinkedHashSet<>(); + private final Set biomeNamespaces = new LinkedHashSet<>(); + private final Set hostBlocks = new LinkedHashSet<>(); + private final Set hostTags = new LinkedHashSet<>(); + private Builder(ResourceLocation id) { dimension = Objects.requireNonNull(id, "dimension"); } + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder biome(ResourceLocation value) { biomeIds.add(value); return this; } + public Builder biomeNamespace(String value) { biomeNamespaces.add(requireModId(value)); return this; } + public Builder hostBlock(ResourceLocation value) { hostBlocks.add(value); return this; } + public Builder hostTag(ResourceLocation value) { hostTags.add(value); return this; } + public TerrainDimensionDefinition build() { + if (enabled && hostBlocks.isEmpty() && hostTags.isEmpty()) { + throw new IllegalStateException("Enabled terrain dimension has no replacement hosts: " + dimension); + } + return new TerrainDimensionDefinition(this); + } + } + } + + public static final class FormationDefinition implements JsonDefinition { + private final GeologyAlgorithm algorithm; + private final FormationPreset horizontal; + private final FormationPreset thickness; + private final FormationPreset waviness; + private final FormationPreset edge; + private final FormationPreset continuity; + private final JsonObject custom; + + public FormationDefinition(GeologyAlgorithm algorithm, FormationPreset horizontal, + FormationPreset thickness, FormationPreset waviness, FormationPreset edge, + FormationPreset continuity, JsonObject custom) { + this.algorithm = Objects.requireNonNull(algorithm); + this.horizontal = Objects.requireNonNull(horizontal); + this.thickness = Objects.requireNonNull(thickness); + this.waviness = Objects.requireNonNull(waviness); + this.edge = Objects.requireNonNull(edge); + this.continuity = Objects.requireNonNull(continuity); + this.custom = custom == null ? new JsonObject() : JsonCopies.copy(custom); + } + + public static Builder builder() { return new Builder(); } + + public GeologyAlgorithm algorithm() { return algorithm; } + public FormationPreset horizontalSize() { return horizontal; } + public FormationPreset verticalThickness() { return thickness; } + public FormationPreset waviness() { return waviness; } + public FormationPreset edgeIrregularity() { return edge; } + public FormationPreset continuity() { return continuity; } + public JsonObject customValues() { return JsonCopies.copy(custom); } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("algorithm", algorithm.configName()); + json.addProperty("horizontal_size", horizontal.configName()); + json.addProperty("vertical_thickness", thickness.configName()); + json.addProperty("waviness", waviness.configName()); + json.addProperty("edge_irregularity", edge.configName()); + json.addProperty("formation_continuity", continuity.configName()); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "custom", JsonCopies.copy(custom)); + return json; + } + + public static final class Builder { + private GeologyAlgorithm algorithm = GeologyAlgorithm.STABLE_LAYERS; + private FormationPreset horizontal = FormationPreset.AVERAGE; + private FormationPreset thickness = FormationPreset.AVERAGE; + private FormationPreset waviness = FormationPreset.AVERAGE; + private FormationPreset edge = FormationPreset.AVERAGE; + private FormationPreset continuity = FormationPreset.AVERAGE; + private final JsonObject custom = new JsonObject(); + + private Builder() { } + public Builder algorithm(GeologyAlgorithm value) { algorithm = Objects.requireNonNull(value); return this; } + public Builder horizontalSize(FormationPreset value) { horizontal = Objects.requireNonNull(value); return this; } + public Builder verticalThickness(FormationPreset value) { thickness = Objects.requireNonNull(value); return this; } + public Builder waviness(FormationPreset value) { waviness = Objects.requireNonNull(value); return this; } + public Builder edgeIrregularity(FormationPreset value) { edge = Objects.requireNonNull(value); return this; } + public Builder continuity(FormationPreset value) { continuity = Objects.requireNonNull(value); return this; } + public Builder customValue(String key, double value) { + if (key == null || key.trim().isEmpty() || !Double.isFinite(value)) { + throw new IllegalArgumentException("Invalid custom formation value"); + } + custom.addProperty(key, value); + return this; + } + public Builder customValues(JsonObject values) { + custom.entrySet().clear(); + if (values != null) { + values.entrySet().forEach(entry -> custom.add(entry.getKey(), JsonCopies.copy(entry.getValue()))); + } + return this; + } + public FormationDefinition build() { + return new FormationDefinition(algorithm, horizontal, thickness, waviness, edge, continuity, custom); + } + } + } + + /** @deprecated Use {@link FluidDepositDefinition}. */ + @Deprecated + public static final class OilDefinition implements JsonDefinition { + private final int minY; + private final int maxY; + private final double frequency; + private final int minRadius; + private final int maxRadius; + private final int minVerticalRadius; + private final int maxVerticalRadius; + private final int maxLobes; + private final int minSolidCover; + + public OilDefinition(int minY, int maxY, double frequency, int minRadius, int maxRadius, + int minVerticalRadius, int maxVerticalRadius, int maxLobes, int minSolidCover) { + requireRange(minY, maxY, "oil Y range"); + if (!Double.isFinite(frequency) || frequency < 0.0D || frequency > 64.0D + || minRadius < 1 || minRadius > maxRadius + || minVerticalRadius < 1 || minVerticalRadius > maxVerticalRadius + || maxLobes < 1 || minSolidCover < 0) { + throw new IllegalArgumentException("Invalid oil placement values"); + } + this.minY = minY; + this.maxY = maxY; + this.frequency = frequency; + this.minRadius = minRadius; + this.maxRadius = maxRadius; + this.minVerticalRadius = minVerticalRadius; + this.maxVerticalRadius = maxVerticalRadius; + this.maxLobes = maxLobes; + this.minSolidCover = minSolidCover; + } + + public static Builder builder() { return new Builder(); } + + public int minY() { return minY; } + public int maxY() { return maxY; } + public double frequency() { return frequency; } + public int minRadius() { return minRadius; } + public int maxRadius() { return maxRadius; } + public int minVerticalRadius() { return minVerticalRadius; } + public int maxVerticalRadius() { return maxVerticalRadius; } + public int maxLobes() { return maxLobes; } + public int minSolidCover() { return minSolidCover; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("min_y", minY); + json.addProperty("max_y", maxY); + json.addProperty("frequency", frequency); + json.addProperty("min_radius", minRadius); + json.addProperty("max_radius", maxRadius); + json.addProperty("min_vertical_radius", minVerticalRadius); + json.addProperty("max_vertical_radius", maxVerticalRadius); + json.addProperty("max_lobes", maxLobes); + json.addProperty("min_solid_cover", minSolidCover); + return json; + } + + public static final class Builder { + private int minY = 0; + private int maxY = 40; + private double frequency = 0.035D; + private int minRadius = 8; + private int maxRadius = 16; + private int minVerticalRadius = 3; + private int maxVerticalRadius = 7; + private int maxLobes = 4; + private int minSolidCover = 2; + + private Builder() { } + public Builder yRange(int min, int max) { minY = min; maxY = max; return this; } + public Builder attempts(double value) { frequency = value; return this; } + public Builder radius(int min, int max) { minRadius = min; maxRadius = max; return this; } + public Builder verticalRadius(int min, int max) { minVerticalRadius = min; maxVerticalRadius = max; return this; } + public Builder maxLobes(int value) { maxLobes = value; return this; } + public Builder minSolidCover(int value) { minSolidCover = value; return this; } + public OilDefinition build() { + return new OilDefinition(minY, maxY, frequency, minRadius, maxRadius, + minVerticalRadius, maxVerticalRadius, maxLobes, minSolidCover); + } + } + } + + public static final class GeologyTemplate implements JsonDefinition { + private final ResourceLocation id; + private final String nameKey; + private final String descriptionKey; + private final Set requiredMods; + private final boolean autoSelect; + private final int autoSelectPriority; + private final JsonObject profile; + + private GeologyTemplate(Builder builder) { + id = builder.id; + nameKey = builder.nameKey; + descriptionKey = builder.descriptionKey; + requiredMods = Collections.unmodifiableSet(new LinkedHashSet<>(builder.requiredMods)); + autoSelect = builder.autoSelect; + autoSelectPriority = builder.autoSelectPriority; + profile = JsonCopies.copy(builder.profile); + } + + public static Builder builder(ResourceLocation id) { return new Builder(id); } + public ResourceLocation id() { return id; } + public String nameKey() { return nameKey; } + public String descriptionKey() { return descriptionKey; } + public Set requiredMods() { return requiredMods; } + public boolean autoSelect() { return autoSelect; } + public int autoSelectPriority() { return autoSelectPriority; } + public JsonObject profile() { return JsonCopies.copy(profile); } + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("name_key", nameKey); + json.addProperty("description_key", descriptionKey); + JsonArray mods = new JsonArray(); + for (String mod : requiredMods) { zone.moddev.mc.orespawn.util.JsonCopies.add(mods, mod); } + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "required_mods", mods); + json.addProperty("auto_select", autoSelect); + json.addProperty("auto_select_priority", autoSelectPriority); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "profile", JsonCopies.copy(profile)); + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private String nameKey; + private String descriptionKey; + private final Set requiredMods = new LinkedHashSet<>(); + private boolean autoSelect; + private int autoSelectPriority; + private final JsonObject profile = new JsonObject(); + private Builder(ResourceLocation id) { + this.id = Objects.requireNonNull(id, "id"); + nameKey = "orespawn.template." + id.getResourceDomain() + "." + id.getResourcePath(); + descriptionKey = nameKey + ".description"; + } + public Builder translationKeys(String name, String description) { nameKey = name; descriptionKey = description; return this; } + public Builder requiresMod(String value) { requiredMods.add(requireModId(value)); return this; } + public Builder autoSelect(boolean value) { autoSelect = value; return this; } + public Builder autoSelectPriority(int value) { autoSelectPriority = value; return this; } + public Builder profile(JsonObject value) { profile.entrySet().clear(); value.entrySet().forEach(e -> profile.add(e.getKey(), JsonCopies.copy(e.getValue()))); return this; } + public Builder formations(FormationDefinition value) { profile.add("formations", value.toJson()); return this; } + public Builder fluidDeposit(FluidDepositDefinition value) { + JsonObject deposits = profile.has("fluid_deposits") && profile.get("fluid_deposits").isJsonObject() + ? profile.getAsJsonObject("fluid_deposits") : new JsonObject(); + deposits.add(value.id().toString(), value.toJson()); + profile.add("fluid_deposits", deposits); + profile.addProperty("place_fluid_deposits", true); + return this; + } + /** @deprecated Use {@code fluidDeposit(FluidDepositDefinition)}. */ + @Deprecated + public Builder oil(OilDefinition value) { + profile.add("oil", value.toJson()); + profile.addProperty("place_crude_oil", true); + return this; + } + public Builder geologyMode(String value) { profile.addProperty("geology_mode", value); return this; } + public Builder manageVanillaOres(boolean value) { profile.addProperty("manage_vanilla_ores", value); return this; } + public GeologyTemplate build() { + if (profile.entrySet().isEmpty()) { throw new IllegalStateException("Template profile is empty: " + id); } + return new GeologyTemplate(this); + } + } + } + + private interface JsonDefinition { + JsonObject toJson(); + } + + private static JsonObject object(Map values) { + JsonObject json = new JsonObject(); + for (Map.Entry entry : values.entrySet()) { + zone.moddev.mc.orespawn.util.JsonCopies.add(json, entry.getKey().toString(), entry.getValue().toJson()); + } + return json; + } + + private static JsonObject weights(Map values) { + JsonObject json = new JsonObject(); + for (Map.Entry entry : values.entrySet()) { + json.addProperty(entry.getKey().toString(), entry.getValue()); + } + return json; + } + + private static JsonArray ids(Collection values) { + JsonArray json = new JsonArray(); + for (ResourceLocation value : values) { zone.moddev.mc.orespawn.util.JsonCopies.add(json, value.toString()); } + return json; + } + + /** + * A provider-owned set of biomes which can augment or replace an existing + * dimension biome source without depending on a particular biome framework. + */ + public static final class BiomePaletteDefinition implements JsonDefinition { + private final ResourceLocation id; + private final ResourceLocation dimension; + private final boolean enabled; + private final BiomePlacementMode mode; + private final BiomeReplacementScope scope; + private final BiomeRegionSize regionSize; + private final double coverage; + private final double fallbackWeight; + private final Set includedNamespaces; + private final Set excludedNamespaces; + private final Map biomes; + + private BiomePaletteDefinition(Builder builder) { + id = builder.id; + dimension = builder.dimension; + enabled = builder.enabled; + mode = builder.mode; + scope = builder.scope; + regionSize = builder.regionSize; + coverage = builder.coverage; + fallbackWeight = builder.fallbackWeight; + includedNamespaces = Collections.unmodifiableSet( + new LinkedHashSet<>(builder.includedNamespaces)); + excludedNamespaces = Collections.unmodifiableSet( + new LinkedHashSet<>(builder.excludedNamespaces)); + biomes = Collections.unmodifiableMap(new LinkedHashMap<>(builder.biomes)); + } + + public static Builder builder(ResourceLocation id, ResourceLocation dimension) { + return new Builder(id, dimension); + } + + public ResourceLocation id() { return id; } + public ResourceLocation dimension() { return dimension; } + public boolean enabled() { return enabled; } + public BiomePlacementMode mode() { return mode; } + public BiomeReplacementScope scope() { return scope; } + public BiomeRegionSize regionSize() { return regionSize; } + public double coverage() { return coverage; } + public double fallbackWeight() { return fallbackWeight; } + public Set includedNamespaces() { return includedNamespaces; } + public Set excludedNamespaces() { return excludedNamespaces; } + public Map biomes() { return biomes; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("dimension", dimension.toString()); + json.addProperty("enabled", enabled); + json.addProperty("mode", mode.configName()); + json.addProperty("scope", scope.configName()); + json.addProperty("region_size", regionSize.configName()); + json.addProperty("coverage", coverage); + json.addProperty("fallback_weight", fallbackWeight); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "include_namespaces", strings(includedNamespaces)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "exclude_namespaces", strings(excludedNamespaces)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "biomes", object(biomes)); + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private final ResourceLocation dimension; + private boolean enabled = true; + private BiomePlacementMode mode = BiomePlacementMode.AUGMENT; + private BiomeReplacementScope scope = BiomeReplacementScope.MINECRAFT_ONLY; + private BiomeRegionSize regionSize = BiomeRegionSize.AVERAGE; + private double coverage = 1.0D; + private double fallbackWeight = 1.0D; + private final Set includedNamespaces = new LinkedHashSet<>(); + private final Set excludedNamespaces = new LinkedHashSet<>(); + private final Map biomes = + new LinkedHashMap<>(); + + private Builder(ResourceLocation id, ResourceLocation dimension) { + this.id = Objects.requireNonNull(id, "id"); + this.dimension = Objects.requireNonNull(dimension, "dimension"); + } + + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder mode(BiomePlacementMode value) { mode = Objects.requireNonNull(value); return this; } + public Builder scope(BiomeReplacementScope value) { scope = Objects.requireNonNull(value); return this; } + public Builder regionSize(BiomeRegionSize value) { regionSize = Objects.requireNonNull(value); return this; } + public Builder coverage(double value) { coverage = value; return this; } + public Builder fallbackWeight(double value) { fallbackWeight = value; return this; } + public Builder includeNamespace(String value) { includedNamespaces.add(requireModId(value)); return this; } + public Builder excludeNamespace(String value) { excludedNamespaces.add(requireModId(value)); return this; } + public Builder biome(BiomePlacementDefinition value) { + putUnique(biomes, value.biome(), value, "biome placement"); + return this; + } + public Builder biome(ResourceLocation biome, + Consumer edit) { + BiomePlacementDefinition.Builder builder = BiomePlacementDefinition.builder(biome); + edit.accept(builder); + return biome(builder.build()); + } + + public BiomePaletteDefinition build() { + if (!Double.isFinite(coverage) || coverage < 0.0D || coverage > 1.0D + || !Double.isFinite(fallbackWeight) || fallbackWeight < 0.0D) { + throw new IllegalStateException("Invalid biome palette coverage or fallback weight: " + id); + } + if (enabled && biomes.isEmpty()) { + throw new IllegalStateException("Enabled biome palette has no biomes: " + id); + } + if (scope == BiomeReplacementScope.SELECTED_NAMESPACES + && includedNamespaces.isEmpty()) { + throw new IllegalStateException("Selected-namespace biome palette has no namespaces: " + id); + } + return new BiomePaletteDefinition(this); + } + } + } + + /** Placement and optional surface settings for one registered biome. */ + public static final class BiomePlacementDefinition implements JsonDefinition { + private final ResourceLocation biome; + private final boolean enabled; + private final double weight; + private final Set similarBiomes; + private final Set requiredSimilarBiomes; + private final double minTemperature; + private final double maxTemperature; + private final double minDownfall; + private final double maxDownfall; + private final BiomeSurfaceDefinition surface; + + private BiomePlacementDefinition(Builder builder) { + biome = builder.biome; + enabled = builder.enabled; + weight = builder.weight; + similarBiomes = immutableSet(builder.similarBiomes); + requiredSimilarBiomes = immutableSet(builder.requiredSimilarBiomes); + minTemperature = builder.minTemperature; + maxTemperature = builder.maxTemperature; + minDownfall = builder.minDownfall; + maxDownfall = builder.maxDownfall; + surface = builder.surface; + } + + public static Builder builder(ResourceLocation biome) { return new Builder(biome); } + public ResourceLocation biome() { return biome; } + public boolean enabled() { return enabled; } + public double weight() { return weight; } + public Set similarBiomes() { return similarBiomes; } + public Set requiredSimilarBiomes() { return requiredSimilarBiomes; } + public BiomeSurfaceDefinition surface() { return surface; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("enabled", enabled); + json.addProperty("weight", weight); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "similar_biomes", ids(similarBiomes)); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, "required_similar_biomes", ids(requiredSimilarBiomes)); + json.addProperty("min_temperature", minTemperature); + json.addProperty("max_temperature", maxTemperature); + json.addProperty("min_downfall", minDownfall); + json.addProperty("max_downfall", maxDownfall); + if (surface != null) zone.moddev.mc.orespawn.util.JsonCopies.add(json, "surface", surface.toJson()); + return json; + } + + public static final class Builder { + private final ResourceLocation biome; + private boolean enabled = true; + private double weight = 1.0D; + private final Set similarBiomes = new LinkedHashSet<>(); + private final Set requiredSimilarBiomes = new LinkedHashSet<>(); + private double minTemperature = -2.0D; + private double maxTemperature = 2.0D; + private double minDownfall = 0.0D; + private double maxDownfall = 1.0D; + private BiomeSurfaceDefinition surface; + + private Builder(ResourceLocation biome) { + this.biome = Objects.requireNonNull(biome, "biome"); + } + + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder weight(double value) { weight = value; return this; } + public Builder similarBiome(ResourceLocation value) { similarBiomes.add(value); return this; } + public Builder requiredSimilarBiome(ResourceLocation value) { + requiredSimilarBiomes.add(value); + return this; + } + public Builder temperature(double min, double max) { + minTemperature = min; + maxTemperature = max; + return this; + } + public Builder downfall(double min, double max) { + minDownfall = min; + maxDownfall = max; + return this; + } + public Builder surface(BiomeSurfaceDefinition value) { surface = value; return this; } + + public BiomePlacementDefinition build() { + if (!Double.isFinite(weight) || weight < 0.0D + || minTemperature > maxTemperature || minDownfall > maxDownfall + || minDownfall < 0.0D || maxDownfall > 1.0D) { + throw new IllegalStateException("Invalid biome placement: " + biome); + } + return new BiomePlacementDefinition(this); + } + } + } + + /** Surface block choices applied only to columns using this biome. */ + public static final class BiomeSurfaceDefinition implements JsonDefinition { + private final ResourceLocation topBlock; + private final ResourceLocation fillerBlock; + private final ResourceLocation underwaterBlock; + private final ResourceLocation ceilingBlock; + private final int fillerDepth; + + private BiomeSurfaceDefinition(Builder builder) { + topBlock = builder.topBlock; + fillerBlock = builder.fillerBlock; + underwaterBlock = builder.underwaterBlock; + ceilingBlock = builder.ceilingBlock; + fillerDepth = builder.fillerDepth; + } + + public static Builder builder() { return new Builder(); } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + if (topBlock != null) json.addProperty("top_block", topBlock.toString()); + if (fillerBlock != null) json.addProperty("filler_block", fillerBlock.toString()); + if (underwaterBlock != null) json.addProperty("underwater_block", underwaterBlock.toString()); + if (ceilingBlock != null) json.addProperty("ceiling_block", ceilingBlock.toString()); + json.addProperty("filler_depth", fillerDepth); + return json; + } + + public static final class Builder { + private ResourceLocation topBlock; + private ResourceLocation fillerBlock; + private ResourceLocation underwaterBlock; + private ResourceLocation ceilingBlock; + private int fillerDepth = 3; + + private Builder() { } + public Builder topBlock(ResourceLocation value) { topBlock = value; return this; } + public Builder fillerBlock(ResourceLocation value) { fillerBlock = value; return this; } + public Builder underwaterBlock(ResourceLocation value) { underwaterBlock = value; return this; } + public Builder ceilingBlock(ResourceLocation value) { ceilingBlock = value; return this; } + public Builder fillerDepth(int value) { fillerDepth = value; return this; } + public BiomeSurfaceDefinition build() { + if (fillerDepth < 0 || fillerDepth > 16) { + throw new IllegalStateException("Biome surface filler depth must be within 0..16"); + } + return new BiomeSurfaceDefinition(this); + } + } + } + + /** Dimension-wide aquifer and weather materials. */ + public static final class DimensionMaterialsDefinition implements JsonDefinition { + private final ResourceLocation id; + private final ResourceLocation dimension; + private final boolean enabled; + private final ResourceLocation defaultFluid; + private final ResourceLocation deepAquiferFluid; + private final int deepAquiferMaxY; + private final ResourceLocation snowBlock; + private final ResourceLocation iceBlock; + + private DimensionMaterialsDefinition(Builder builder) { + id = builder.id; + dimension = builder.dimension; + enabled = builder.enabled; + defaultFluid = builder.defaultFluid; + deepAquiferFluid = builder.deepAquiferFluid; + deepAquiferMaxY = builder.deepAquiferMaxY; + snowBlock = builder.snowBlock; + iceBlock = builder.iceBlock; + } + + public static Builder builder(ResourceLocation id, ResourceLocation dimension) { + return new Builder(id, dimension); + } + public ResourceLocation id() { return id; } + public ResourceLocation dimension() { return dimension; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("dimension", dimension.toString()); + json.addProperty("enabled", enabled); + if (defaultFluid != null) json.addProperty("default_fluid", defaultFluid.toString()); + if (deepAquiferFluid != null) json.addProperty("deep_aquifer_fluid", deepAquiferFluid.toString()); + json.addProperty("deep_aquifer_max_y", deepAquiferMaxY); + if (snowBlock != null) json.addProperty("snow_block", snowBlock.toString()); + if (iceBlock != null) json.addProperty("ice_block", iceBlock.toString()); + return json; + } + + public static final class Builder { + private final ResourceLocation id; + private final ResourceLocation dimension; + private boolean enabled = true; + private ResourceLocation defaultFluid; + private ResourceLocation deepAquiferFluid; + private int deepAquiferMaxY = -54; + private ResourceLocation snowBlock; + private ResourceLocation iceBlock; + + private Builder(ResourceLocation id, ResourceLocation dimension) { + this.id = Objects.requireNonNull(id, "id"); + this.dimension = Objects.requireNonNull(dimension, "dimension"); + } + public Builder enabled(boolean value) { enabled = value; return this; } + public Builder defaultFluid(ResourceLocation value) { defaultFluid = value; return this; } + public Builder deepAquiferFluid(ResourceLocation value, int maxY) { + deepAquiferFluid = value; + deepAquiferMaxY = maxY; + return this; + } + public Builder snowBlock(ResourceLocation value) { snowBlock = value; return this; } + public Builder iceBlock(ResourceLocation value) { iceBlock = value; return this; } + public DimensionMaterialsDefinition build() { + if (enabled && defaultFluid == null && deepAquiferFluid == null + && snowBlock == null && iceBlock == null) { + throw new IllegalStateException("Enabled dimension materials are empty: " + id); + } + return new DimensionMaterialsDefinition(this); + } + } + } + + private static JsonArray strings(Collection values) { + JsonArray json = new JsonArray(); + for (String value : values) { zone.moddev.mc.orespawn.util.JsonCopies.add(json, value); } + return json; + } + + private static JsonArray weightedIds(Collection values, + Map weights, String idKey) { + JsonArray json = new JsonArray(); + for (ResourceLocation value : values) { + Double weight = weights.get(value); + if (weight == null || weight.doubleValue() == 1.0D) { + zone.moddev.mc.orespawn.util.JsonCopies.add(json, value.toString()); + } else { + JsonObject entry = new JsonObject(); + entry.addProperty(idKey, value.toString()); + entry.addProperty("weight", weight); + zone.moddev.mc.orespawn.util.JsonCopies.add(json, entry); + } + } + return json; + } + + private static double replacementWeight(double value) { + if (!Double.isFinite(value) || value < 0.0D || value > 1.0D) { + throw new IllegalArgumentException("Replacement weight must be between 0 and 1: " + value); + } + return value; + } + + private static void putUnique(Map values, K key, V value, String type) { + if (values.putIfAbsent(key, value) != null) { + throw new IllegalArgumentException("Duplicate " + type + ": " + key); + } + } + + private static String requireModId(String value) { + Objects.requireNonNull(value, "modId"); + ResourceLocation probe = new ResourceLocation(value, "provider"); + if (!probe.getResourceDomain().equals(value)) { + throw new IllegalArgumentException("Invalid mod ID: " + value); + } + return value; + } + + private static String nonBlank(String value) { + Objects.requireNonNull(value, "value"); + String normalized = value.trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Value must not be blank"); + } + return normalized; + } + + private static void requireRange(int min, int max, String name) { + if (min < -2048 || max > 2048 || min > max) { + throw new IllegalArgumentException("Invalid " + name + ": " + min + ".." + max); + } + } + + private static Map immutableMap(Map values) { + return Collections.unmodifiableMap(new LinkedHashMap<>(values)); + } + + private static Set immutableSet(Set values) { + return Collections.unmodifiableSet(new LinkedHashSet<>(values)); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/api/package-info.java b/src/main/java/zone/moddev/mc/orespawn/api/package-info.java new file mode 100644 index 00000000..5e6244cd --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/api/package-info.java @@ -0,0 +1,7 @@ +/** + * OreSpawn's supported, versioned integration API. + * + *

Classes outside this package are implementation details and may change + * between releases without an API-version change.

+ */ +package zone.moddev.mc.orespawn.api; diff --git a/src/main/java/zone/moddev/mc/orespawn/client/AdvancedGeologySettingsScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/AdvancedGeologySettingsScreen.java new file mode 100644 index 00000000..b756b6b9 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/AdvancedGeologySettingsScreen.java @@ -0,0 +1,62 @@ +package zone.moddev.mc.orespawn.client; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.TextComponentTranslation; + +/** Less commonly changed numeric controls, kept off the world settings overview. */ +final class AdvancedGeologySettingsScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + + AdvancedGeologySettingsScreen(GuiScreen parent, GeologyEditorSession session) { + super(new TextComponentTranslation("screen.orespawn.advanced")); + this.parent = parent; + this.session = session; + } + + @Override + protected void init() { + OreSpawnScreenLayout.beginHelp(this); + int left = width / 2 - 155; + int top = 54; + int row = 0; + if (session.hasTerrainRules()) { + addButton(OreSpawnScreenLayout.explainedButton(this, font, + left, top + (row++ * 28), 310, 20, + new TextComponentTranslation("button.orespawn.formation_details"), + button -> openNumeric("formations.custom", NumericConfigScreen.FORMATION_FIELDS), + "tooltip.orespawn.advanced.formations")); + addButton(OreSpawnScreenLayout.explainedButton(this, font, + left, top + (row++ * 28), 310, 20, + new TextComponentTranslation("button.orespawn.cyano_details"), + button -> openNumeric("cyano", NumericConfigScreen.CYANO_FIELDS), + "tooltip.orespawn.advanced.cyano")); + } + if (!session.fluidDepositIds().isEmpty()) { + addButton(OreSpawnScreenLayout.explainedButton(this, font, + left, top + (row * 28), 310, 20, + new TextComponentTranslation("button.orespawn.fluid_deposit_details"), + button -> minecraft.displayGuiScreen(new FluidDepositListScreen(this, session)), + "tooltip.orespawn.advanced.fluid_deposits")); + } + addButton(new Button(width / 2 - 75, OreSpawnScreenLayout.footerY(height), 150, 20, + DialogTexts.GUI_DONE, button -> onClose())); + } + + private void openNumeric(String path, NumericConfigScreen.Field[] fields) { + minecraft.displayGuiScreen(new NumericConfigScreen(this, session, path, fields)); + } + + @Override + public void onClose() { + minecraft.displayGuiScreen(parent); + } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 20, 0xFFFFFF); + super.render(mouseX, mouseY, partialTick); + OreSpawnScreenLayout.renderExplanations(this, mouseX, mouseY); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BiomePaletteScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BiomePaletteScreen.java new file mode 100644 index 00000000..3bea7644 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BiomePaletteScreen.java @@ -0,0 +1,76 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.List; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +final class BiomePaletteScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private final String dimension; + private int page; + + BiomePaletteScreen(GuiScreen parent, GeologyEditorSession session, String dimension) { + super(new TextComponentTranslation("screen.orespawn.biome_palette")); + this.parent = parent; + this.session = session; + this.dimension = dimension; + } + + @Override + protected void init() { + int contentWidth = Math.min(390, Math.max(280, width - 24)); + int left = (width - contentWidth) / 2; + int removeWidth = 70; + int listTop = 48; + int controlsY = height - 52; + List ids = session.biomePlacementIds(dimension); + int pageSize = Math.max(1, (controlsY - listTop) / 24); + int pageCount = Math.max(1, (ids.size() + pageSize - 1) / pageSize); + page = Math.max(0, Math.min(page, pageCount - 1)); + int start = page * pageSize; + for (int i = 0; i < pageSize && start + i < ids.size(); i++) { + String id = ids.get(start + i); + int y = listTop + i * 24; + addButton(OreSpawnScreenLayout.button(this, font, left, y, + contentWidth - removeWidth - 5, 20, new TextComponentString(id), + button -> minecraft.displayGuiScreen(new BiomePlacementScreen(this, session, + dimension, id)))); + addButton(new Button(left + contentWidth - removeWidth, y, removeWidth, 20, + new TextComponentTranslation("button.orespawn.remove"), + button -> { session.removeBiomePlacement(dimension, id); rebuildWidgets(); })); + } + Button previous = addButton(new Button(left, controlsY, 45, 20, + new TextComponentString("<"), button -> { page--; rebuildWidgets(); })); + Button next = addButton(new Button(left + 50, controlsY, 45, 20, + new TextComponentString(">"), button -> { page++; rebuildWidgets(); })); + previous.enabled = page > 0; + next.enabled = page + 1 < pageCount; + addButton(OreSpawnScreenLayout.button(this, font, + left + contentWidth - 150, controlsY, 150, 20, + new TextComponentTranslation("button.orespawn.add_biome"), + button -> minecraft.displayGuiScreen(new BiomePickerScreen(this, session, id -> { + session.addBiomePlacement(dimension, id); + minecraft.displayGuiScreen(new BiomePlacementScreen(this, session, dimension, id)); + })))); + addButton(new Button(width / 2 - 75, height - 28, 150, 20, + DialogTexts.GUI_DONE, button -> onClose())); + } + + private void rebuildWidgets() { + buttons.clear(); children.clear(); + init(); + } + + @Override public void onClose() { minecraft.displayGuiScreen(parent); } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 12, 0xFFFFFF); + drawCenteredString(font, new TextComponentString(dimension), width / 2, 28, 0xCCCCCC); + super.render(mouseX, mouseY, partialTick); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BiomePickerScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BiomePickerScreen.java new file mode 100644 index 00000000..be4f9316 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BiomePickerScreen.java @@ -0,0 +1,83 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +/** Registry-backed biome chooser. */ +final class BiomePickerScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private final Consumer select; + private String searchText = ""; + private int page; + private TextFieldWidget search; + + BiomePickerScreen(GuiScreen parent, GeologyEditorSession session, Consumer select) { + super(new TextComponentTranslation("screen.orespawn.choose_biome")); + this.parent = parent; + this.session = session; + this.select = select; + } + + @Override + protected void init() { + int contentWidth = Math.min(390, Math.max(280, width - 24)); + int left = (width - contentWidth) / 2; + search = addButton(new TextFieldWidget(font, left, 36, contentWidth - 75, 20, + new TextComponentTranslation("option.orespawn.search"))); + search.setValue(searchText); + addButton(new Button(left + contentWidth - 70, 36, 70, 20, + new TextComponentTranslation("button.orespawn.search"), button -> { + searchText = search.getValue(); + page = 0; + rebuildWidgets(); + })); + List ids = filtered(); + int listTop = 64; + int controlsY = height - 52; + int pageSize = Math.max(1, (controlsY - listTop) / 24); + int pageCount = Math.max(1, (ids.size() + pageSize - 1) / pageSize); + page = Math.max(0, Math.min(page, pageCount - 1)); + int start = page * pageSize; + for (int i = 0; i < pageSize && start + i < ids.size(); i++) { + String id = ids.get(start + i); + addButton(OreSpawnScreenLayout.button(this, font, left, + listTop + i * 24, contentWidth, 20, new TextComponentString(id), + button -> select.accept(id))); + } + Button previous = addButton(new Button(left, controlsY, 45, 20, + new TextComponentString("<"), button -> { page--; rebuildWidgets(); })); + Button next = addButton(new Button(left + 50, controlsY, 45, 20, + new TextComponentString(">"), button -> { page++; rebuildWidgets(); })); + previous.enabled = page > 0; + next.enabled = page + 1 < pageCount; + addButton(new Button(width / 2 - 75, height - 28, 150, 20, + DialogTexts.GUI_CANCEL, button -> onClose())); + } + + private List filtered() { + if (searchText.trim().isEmpty()) return session.installedBiomeIds(); + String query = searchText.trim().toLowerCase(Locale.ROOT); + List result = new ArrayList<>(); + for (String id : session.installedBiomeIds()) { + if (id.toLowerCase(Locale.ROOT).contains(query)) result.add(id); + } + return result; + } + + private void rebuildWidgets() { buttons.clear(); children.clear(); init(); } + @Override public void onClose() { minecraft.displayGuiScreen(parent); } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 14, 0xFFFFFF); + super.render(mouseX, mouseY, partialTick); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BiomePlacementScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BiomePlacementScreen.java new file mode 100644 index 00000000..a2e085a3 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BiomePlacementScreen.java @@ -0,0 +1,210 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.Arrays; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +final class BiomePlacementScreen extends OreSpawnScreen { + private enum Tab { PLACEMENT, CLIMATE, SURFACE } + + private final GuiScreen parent; + private final GeologyEditorSession session; + private final String dimension; + private final String biomeId; + private Tab tab = Tab.PLACEMENT; + private TextFieldWidget weight; + private TextFieldWidget minTemperature; + private TextFieldWidget maxTemperature; + private TextFieldWidget minDownfall; + private TextFieldWidget maxDownfall; + private TextFieldWidget fillerDepth; + + BiomePlacementScreen(GuiScreen parent, GeologyEditorSession session, + String dimension, String biomeId) { + super(new TextComponentTranslation("screen.orespawn.biome_placement")); + this.parent = parent; + this.session = session; + this.dimension = dimension; + this.biomeId = biomeId; + } + + @Override + protected void init() { + OreSpawnScreenLayout.beginHelp(this); + int contentWidth = Math.min(390, Math.max(290, width - 24)); + int left = (width - contentWidth) / 2; + int half = (contentWidth - 5) / 2; + int tabWidth = (contentWidth - 10) / 3; + for (int i = 0; i < Tab.values().length; i++) { + Tab value = Tab.values()[i]; + Button button = addButton(new Button(left + i * (tabWidth + 5), 40, + tabWidth, 20, new TextComponentTranslation("tab.orespawn.biome_" + + value.name().toLowerCase(java.util.Locale.ROOT)), + selected -> { saveFields(); tab = value; rebuildWidgets(); })); + button.enabled = value != tab; + } + JsonObject placement = session.biomePlacement(dimension, biomeId); + OreSpawnScreenLayout.explain(this, addButton(CycleButton.onOffBuilder( + bool(placement, "enabled", true)) + .create(left, 64, contentWidth, 20, + new TextComponentTranslation("option.orespawn.enabled"), + (button, value) -> placement.addProperty("enabled", value))), + "tooltip.orespawn.enabled"); + if (tab == Tab.PLACEMENT) initPlacement(left, half, placement); + else if (tab == Tab.CLIMATE) initClimate(left, half, placement); + else initSurface(left, contentWidth, placement); + addButton(new Button(left, OreSpawnScreenLayout.footerY(height), + half, 20, DialogTexts.GUI_DONE, button -> { saveFields(); onClose(); })); + addButton(new Button(left + half + 5, OreSpawnScreenLayout.footerY(height), + half, 20, new TextComponentTranslation("button.orespawn.remove"), button -> { + session.removeBiomePlacement(dimension, biomeId); + onClose(); + })); + } + + private void initPlacement(int left, int half, JsonObject placement) { + int y = 90; + weight = field(left + half + 5, y, half, decimal(placement, "weight", 1.0D)); + OreSpawnScreenLayout.explain(this, weight, "tooltip.orespawn.weight"); + label(left, y + 6, half, "option.orespawn.weight"); + y += 28; + addButton(OreSpawnScreenLayout.explainedButton(this, font, left, y, half, 20, + new TextComponentTranslation("button.orespawn.similar_biomes", + array(placement, "similar_biomes").size()), + button -> { saveFields(); minecraft.displayGuiScreen(new BiomeReferenceScreen( + this, session, placement, "similar_biomes")); }, + "tooltip.orespawn.biome.similar_biomes")); + addButton(OreSpawnScreenLayout.explainedButton(this, font, left + half + 5, y, + half, 20, new TextComponentTranslation("button.orespawn.required_biomes", + array(placement, "required_similar_biomes").size()), + button -> { saveFields(); minecraft.displayGuiScreen(new BiomeReferenceScreen( + this, session, placement, "required_similar_biomes")); }, + "tooltip.orespawn.biome.required_similar_biomes")); + } + + private void initClimate(int left, int half, JsonObject placement) { + int y = 90; + minTemperature = field(left + half + 5, y, half, + decimal(placement, "min_temperature", -2.0D)); + OreSpawnScreenLayout.explain(this, minTemperature, "tooltip.orespawn.biome.min_temperature"); + label(left, y + 6, half, "option.orespawn.min_temperature"); + y += 24; + maxTemperature = field(left + half + 5, y, half, + decimal(placement, "max_temperature", 2.0D)); + OreSpawnScreenLayout.explain(this, maxTemperature, "tooltip.orespawn.biome.max_temperature"); + label(left, y + 6, half, "option.orespawn.max_temperature"); + y += 24; + minDownfall = field(left + half + 5, y, half, + decimal(placement, "min_downfall", 0.0D)); + OreSpawnScreenLayout.explain(this, minDownfall, "tooltip.orespawn.biome.min_downfall"); + label(left, y + 6, half, "option.orespawn.min_downfall"); + y += 24; + maxDownfall = field(left + half + 5, y, half, + decimal(placement, "max_downfall", 1.0D)); + OreSpawnScreenLayout.explain(this, maxDownfall, "tooltip.orespawn.biome.max_downfall"); + label(left, y + 6, half, "option.orespawn.max_downfall"); + } + + private void initSurface(int left, int width, JsonObject placement) { + JsonObject surface = object(placement, "surface"); + int y = 90; + for (String key : Arrays.asList("top_block", "filler_block", + "underwater_block", "ceiling_block")) { + String current = string(surface, key, ""); + Button material = addButton(OreSpawnScreenLayout.explainedButton(this, font, left, y, + width - 65, 20, materialLabel(key, current), button -> { + saveFields(); + minecraft.displayGuiScreen(new MaterialBlockPickerScreen(this, session, + false, id -> surface.addProperty(key, id))); + }, "tooltip.orespawn.biome." + key)); + Button clear = addButton(new Button(left + width - 60, y, 60, 20, + new TextComponentTranslation("button.orespawn.clear"), + button -> { surface.remove(key); rebuildWidgets(); })); + clear.enabled = !current.isEmpty(); + y += 24; + } + fillerDepth = field(left + width / 2, y, width / 2, + integer(surface, "filler_depth", 3)); + OreSpawnScreenLayout.explain(this, fillerDepth, "tooltip.orespawn.biome.filler_depth"); + label(left, y + 6, width / 2 - 5, "option.orespawn.filler_depth"); + } + + private ITextComponent materialLabel(String key, String value) { + return new TextComponentTranslation("option.orespawn." + key, + value.isEmpty() ? new TextComponentTranslation("value.orespawn.not_set") + : new TextComponentString(value)); + } + + private TextFieldWidget field(int x, int y, int width, double value) { + TextFieldWidget result = addButton(new TextFieldWidget(font, x, y, width, 20, "")); + result.setValue(Double.toString(value)); + return result; + } + + private void label(int x, int y, int labelWidth, String key) { + addButton(new Button(x, y - 6, Math.max(1, labelWidth), 20, + new TextComponentTranslation(key), button -> { })).enabled = false; + } + + private void saveFields() { + JsonObject placement = session.biomePlacement(dimension, biomeId); + if (weight != null) putDouble(placement, "weight", weight, 0.0D, 1000.0D); + if (minTemperature != null) putDouble(placement, "min_temperature", minTemperature, -2.0D, 2.0D); + if (maxTemperature != null) putDouble(placement, "max_temperature", maxTemperature, -2.0D, 2.0D); + if (minDownfall != null) putDouble(placement, "min_downfall", minDownfall, 0.0D, 1.0D); + if (maxDownfall != null) putDouble(placement, "max_downfall", maxDownfall, 0.0D, 1.0D); + if (fillerDepth != null) { + try { + int value = Integer.parseInt(fillerDepth.getValue().trim()); + object(placement, "surface").addProperty("filler_depth", + Math.max(0, Math.min(16, value))); + } catch (NumberFormatException ignored) { } + } + } + + private static void putDouble(JsonObject root, String key, TextFieldWidget field, + double min, double max) { + try { + double value = Double.parseDouble(field.getValue().trim()); + if (Double.isFinite(value)) root.addProperty(key, Math.max(min, Math.min(max, value))); + } catch (NumberFormatException ignored) { } + } + + private void rebuildWidgets() { buttons.clear(); children.clear(); init(); } + @Override public void onClose() { minecraft.displayGuiScreen(parent); } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 10, 0xFFFFFF); + drawCenteredString(font, new TextComponentString(biomeId), width / 2, 28, 0xCCCCCC); + super.render(mouseX, mouseY, partialTick); + OreSpawnScreenLayout.renderExplanations(this, mouseX, mouseY); + } + + private static JsonObject object(JsonObject root, String key) { + if (!root.has(key) || !root.get(key).isJsonObject()) root.add(key, new JsonObject()); + return root.getAsJsonObject(key); + } + private static JsonArray array(JsonObject root, String key) { + if (!root.has(key) || !root.get(key).isJsonArray()) root.add(key, new JsonArray()); + return root.getAsJsonArray(key); + } + private static String string(JsonObject root, String key, String fallback) { + return root.has(key) ? root.get(key).getAsString() : fallback; + } + private static boolean bool(JsonObject root, String key, boolean fallback) { + return root.has(key) ? root.get(key).getAsBoolean() : fallback; + } + private static int integer(JsonObject root, String key, int fallback) { + return root.has(key) ? root.get(key).getAsInt() : fallback; + } + private static double decimal(JsonObject root, String key, double fallback) { + return root.has(key) ? root.get(key).getAsDouble() : fallback; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BiomeReferenceScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BiomeReferenceScreen.java new file mode 100644 index 00000000..29803203 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BiomeReferenceScreen.java @@ -0,0 +1,111 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +/** Registry-backed multi-select list for similar-biome references. */ +final class BiomeReferenceScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private final JsonObject placement; + private final String key; + private String searchText = ""; + private int page; + private TextFieldWidget search; + + BiomeReferenceScreen(GuiScreen parent, GeologyEditorSession session, + JsonObject placement, String key) { + super(new TextComponentTranslation("screen.orespawn.biome_references")); + this.parent = parent; + this.session = session; + this.placement = placement; + this.key = key; + } + + @Override + protected void init() { + int contentWidth = Math.min(390, Math.max(280, width - 24)); + int left = (width - contentWidth) / 2; + search = addButton(new TextFieldWidget(font, left, 36, contentWidth - 75, 20, + new TextComponentTranslation("option.orespawn.search"))); + search.setValue(searchText); + addButton(new Button(left + contentWidth - 70, 36, 70, 20, + new TextComponentTranslation("button.orespawn.search"), button -> { + searchText = search.getValue(); page = 0; rebuildWidgets(); + })); + List ids = filtered(); + Set selected = selected(); + int listTop = 64; + int controlsY = height - 52; + int pageSize = Math.max(1, (controlsY - listTop) / 24); + int pageCount = Math.max(1, (ids.size() + pageSize - 1) / pageSize); + page = Math.max(0, Math.min(page, pageCount - 1)); + int start = page * pageSize; + for (int i = 0; i < pageSize && start + i < ids.size(); i++) { + String id = ids.get(start + i); + TextComponentTranslation label = new TextComponentTranslation( + selected.contains(id) ? "button.orespawn.biome_selected" + : "button.orespawn.biome_available", id); + addButton(OreSpawnScreenLayout.button(this, font, left, + listTop + i * 24, contentWidth, 20, label, + button -> { toggle(id); rebuildWidgets(); })); + } + Button previous = addButton(new Button(left, controlsY, 45, 20, + new TextComponentString("<"), button -> { page--; rebuildWidgets(); })); + Button next = addButton(new Button(left + 50, controlsY, 45, 20, + new TextComponentString(">"), button -> { page++; rebuildWidgets(); })); + previous.enabled = page > 0; + next.enabled = page + 1 < pageCount; + addButton(new Button(width / 2 - 75, height - 28, 150, 20, + DialogTexts.GUI_DONE, button -> onClose())); + } + + private List filtered() { + String query = searchText.trim().toLowerCase(Locale.ROOT); + List result = new ArrayList<>(); + for (String id : session.installedBiomeIds()) { + if (query.isEmpty() || id.toLowerCase(Locale.ROOT).contains(query)) result.add(id); + } + return result; + } + + private Set selected() { + Set result = new HashSet<>(); + for (JsonElement value : array()) result.add(value.getAsString()); + return result; + } + + private void toggle(String id) { + Set selected = selected(); + if (!selected.add(id)) selected.remove(id); + JsonArray array = new JsonArray(); + selected.stream().sorted().forEach(value -> + zone.moddev.mc.orespawn.util.JsonCopies.add(array, value)); + placement.add(key, array); + } + + private JsonArray array() { + if (!placement.has(key) || !placement.get(key).isJsonArray()) placement.add(key, new JsonArray()); + return placement.getAsJsonArray(key); + } + + private void rebuildWidgets() { buttons.clear(); children.clear(); init(); } + @Override public void onClose() { minecraft.displayGuiScreen(parent); } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 14, 0xFFFFFF); + super.render(mouseX, mouseY, partialTick); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BiomeWorldMaterialsScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BiomeWorldMaterialsScreen.java new file mode 100644 index 00000000..1d2969c8 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BiomeWorldMaterialsScreen.java @@ -0,0 +1,150 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.JsonObject; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +/** Dimension-level entry point for biome placement and world materials. */ +final class BiomeWorldMaterialsScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private String dimension; + + BiomeWorldMaterialsScreen(GuiScreen parent, GeologyEditorSession session) { + super(new TextComponentTranslation("screen.orespawn.biomes_world_materials")); + this.parent = parent; + this.session = session; + this.dimension = session.availableDimensionIds().get(0); + } + + @Override + protected void init() { + OreSpawnScreenLayout.beginHelp(this); + int contentWidth = Math.min(390, Math.max(280, width - 24)); + int left = (width - contentWidth) / 2; + int half = (contentWidth - 5) / 2; + List dimensions = session.availableDimensionIds(); + if (!dimensions.contains(dimension)) dimension = dimensions.get(0); + OreSpawnScreenLayout.explain(this, addButton(CycleButton.builder(this::dimensionName) + .withValues(dimensions).withInitialValue(dimension) + .create(left, 34, contentWidth, 20, + new TextComponentTranslation("option.orespawn.dimension"), + (button, value) -> { dimension = value; rebuildWidgets(); })), + "tooltip.orespawn.biome.dimension"); + + JsonObject palette = session.biomePalette(dimension, false); + boolean active = palette != null && bool(palette, "enabled", false); + String mode = palette == null ? "augment" : string(palette, "mode", "augment"); + String scope = palette == null ? "minecraft_only" : string(palette, "scope", "minecraft_only"); + String size = palette == null ? "average" : string(palette, "region_size", "average"); + int y = 64; + OreSpawnScreenLayout.explain(this, addButton(CycleButton.onOffBuilder(active) + .create(left, y, contentWidth, 20, + new TextComponentTranslation("option.orespawn.biome_palette"), + (button, value) -> setPaletteEnabled(value))), + "tooltip.orespawn.biome.palette_enabled"); + y += 26; + OreSpawnScreenLayout.explain(this, addButton(CycleButton.builder(this::modeName) + .withValues(Arrays.asList("augment", "replace")).withInitialValue(mode) + .create(left, y, half, 20, + new TextComponentTranslation("option.orespawn.biome_mode"), + (button, value) -> setPalette("mode", value))), + "tooltip.orespawn.biome.mode"); + OreSpawnScreenLayout.explain(this, addButton(CycleButton.builder(this::scopeName) + .withValues(Arrays.asList("all", "minecraft_only", "selected_namespaces")) + .withInitialValue(scope) + .create(left + half + 5, y, half, 20, + new TextComponentTranslation("option.orespawn.biome_scope"), + (button, value) -> setPalette("scope", value))), + "tooltip.orespawn.biome.scope"); + y += 26; + OreSpawnScreenLayout.explain(this, addButton(CycleButton.builder(this::regionName) + .withValues(Arrays.asList("tiny", "small", "average", "large", "huge")) + .withInitialValue(size) + .create(left, y, contentWidth, 20, + new TextComponentTranslation("option.orespawn.biome_region_size"), + (button, value) -> setPalette("region_size", value))), + "tooltip.orespawn.biome.region_size"); + y += 30; + addButton(OreSpawnScreenLayout.explainedButton(this, font, left, y, half, 20, + new TextComponentTranslation("button.orespawn.biome_palette_count", + session.biomePlacementIds(dimension).size()), + button -> minecraft.displayGuiScreen(new BiomePaletteScreen(this, session, dimension)), + "tooltip.orespawn.biome.entries")); + addButton(OreSpawnScreenLayout.explainedButton(this, font, left + half + 5, y, half, 20, + new TextComponentTranslation("button.orespawn.dimension_materials"), + button -> minecraft.displayGuiScreen(new DimensionMaterialsScreen(this, session, dimension)), + "tooltip.orespawn.biome.dimension_materials")); + y += 26; + addButton(OreSpawnScreenLayout.explainedButton(this, font, left, y, contentWidth, 20, + new TextComponentTranslation("button.orespawn.geome_influences"), + button -> minecraft.displayGuiScreen(new GeomeBiomeScreen(this, session)), + "tooltip.orespawn.biome.geome_influences")); + addButton(new Button(width / 2 - 75, OreSpawnScreenLayout.footerY(height), + 150, 20, DialogTexts.GUI_DONE, button -> onClose())); + } + + private void setPaletteEnabled(boolean enabled) { + JsonObject palette = session.biomePalette(dimension, true); + if (enabled && session.biomePlacementIds(dimension).isEmpty()) { + minecraft.displayGuiScreen(new BiomePickerScreen(this, session, + id -> { + session.addBiomePlacement(dimension, id); + minecraft.displayGuiScreen(new BiomePlacementScreen(this, session, dimension, id)); + })); + return; + } + palette.addProperty("enabled", enabled); + } + + private void setPalette(String key, String value) { + session.biomePalette(dimension, true).addProperty(key, value); + } + + private ITextComponent dimensionName(String value) { + return new TextComponentString(value); + } + + private ITextComponent modeName(String value) { + return new TextComponentTranslation("value.orespawn.biome_mode." + value); + } + + private ITextComponent scopeName(String value) { + return new TextComponentTranslation("value.orespawn.biome_scope." + value); + } + + private ITextComponent regionName(String value) { + return new TextComponentTranslation("value.orespawn.preset." + value); + } + + private void rebuildWidgets() { + buttons.clear(); children.clear(); + init(); + } + + @Override + public void onClose() { + minecraft.displayGuiScreen(parent); + } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 14, 0xFFFFFF); + super.render(mouseX, mouseY, partialTick); + OreSpawnScreenLayout.renderExplanations(this, mouseX, mouseY); + } + + private static String string(JsonObject root, String key, String fallback) { + return root.has(key) ? root.get(key).getAsString() : fallback; + } + + private static boolean bool(JsonObject root, String key, boolean fallback) { + return root.has(key) ? root.get(key).getAsBoolean() : fallback; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BlockAssignmentScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BlockAssignmentScreen.java new file mode 100644 index 00000000..98832697 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BlockAssignmentScreen.java @@ -0,0 +1,71 @@ +package zone.moddev.mc.orespawn.client; + +import zone.moddev.mc.orespawn.worldgen.RockFamily; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +final class BlockAssignmentScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private final String blockId; + private ITextComponent error; + + BlockAssignmentScreen(GuiScreen parent, GeologyEditorSession session, String blockId) { + super(new TextComponentTranslation("screen.orespawn.assign_block")); + this.parent = parent; + this.session = session; + String canonicalId = session.canonicalBlockId(blockId); + this.blockId = canonicalId == null ? blockId : canonicalId; + } + + @Override + protected void init() { + OreSpawnScreenLayout.beginHelp(this); + int left = width / 2 - 155; + int right = width / 2 + 5; + addRockButton(left, 70, "tab.orespawn.sedimentary", RockFamily.SEDIMENTARY); + addRockButton(right, 70, "tab.orespawn.metamorphic", RockFamily.METAMORPHIC); + addRockButton(left, 98, "value.orespawn.intrusive", RockFamily.IGNEOUS_INTRUSIVE); + addRockButton(right, 98, "value.orespawn.volcanic", RockFamily.IGNEOUS_VOLCANIC); + OreSpawnScreenLayout.explain(this, addButton(new Button(left, 126, 310, 20, + new TextComponentTranslation("tab.orespawn.ores"), button -> assignOre())), + "tooltip.orespawn.assignment.ore"); + addButton(new Button(width / 2 - 75, height - 28, 150, 20, DialogTexts.GUI_CANCEL, + button -> onClose())); + } + + private void addRockButton(int x, int y, String labelKey, RockFamily family) { + OreSpawnScreenLayout.explain(this, addButton(new Button(x, y, 150, 20, + new TextComponentTranslation(labelKey), button -> assignRock(family))), + "tooltip.orespawn.assignment.rock_family"); + } + + private void assignRock(RockFamily family) { + session.assignRock(blockId, family); + if (session.section("rocks").has(blockId)) minecraft.displayGuiScreen(parent); + else error = new TextComponentString("Unknown or unsuitable block: " + blockId); + } + + private void assignOre() { + session.assignOre(blockId); + if (session.section("ores").has(blockId)) minecraft.displayGuiScreen(parent); + else error = new TextComponentString("Unknown or unsuitable block: " + blockId); + } + + @Override + public void onClose() { + minecraft.displayGuiScreen(parent); + } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 18, 0xFFFFFF); + drawCenteredString(font, new TextComponentString(blockId), width / 2, 42, 0xDDDDDD); + if (error != null) drawCenteredString(font, error, width / 2, 155, 0xFF5555); + super.render(mouseX, mouseY, partialTick); + OreSpawnScreenLayout.renderExplanations(this, mouseX, mouseY); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/BlockPickerScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/BlockPickerScreen.java new file mode 100644 index 00000000..bb88c75c --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/BlockPickerScreen.java @@ -0,0 +1,127 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.List; + +import zone.moddev.mc.orespawn.client.GeologyEditorSession.MaterialTab; +import zone.moddev.mc.orespawn.worldgen.RockFamily; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +/** Registry-backed picker; text narrows installed blocks but never creates an ID. */ +final class BlockPickerScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private final MaterialTab target; + private String searchText = ""; + private String namespace = ""; + private boolean showAll; + private int page; + private TextFieldWidget search; + + BlockPickerScreen(GuiScreen parent, GeologyEditorSession session, MaterialTab target) { + super(new TextComponentTranslation("screen.orespawn.choose_block")); + this.parent = parent; + this.session = session; + this.target = target; + } + + @Override + protected void init() { + OreSpawnScreenLayout.beginHelp(this); + int left = width / 2 - 155; + search = addButton(new TextFieldWidget(font, left, 34, 230, 20, + new TextComponentTranslation("option.orespawn.search"))); + search.setMaxLength(128); + search.setValue(searchText); + addButton(new Button(left + 235, 34, 75, 20, + new TextComponentTranslation("button.orespawn.search"), button -> { + searchText = search.getValue(); + page = 0; + rebuildWidgets(); + })); + + List namespaces = session.installedBlockNamespaces(); + if (!namespaces.contains(namespace)) namespace = ""; + OreSpawnScreenLayout.explain(this, addButton(CycleButton.builder(this::namespaceName) + .withValues(namespaces).withInitialValue(namespace) + .create(left, 58, 150, 20, new TextComponentTranslation("option.orespawn.mod_filter"), + (button, value) -> { namespace = value; page = 0; rebuildWidgets(); })), + "tooltip.orespawn.picker.mod_filter"); + OreSpawnScreenLayout.explain(this, addButton(new Button(left + 160, 58, 150, 20, + new TextComponentTranslation(showAll ? "button.orespawn.safe_only" : "button.orespawn.show_all"), + button -> { showAll = !showAll; page = 0; rebuildWidgets(); })), + showAll ? "tooltip.orespawn.material.safe_only" : "tooltip.orespawn.material.show_all"); + + List ids = session.availableBlockIds(searchText, namespace, showAll); + int listTop = 84; + int controlsY = height - 52; + int pageSize = Math.max(1, (controlsY - listTop) / 24); + int pageCount = Math.max(1, (ids.size() + pageSize - 1) / pageSize); + page = Math.max(0, Math.min(page, pageCount - 1)); + int start = page * pageSize; + for (int i = 0; i < pageSize && start + i < ids.size(); i++) { + String id = ids.get(start + i); + addButton(new Button(left, listTop + (i * 24), 310, 20, + new TextComponentString(id), button -> select(id))); + } + Button previous = addButton(new Button(left, controlsY, 45, 20, + new TextComponentString("<"), button -> { page--; rebuildWidgets(); })); + Button next = addButton(new Button(left + 50, controlsY, 45, 20, + new TextComponentString(">"), button -> { page++; rebuildWidgets(); })); + previous.enabled = page > 0; + next.enabled = page + 1 < pageCount; + addButton(new Button(width / 2 - 75, height - 28, 150, 20, + DialogTexts.GUI_CANCEL, button -> onClose())); + } + + private void select(String id) { + switch (target) { + case SEDIMENTARY: + session.assignRock(id, RockFamily.SEDIMENTARY); + minecraft.displayGuiScreen(new RockEntryScreen(parent, session, id)); + break; + case METAMORPHIC: + session.assignRock(id, RockFamily.METAMORPHIC); + minecraft.displayGuiScreen(new RockEntryScreen(parent, session, id)); + break; + case IGNEOUS: + session.assignRock(id, RockFamily.IGNEOUS_INTRUSIVE); + minecraft.displayGuiScreen(new RockEntryScreen(parent, session, id)); + break; + case ORES: + session.assignOre(id); + minecraft.displayGuiScreen(new OreEntryScreen(parent, session, id)); + break; + default: + minecraft.displayGuiScreen(new BlockAssignmentScreen(parent, session, id)); + } + } + + private ITextComponent namespaceName(String value) { + return value.isEmpty() ? new TextComponentTranslation("value.orespawn.all_mods") : new TextComponentString(value); + } + + private void rebuildWidgets() { + buttons.clear(); children.clear(); + init(); + } + + @Override + public void onClose() { + minecraft.displayGuiScreen(parent); + } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 8, 0xFFFFFF); + drawCenteredString(font, + new TextComponentTranslation("label.orespawn.adding_to", + new TextComponentTranslation("tab.orespawn." + target.key)), + width / 2, 20, 0xCCCCCC); + super.render(mouseX, mouseY, partialTick); + OreSpawnScreenLayout.renderExplanations(this, mouseX, mouseY); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/Button.java b/src/main/java/zone/moddev/mc/orespawn/client/Button.java new file mode 100644 index 00000000..24d7f5c5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/Button.java @@ -0,0 +1,63 @@ +package zone.moddev.mc.orespawn.client; + +import net.minecraft.util.text.ITextComponent; + +/** Target-native string button with OreSpawn's callback and tooltip contract. */ +class Button extends net.minecraft.client.gui.GuiButton { + private final IPressable onPress; + private final Tooltip tooltip; + + Button(int x, int y, int width, int height, ITextComponent message, IPressable onPress) { + this(x, y, width, height, message.getFormattedText(), onPress); + } + + Button(int id, int x, int y, int width, int height, ITextComponent message, + IPressable onPress) { + this(id, x, y, width, height, message.getFormattedText(), onPress, null); + } + + Button(int x, int y, int width, int height, String message, IPressable onPress) { + this(x, y, width, height, message, onPress, null); + } + + Button(int x, int y, int width, int height, ITextComponent message, IPressable onPress, + Tooltip tooltip) { + this(x, y, width, height, message.getFormattedText(), onPress, tooltip); + } + + Button(int x, int y, int width, int height, String message, IPressable onPress, + Tooltip tooltip) { + this(0, x, y, width, height, message, onPress, tooltip); + } + + private Button(int id, int x, int y, int width, int height, String message, + IPressable onPress, Tooltip tooltip) { + super(id, x, y, width, height, message); + this.onPress = onPress; + this.tooltip = tooltip; + } + + void press() { + if (onPress != null) onPress.onPress(this); + } + + void renderTooltip(int mouseX, int mouseY) { + if (tooltip != null && isMouseOver()) tooltip.render(this, mouseX, mouseY); + } + + void setMessage(String message) { + displayString = message; + } + + String getMessage() { + return displayString; + } + + interface IPressable { + void onPress(Button button); + } + + interface Tooltip { + void render(Button button, int mouseX, int mouseY); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/ClientSetup.java b/src/main/java/zone/moddev/mc/orespawn/client/ClientSetup.java new file mode 100644 index 00000000..f7b62945 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/ClientSetup.java @@ -0,0 +1,20 @@ +package zone.moddev.mc.orespawn.client; + +import net.minecraftforge.common.MinecraftForge; + +/** Client-only registration invoked from Forge 1.11 pre-initialization. */ +public final class ClientSetup { + private static boolean initialized; + + private ClientSetup() { + } + + public static synchronized void initialize() { + if (initialized) return; + initialized = true; + // Forge 1.11 ignores static @SubscribeEvent methods when their declaring + // class is registered as an instance. Both world-creation hooks are static, + // so the class object is required for the OreSpawn button to be installed. + MinecraftForge.EVENT_BUS.register(WorldCreationScreenHandler.class); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/CycleButton.java b/src/main/java/zone/moddev/mc/orespawn/client/CycleButton.java new file mode 100644 index 00000000..72c9bd03 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/CycleButton.java @@ -0,0 +1,98 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentTranslation; + +/** Java 8 equivalent of the vanilla cycle button introduced after this target. */ +final class CycleButton extends Button { + private final ITextComponent label; + private final Function valueLabel; + private final List values; + private final OnValueChange callback; + private int index; + + private CycleButton(int x, int y, int width, int height, ITextComponent label, + Function valueLabel, List values, T initialValue, + OnValueChange callback) { + super(x, y, width, height, message(label, valueLabel, initialValue), button -> { + CycleButton cycle = (CycleButton) button; + cycle.advance(); + }); + this.label = Objects.requireNonNull(label, "label"); + this.valueLabel = Objects.requireNonNull(valueLabel, "valueLabel"); + this.values = new ArrayList<>(values); + this.callback = Objects.requireNonNull(callback, "callback"); + this.index = Math.max(0, this.values.indexOf(initialValue)); + setMessage(message(label, valueLabel, getValue())); + } + + static Builder builder(Function valueLabel) { + return new Builder<>(valueLabel); + } + + static Builder onOffBuilder(boolean initialValue) { + return CycleButton.builder(value -> new TextComponentTranslation( + value ? "options.on" : "options.off")) + .withValues(Arrays.asList(Boolean.FALSE, Boolean.TRUE)) + .withInitialValue(initialValue); + } + + T getValue() { + return values.get(index); + } + + private void advance() { + index = (index + 1) % values.size(); + T value = getValue(); + setMessage(message(label, valueLabel, value)); + callback.onValueChange(this, value); + } + + private static String message(ITextComponent label, + Function valueLabel, T value) { + return label.getUnformattedText() + ": " + valueLabel.apply(value).getUnformattedText(); + } + + interface OnValueChange { + void onValueChange(CycleButton button, T value); + } + + static final class Builder { + private final Function valueLabel; + private List values; + private T initialValue; + + private Builder(Function valueLabel) { + this.valueLabel = Objects.requireNonNull(valueLabel, "valueLabel"); + } + + Builder withValues(List values) { + if (values == null || values.isEmpty()) { + throw new IllegalArgumentException("Cycle button values must not be empty"); + } + this.values = new ArrayList<>(values); + return this; + } + + Builder withInitialValue(T initialValue) { + this.initialValue = initialValue; + return this; + } + + CycleButton create(int x, int y, int width, int height, ITextComponent label, + OnValueChange callback) { + if (values == null || values.isEmpty()) { + throw new IllegalStateException("Cycle button values were not configured"); + } + T selected = values.contains(initialValue) ? initialValue : values.get(0); + return new CycleButton<>(x, y, width, height, label, valueLabel, values, + selected, callback); + } + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/DialogTexts.java b/src/main/java/zone/moddev/mc/orespawn/client/DialogTexts.java new file mode 100644 index 00000000..68746313 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/DialogTexts.java @@ -0,0 +1,24 @@ +package zone.moddev.mc.orespawn.client; + +import net.minecraft.block.Block; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +/** Shared translated labels for Minecraft 1.11 screens. */ +final class DialogTexts { + static final TextComponentTranslation GUI_DONE = new TextComponentTranslation("gui.done"); + static final TextComponentTranslation GUI_CANCEL = new TextComponentTranslation("gui.cancel"); + + /** Minecraft 1.11 stores translated block names under {@code tile.*.name}. */ + static TextComponentTranslation blockName(Block block) { + return new TextComponentTranslation(block.getUnlocalizedName() + ".name"); + } + + static ITextComponent blockName(Block block, String fallback) { + return block == null ? new TextComponentString(fallback) : blockName(block); + } + + private DialogTexts() { + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/DimensionDiscovery.java b/src/main/java/zone/moddev/mc/orespawn/client/DimensionDiscovery.java new file mode 100644 index 00000000..68e71a2b --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/DimensionDiscovery.java @@ -0,0 +1,53 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.client.gui.GuiCreateWorld; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.common.DimensionManager; + +final class DimensionDiscovery { + private static final String OVERWORLD = "minecraft:overworld"; + private static final String NETHER = "minecraft:the_nether"; + private static final String END = "minecraft:the_end"; + + private DimensionDiscovery() { + } + + static List availableDimensionIds(GuiCreateWorld screen) { + Set result = new TreeSet<>(); + result.add(OVERWORLD); + result.add(NETHER); + result.add(END); + + for (Integer id : DimensionManager.getStaticDimensionIDs()) { + if (id == 0 || id == -1 || id == 1) continue; + result.add("legacy:dimension_" + id); + } + return vanillaFirst(result); + } + + private static List vanillaFirst(Set ids) { + List result = new ArrayList<>(); + result.add(OVERWORLD); + result.add(NETHER); + result.add(END); + ids.remove(OVERWORLD); + ids.remove(NETHER); + ids.remove(END); + result.addAll(ids); + return result; + } + + static void addDimensionId(Set target, String namespace, String path) { + if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+")) return; + try { + target.add(new ResourceLocation(namespace, path).toString()); + } catch (RuntimeException ignored) { + // Ignore malformed resource paths from third-party jars. + } + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/DimensionMaterialsScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/DimensionMaterialsScreen.java new file mode 100644 index 00000000..4c0d710a --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/DimensionMaterialsScreen.java @@ -0,0 +1,98 @@ +package zone.moddev.mc.orespawn.client; + +import com.google.gson.JsonObject; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; + +final class DimensionMaterialsScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private final String dimension; + private TextFieldWidget deepY; + + DimensionMaterialsScreen(GuiScreen parent, GeologyEditorSession session, String dimension) { + super(new TextComponentTranslation("screen.orespawn.dimension_materials")); + this.parent = parent; + this.session = session; + this.dimension = dimension; + } + + @Override + protected void init() { + OreSpawnScreenLayout.beginHelp(this); + int contentWidth = Math.min(390, Math.max(290, width - 24)); + int left = (width - contentWidth) / 2; + JsonObject materials = session.dimensionMaterials(dimension, true); + int y = 58; + y = materialRow(left, y, contentWidth, materials, "default_fluid", true, true); + y = materialRow(left, y, contentWidth, materials, "deep_aquifer_fluid", true, false); + deepY = addButton(new TextFieldWidget(font, left + contentWidth / 2, y, + contentWidth / 2, 20, new TextComponentTranslation("option.orespawn.deep_aquifer_y"))); + deepY.setValue(Integer.toString(integer(materials, "deep_aquifer_max_y", -54))); + deepY.enabled = false; + OreSpawnScreenLayout.explain(this, deepY, "tooltip.orespawn.material.deep_aquifer_y"); + addButton(new Button(left, y, contentWidth / 2 - 5, 20, + new TextComponentTranslation("option.orespawn.deep_aquifer_y"), button -> { })).enabled = false; + y += 26; + y = materialRow(left, y, contentWidth, materials, "snow_block", false, true); + materialRow(left, y, contentWidth, materials, "ice_block", false, true); + addButton(new Button(left, OreSpawnScreenLayout.footerY(height), + contentWidth, 20, DialogTexts.GUI_DONE, button -> { save(); onClose(); })); + } + + private int materialRow(int left, int y, int width, JsonObject materials, + String key, boolean fluid, boolean editable) { + String value = string(materials, key, ""); + Button selector = addButton(OreSpawnScreenLayout.explainedButton(this, font, + left, y, width - 65, 20, + label(key, value), button -> { + save(); + minecraft.displayGuiScreen(new MaterialBlockPickerScreen(this, session, fluid, + id -> session.setMaterialBlock(dimension, key, id, fluid))); + }, materialHelp(key))); + selector.enabled = editable; + Button clear = addButton(new Button(left + width - 60, y, 60, 20, + new TextComponentTranslation("button.orespawn.clear"), button -> { + session.setMaterialBlock(dimension, key, null, fluid); + rebuildWidgets(); + })); + clear.enabled = editable && !value.isEmpty(); + return y + 26; + } + + private static String materialHelp(String key) { + return "tooltip.orespawn.material." + key; + } + + private ITextComponent label(String key, String value) { + return new TextComponentTranslation("option.orespawn." + key, + value.isEmpty() ? new TextComponentTranslation("value.orespawn.not_set") + : new TextComponentString(value)); + } + + private void save() { + // Minecraft 1.11.2 exposes one generator fluid. Retain stored deep-aquifer + // fields unchanged so the same provider/profile can still be used by later ports. + } + + private void rebuildWidgets() { buttons.clear(); children.clear(); init(); } + @Override public void onClose() { minecraft.displayGuiScreen(parent); } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 12, 0xFFFFFF); + drawCenteredString(font, new TextComponentString(dimension), width / 2, 30, 0xCCCCCC); + super.render(mouseX, mouseY, partialTick); + OreSpawnScreenLayout.renderExplanations(this, mouseX, mouseY); + } + + private static String string(JsonObject root, String key, String fallback) { + return root.has(key) ? root.get(key).getAsString() : fallback; + } + private static int integer(JsonObject root, String key, int fallback) { + return root.has(key) ? root.get(key).getAsInt() : fallback; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/FluidBlockPickerScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/FluidBlockPickerScreen.java new file mode 100644 index 00000000..4245230a --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/FluidBlockPickerScreen.java @@ -0,0 +1,102 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.List; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.ResourceLocation; +import net.minecraft.block.Block; +import net.minecraftforge.fml.common.registry.ForgeRegistries; + +final class FluidBlockPickerScreen extends OreSpawnScreen { + private final GuiScreen parent; + private final GeologyEditorSession session; + private TextFieldWidget search; + private String searchText = ""; + private int page; + + FluidBlockPickerScreen(GuiScreen parent, GeologyEditorSession session) { + super(new TextComponentTranslation("screen.orespawn.choose_block")); + this.parent = parent; + this.session = session; + } + + @Override + protected void init() { + int contentWidth = Math.min(390, Math.max(260, width - 24)); + int left = (width - contentWidth) / 2; + int searchButtonWidth = 70; + search = addButton(new TextFieldWidget(font, left, 40, + contentWidth - searchButtonWidth - 5, 20, + new TextComponentTranslation("option.orespawn.search"))); + search.setValue(searchText); + addButton(OreSpawnScreenLayout.button(this, font, + left + contentWidth - searchButtonWidth, 40, searchButtonWidth, 20, + new TextComponentTranslation("button.orespawn.search"), button -> { + searchText = search.getValue(); + page = 0; + rebuildWidgets(); + })); + + List ids = session.availableFluidBlockIds(searchText); + int listTop = 68; + int controlsY = height - 52; + int pageSize = Math.max(1, (controlsY - listTop) / 24); + int pageCount = Math.max(1, (ids.size() + pageSize - 1) / pageSize); + page = Math.max(0, Math.min(page, pageCount - 1)); + int start = page * pageSize; + for (int i = 0; i < pageSize && start + i < ids.size(); i++) { + String id = ids.get(start + i); + addButton(new Button(left, listTop + (i * 24), contentWidth, 20, + OreSpawnScreenLayout.fit(font, fluidName(id), contentWidth - 8), + button -> choose(id), + (button, mouseX, mouseY) -> renderComponentTooltip( + java.util.Collections.singletonList(new TextComponentString(id)), + mouseX, mouseY))); + } + + Button previous = addButton(new Button(left, controlsY, 45, 20, + new TextComponentString("<"), button -> { page--; rebuildWidgets(); })); + Button next = addButton(new Button(left + 50, controlsY, 45, 20, + new TextComponentString(">"), button -> { page++; rebuildWidgets(); })); + previous.enabled = page > 0; + next.enabled = page + 1 < pageCount; + addButton(new Button(width / 2 - 75, height - 28, 150, 20, + DialogTexts.GUI_CANCEL, button -> onClose())); + } + + private void choose(String blockId) { + String ruleId = session.assignFluidDeposit(blockId); + if (ruleId != null) { + minecraft.displayGuiScreen(new FluidDepositEntryScreen(parent, session, ruleId)); + } + } + + private ITextComponent fluidName(String id) { + try { + Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(id)); + return DialogTexts.blockName(block, id); + } catch (RuntimeException ignored) { + return new TextComponentString(id); + } + } + + private void rebuildWidgets() { + buttons.clear(); children.clear(); + init(); + } + + @Override + public void onClose() { + minecraft.displayGuiScreen(parent); + } + + @Override + public void render(int mouseX, int mouseY, float partialTick) { + renderBackground(); + drawCenteredString(font, title, width / 2, 18, 0xFFFFFF); + super.render(mouseX, mouseY, partialTick); + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java new file mode 100644 index 00000000..d212fce8 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/FluidDepositDimensionScreen.java @@ -0,0 +1,367 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import zone.moddev.mc.orespawn.worldgen.RockFamily; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.ResourceLocation; + +final class FluidDepositDimensionScreen extends OreSpawnScreen { + private enum Page { PLACEMENT, HOSTS, BIOMES } + + private final GuiScreen parent; + private final GeologyEditorSession session; + private final String depositId; + private final String dimensionId; + private final EnumSet families = EnumSet.noneOf(RockFamily.class); + private final List