commit 2939a4b16b5168c347fb8b89d86fb2c7e3bc6383 Author: AlphaKR93 Date: Mon Mar 6 23:57:53 2023 +0900 Initial Commit diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b414612 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "*" + schedule: + interval: "daily" + + - package-ecosystem: "gradle" + directory: "/" + target-branch: "*" + schedule: + interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c7529c0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,104 @@ +name: Build Plazma + +on: + push: + branches: [ "ver/*", "feat/*", "dev/*", "expr/*" ] + workflow_dispatch: + +env: + ORG_NAME: PlazmaMC + MC_VERSION: 1.19.3 + MAIN_BRANCH: ver/1.19.3 + DEBUG: 'false' + +jobs: + release: + strategy: + matrix: + jdk: [19.0.1] + java: ['temurin'] + os: [ubuntu-20.04] + + if: "!startsWith(github.event.commits[0].message, '[CI-Skip]')" + runs-on: ${{ matrix.os }} + steps: + - name: Checkout action + uses: actions/checkout@v3 + + - name: Checkout pages + uses: actions/checkout@v3 + with: + path: javadoc + ref: gh-pages + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Set up JDK ${{ matrix.java }} ${{ matrix.jdk }} + uses: actions/setup-java@v3 + with: + distribution: ${{ matrix.java }} + java-version: ${{ matrix.jdk }} + cache: 'gradle' + + - name: Configure Git + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]" + echo "workflow=$GITHUB_RUN_NUMBER" >> $GITHUB_ENV + + - name: Apply Patches + run: ./gradlew applyPatches --stacktrace + + - name: Build + run: ./gradlew build --stacktrace + + - name: Create Reobf Jar + run: ./gradlew createReobfPaperclipJar --stacktrace + + - name: Create Mojmap Jar + run: ./gradlew createMojmapPaperclipJar --stacktrace + + - name: Update Javadoc + continue-on-error: true + if: github.ref_name == env.MAIN_BRANCH + run: | + (cd Plazma-API/build/docs/javadoc && tar c .) | (cd javadoc && tar xf -) + cd javadoc + git add . && git commit -m "Update Javadocs" + git push + + - name: Publish Packages + if: github.ref_name == env.MAIN_BRANCH + run: | + export GITHUB_USERNAME=${{ env.ORG_NAME }} + export GITHUB_TOKEN=${{ secrets.GH_PAT }} + ./gradlew publish --stacktrace + + - name: Upload Artifacts + if: env.DEBUG == 'true' || github.ref_name != env.MAIN_BRANCH + uses: actions/upload-artifact@v3 + with: + name: Artifacts + path: | + build/libs + Plazma-API/build/docs/javadoc + + - name: Release Artifacts + if: github.ref_name == env.MAIN_BRANCH + uses: marvinpinto/action-automatic-releases@latest + with: + title: "Release #${{ env.workflow }}" + automatic_release_tag: release-${{ env.workflow }} + repo_token: "${{ secrets.GH_PAT }}" + files: build/libs/*.jar + prerelease: false + + - name: Release Artifacts (Latest) + if: github.ref_name == env.MAIN_BRANCH + uses: marvinpinto/action-automatic-releases@latest + with: + title: "Release #${{ env.workflow }}" + automatic_release_tag: latest-${{ env.MC_VERSION }} + repo_token: "${{ secrets.GH_PAT }}" + files: build/libs/*.jar + prerelease: false diff --git a/.github/workflows/build_javadoc.yml b/.github/workflows/build_javadoc.yml new file mode 100644 index 0000000..93742fc --- /dev/null +++ b/.github/workflows/build_javadoc.yml @@ -0,0 +1,46 @@ +name: Deploy Javadocs + +on: + push: + branches: ["gh-pages"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + if: "!startsWith(github.event.commits[0].message, '[CI-Skip]')" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site + + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml new file mode 100644 index 0000000..e83c455 --- /dev/null +++ b/.github/workflows/pull_requests.yml @@ -0,0 +1,55 @@ +name: Test Pull Request + +on: + pull_request: + branches: [ "ver/*", "feat/*", "dev/*", "expr/*" ] + workflow_dispatch: + +jobs: + release: + strategy: + matrix: + jdk: [19.0.1] + java: ['temurin'] + os: [ubuntu-20.04] + + if: contains(github.event.pull_request.labels.*.name, 'build') && !startsWith(github.event.commits[0].message, '[CI-Skip]') + runs-on: ${{ matrix.os }} + steps: + - name: Checkout action + uses: actions/checkout@v3 + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Set up JDK ${{ matrix.java }} ${{ matrix.jdk }} + uses: actions/setup-java@v3 + with: + distribution: ${{ matrix.java }} + java-version: ${{ matrix.jdk }} + cache: 'gradle' + + - name: Configure Git + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]" + + - name: Apply Patches + run: ./gradlew applyPatches --stacktrace + + - name: Build + run: ./gradlew build --stacktrace + + - name: Create Reobf Jar + run: ./gradlew createReobfPaperclipJar --stacktrace + + - name: Create Mojmap Jar + run: ./gradlew createMojmapPaperclipJar --stacktrace + + - name: Upload Artifacts + if: contains(github.event.pull_request.labels.*.name, 'upload_artifact') + uses: actions/upload-artifact@v3 + with: + name: Artifacts + path: | + build/libs + Plazma-API/build/docs/javadoc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e36a572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# JVM crash related +core.* +hs_err_pid* + +# Intellij +.idea/ +*.iml +*.ipr +*.iws +out/ + +# Eclipse +.classpath +.project +.settings/ + +# netbeans +nbproject/ +nbactions.xml + +# Gradle +!gradle-wrapper.jar +.gradle/ +build/ +*/build/ + +# we use maven! +build.xml + +# Maven +log/ +target/ +dependency-reduced-pom.xml + +# various other potential build files +build/ +bin/ +dist/ +manifest.mf + +# Mac +.DS_Store/ +.DS_Store + +# vim +.*.sw[a-p] + +# Linux temp files +*~ + +# other stuff +run/ + +build-data/ +*-API +*-MojangAPI +*-Server +*.jar diff --git a/.upstream-data b/.upstream-data new file mode 100644 index 0000000..2514ce0 --- /dev/null +++ b/.upstream-data @@ -0,0 +1,2 @@ +purpurCommit = 3940c828a6e1945054ab8eeb747dbfcb99f6ccfd +pufferfishCommit = 52307ae220ad5a9a01ac4e273b921f3cbb097de5 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6c40a95 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,23 @@ +The MIT License (MIT) +===================== + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the โ€œSoftwareโ€), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED โ€œAS ISโ€, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcbdd99 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ + + +# Plazma +[![License](https://img.shields.io/github/license/PlazmaMC/Plazma?style=flat-square)](LICENSE.md) +[![Build](https://img.shields.io/github/actions/workflow/status/PlazmaMC/Plazma/build.yml?branch=ver/1.19.3&logo=github&style=flat-square)](https://github.com/PlazmaMC/Plazma/actions/workflows/build.yml?query=branch:ver/1.19.3) + +A Server Platform for Minecraft: Java Edition based on [Paper](https://github.com/PaperMC/Paper) + +## ๐Ÿงช Still Work In Progress +Plazma is still under development and has no functionality. + +## โš ๏ธ Warning +Plazma is VERY UNSTABLE and can cause unexpected problems, so DO NOT USE it in production server. + +## โฌ‡๏ธ Downloads +[![Build](https://img.shields.io/github/actions/workflow/status/PlazmaMC/Plazma/build.yml?branch=ver/1.19.3&logo=github&style=flat-square)](https://github.com/PlazmaMC/Plazma/actions/workflows/build.yml?query=branch:ver/1.19.3) + +You can download the file from the [Releases](https://github.com/PlazmaMC/Plazma/releases) tab, or you can [click here](https://github.com/PlazmaMC/Plazma/releases/download/latest-1.19.3/andromeda-paperclip-1.19.3-R0.1-SNAPSHOT-reobf.jar) to download it. + +If you do not know about Mojmap or Bundler, download `andromeda-paperclip-*-reobf.jar`. + +## โš–๏ธ License +[![License](https://img.shields.io/github/license/PlazmaMC/Plazma?style=flat-square)](LICENSE.md) + +All patches are licensed under the MIT license, unless otherwise noted in the patch headers. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c814a97 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,82 @@ +import io.papermc.paperweight.util.constants.* + +plugins { + java + `maven-publish` + id("com.github.johnrengelman.shadow") version "8.1.0" apply false + id("io.papermc.paperweight.patcher") version "1.5.3" +} + +repositories { + mavenCentral() + maven("https://papermc.io/repo/repository/maven-public/") { + content { + onlyForConfigurations(PAPERCLIP_CONFIG) + } + } +} + +dependencies { + remapper("net.fabricmc:tiny-remapper:0.8.6:fat") + decompiler("net.minecraftforge:forgeflower:2.0.605.2") + paperclip("io.papermc:paperclip:3.0.2") +} + +subprojects { + apply(plugin = "java") + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } +} + +subprojects { + tasks.withType().configureEach { + options.encoding = "UTF-8" + options.release.set(17) + } + + tasks.withType { + options.encoding = Charsets.UTF_8.name() + } + + tasks.withType { + filteringCharset = Charsets.UTF_8.name() + } + tasks.withType { + minHeapSize = "2g" + maxHeapSize = "2g" + } + + repositories { + mavenCentral() + maven("https://oss.sonatype.org/content/groups/public/") + maven("https://papermc.io/repo/repository/maven-public/") + maven("https://ci.emc.gs/nexus/content/groups/aikar/") + maven("https://repo.aikar.co/content/groups/aikar") + maven("https://repo.md-5.net/content/repositories/releases/") + maven("https://hub.spigotmc.org/nexus/content/groups/public/") + maven("https://jitpack.io") + maven("https://oss.sonatype.org/content/repositories/snapshots/") + } + +} + +paperweight { + serverProject.set(project(":plazma-server")) + + remapRepo.set("https://maven.fabricmc.net/") + decompileRepo.set("https://files.minecraftforge.net/maven/") + + usePaperUpstream(providers.gradleProperty("paperCommit")) { + withPaperPatcher { + apiPatchDir.set(layout.projectDirectory.dir("patches/api")) + apiOutputDir.set(layout.projectDirectory.dir("Plazma-API")) + + serverPatchDir.set(layout.projectDirectory.dir("patches/server")) + serverOutputDir.set(layout.projectDirectory.dir("Plazma-Server")) + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2451b08 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +group = org.plazmamc.plazma +version = 1.19.3-R0.1-SNAPSHOT + +paperCommit = 5cc78f2b6f42132843b4fe02c3e509f727aa4714 + +org.gradle.caching = true +org.gradle.parallel = true +org.gradle.vfs.watch = false +org.gradle.jvmargs = -Xmx3G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bdc9a83 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..79a61d4 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright ยฉ 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป, +# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป; +# * compound commands having a testable exit status, especially ยซcaseยป; +# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/patches/api/0001-Pufferfish-API-Changes.patch b/patches/api/0001-Pufferfish-API-Changes.patch new file mode 100644 index 0000000..5c03218 --- /dev/null +++ b/patches/api/0001-Pufferfish-API-Changes.patch @@ -0,0 +1,523 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Mon, 6 Mar 2023 09:28:59 +0000 +Subject: [PATCH] Pufferfish API Changes + +Original: Kevin Raneri +Copyright (C) 2023 Pufferfish Studios LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +diff --git a/build.gradle.kts b/build.gradle.kts +index 7ba2b08b8c5eb405a64f9edfa72195dcf48f82bd..d351ae66db72208a78ee6c522201693f19f68ef5 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -41,6 +41,7 @@ dependencies { + apiAndDocs("net.kyori:adventure-text-logger-slf4j") + api("org.apache.logging.log4j:log4j-api:2.17.1") + api("org.slf4j:slf4j-api:1.8.0-beta4") ++ api("io.sentry:sentry:5.4.0") // Pufferfish + + implementation("org.ow2.asm:asm:9.2") + implementation("org.ow2.asm:asm-commons:9.2") +@@ -84,6 +85,13 @@ val generateApiVersioningFile by tasks.registering { + } + } + ++// Pufferfish Start ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("--add-modules=jdk.incubator.vector") ++} ++// Pufferfish End ++ + tasks.jar { + from(generateApiVersioningFile.map { it.outputs.files.singleFile }) { + into("META-INF/maven/${project.group}/${project.name}") +diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java +new file mode 100644 +index 0000000000000000000000000000000000000000..10310fdd53de28efb8a8250f6d3b0c8eb08fb68a +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java +@@ -0,0 +1,161 @@ ++package gg.pufferfish.pufferfish.sentry; ++ ++import com.google.gson.Gson; ++import java.lang.reflect.Field; ++import java.lang.reflect.Modifier; ++import java.util.Map; ++import java.util.TreeMap; ++import org.apache.logging.log4j.ThreadContext; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Event; ++import org.bukkit.event.player.PlayerEvent; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.RegisteredListener; ++import org.jetbrains.annotations.Nullable; ++ ++public class SentryContext { ++ ++ private static final Gson GSON = new Gson(); ++ ++ public static void setPluginContext(@Nullable Plugin plugin) { ++ if (plugin != null) { ++ ThreadContext.put("pufferfishsentry_pluginname", plugin.getName()); ++ ThreadContext.put("pufferfishsentry_pluginversion", plugin.getDescription().getVersion()); ++ } ++ } ++ ++ public static void removePluginContext() { ++ ThreadContext.remove("pufferfishsentry_pluginname"); ++ ThreadContext.remove("pufferfishsentry_pluginversion"); ++ } ++ ++ public static void setSenderContext(@Nullable CommandSender sender) { ++ if (sender != null) { ++ ThreadContext.put("pufferfishsentry_playername", sender.getName()); ++ if (sender instanceof Player player) { ++ ThreadContext.put("pufferfishsentry_playerid", player.getUniqueId().toString()); ++ } ++ } ++ } ++ ++ public static void removeSenderContext() { ++ ThreadContext.remove("pufferfishsentry_playername"); ++ ThreadContext.remove("pufferfishsentry_playerid"); ++ } ++ ++ public static void setEventContext(Event event, RegisteredListener registration) { ++ setPluginContext(registration.getPlugin()); ++ ++ try { ++ // Find the player that was involved with this event ++ Player player = null; ++ if (event instanceof PlayerEvent) { ++ player = ((PlayerEvent) event).getPlayer(); ++ } else { ++ Class eventClass = event.getClass(); ++ ++ Field playerField = null; ++ ++ for (Field field : eventClass.getDeclaredFields()) { ++ if (field.getType().equals(Player.class)) { ++ playerField = field; ++ break; ++ } ++ } ++ ++ if (playerField != null) { ++ playerField.setAccessible(true); ++ player = (Player) playerField.get(event); ++ } ++ } ++ ++ if (player != null) { ++ setSenderContext(player); ++ } ++ } catch (Exception e) {} // We can't really safely log exceptions. ++ ++ ThreadContext.put("pufferfishsentry_eventdata", GSON.toJson(serializeFields(event))); ++ } ++ ++ public static void removeEventContext() { ++ removePluginContext(); ++ removeSenderContext(); ++ ThreadContext.remove("pufferfishsentry_eventdata"); ++ } ++ ++ private static Map serializeFields(Object object) { ++ Map fields = new TreeMap<>(); ++ fields.put("_class", object.getClass().getName()); ++ for (Field declaredField : object.getClass().getDeclaredFields()) { ++ try { ++ if (Modifier.isStatic(declaredField.getModifiers())) { ++ continue; ++ } ++ ++ String fieldName = declaredField.getName(); ++ if (fieldName.equals("handlers")) { ++ continue; ++ } ++ declaredField.setAccessible(true); ++ Object value = declaredField.get(object); ++ if (value != null) { ++ fields.put(fieldName, value.toString()); ++ } else { ++ fields.put(fieldName, ""); ++ } ++ } catch (Exception e) {} // We can't really safely log exceptions. ++ } ++ return fields; ++ } ++ ++ public static class State { ++ ++ private Plugin plugin; ++ private Command command; ++ private String commandLine; ++ private Event event; ++ private RegisteredListener registeredListener; ++ ++ public Plugin getPlugin() { ++ return plugin; ++ } ++ ++ public void setPlugin(Plugin plugin) { ++ this.plugin = plugin; ++ } ++ ++ public Command getCommand() { ++ return command; ++ } ++ ++ public void setCommand(Command command) { ++ this.command = command; ++ } ++ ++ public String getCommandLine() { ++ return commandLine; ++ } ++ ++ public void setCommandLine(String commandLine) { ++ this.commandLine = commandLine; ++ } ++ ++ public Event getEvent() { ++ return event; ++ } ++ ++ public void setEvent(Event event) { ++ this.event = event; ++ } ++ ++ public RegisteredListener getRegisteredListener() { ++ return registeredListener; ++ } ++ ++ public void setRegisteredListener(RegisteredListener registeredListener) { ++ this.registeredListener = registeredListener; ++ } ++ } ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ab5fea0b03224bf249352ce340e94704ff713345 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java +@@ -0,0 +1,40 @@ ++package gg.pufferfish.pufferfish.simd; ++ ++import java.util.logging.Level; ++import java.util.logging.Logger; ++import jdk.incubator.vector.FloatVector; ++import jdk.incubator.vector.IntVector; ++import jdk.incubator.vector.VectorSpecies; ++ ++/** ++ * Basically, java is annoying and we have to push this out to its own class. ++ */ ++@Deprecated ++public class SIMDChecker { ++ ++ @Deprecated ++ public static boolean canEnable(Logger logger) { ++ try { ++ if (SIMDDetection.getJavaVersion() != 17 && SIMDDetection.getJavaVersion() != 18 && SIMDDetection.getJavaVersion() != 19) { ++ return false; ++ } else { ++ SIMDDetection.testRun = true; ++ ++ VectorSpecies ISPEC = IntVector.SPECIES_PREFERRED; ++ VectorSpecies FSPEC = FloatVector.SPECIES_PREFERRED; ++ ++ logger.log(Level.INFO, "Max SIMD vector size on this system is " + ISPEC.vectorBitSize() + " bits (int)"); ++ logger.log(Level.INFO, "Max SIMD vector size on this system is " + FSPEC.vectorBitSize() + " bits (float)"); ++ ++ if (ISPEC.elementSize() < 2 || FSPEC.elementSize() < 2) { ++ logger.log(Level.WARNING, "SIMD is not properly supported on this system!"); ++ return false; ++ } ++ ++ return true; ++ } ++ } catch (NoClassDefFoundError | Exception ignored) {} // Basically, we don't do anything. This lets us detect if it's not functional and disable it. ++ return false; ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a84889d3e9cfc4d7ab5f867820a6484c6070711b +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java +@@ -0,0 +1,35 @@ ++package gg.pufferfish.pufferfish.simd; ++ ++import java.util.logging.Logger; ++ ++@Deprecated ++public class SIMDDetection { ++ ++ public static boolean isEnabled = false; ++ public static boolean versionLimited = false; ++ public static boolean testRun = false; ++ ++ @Deprecated ++ public static boolean canEnable(Logger logger) { ++ try { ++ return SIMDChecker.canEnable(logger); ++ } catch (NoClassDefFoundError | Exception ignored) { ++ return false; ++ } ++ } ++ ++ @Deprecated ++ public static int getJavaVersion() { ++ // https://stackoverflow.com/a/2591122 ++ String version = System.getProperty("java.version"); ++ if(version.startsWith("1.")) { ++ version = version.substring(2, 3); ++ } else { ++ int dot = version.indexOf("."); ++ if(dot != -1) { version = version.substring(0, dot); } ++ } ++ version = version.split("-")[0]; // Azul is stupid ++ return Integer.parseInt(version); ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java b/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ae2464920c9412ac90b819a540ee58be0741465f +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java +@@ -0,0 +1,83 @@ ++package gg.pufferfish.pufferfish.simd; ++ ++import java.awt.Color; ++import jdk.incubator.vector.FloatVector; ++import jdk.incubator.vector.IntVector; ++import jdk.incubator.vector.VectorMask; ++import jdk.incubator.vector.VectorSpecies; ++import org.bukkit.map.MapPalette; ++ ++@Deprecated ++public class VectorMapPalette { ++ ++ private static final VectorSpecies I_SPEC = IntVector.SPECIES_PREFERRED; ++ private static final VectorSpecies F_SPEC = FloatVector.SPECIES_PREFERRED; ++ ++ @Deprecated ++ public static void matchColorVectorized(int[] in, byte[] out) { ++ int speciesLength = I_SPEC.length(); ++ int i; ++ for (i = 0; i < in.length - speciesLength; i += speciesLength) { ++ float[] redsArr = new float[speciesLength]; ++ float[] bluesArr = new float[speciesLength]; ++ float[] greensArr = new float[speciesLength]; ++ int[] alphasArr = new int[speciesLength]; ++ ++ for (int j = 0; j < speciesLength; j++) { ++ alphasArr[j] = (in[i + j] >> 24) & 0xFF; ++ redsArr[j] = (in[i + j] >> 16) & 0xFF; ++ greensArr[j] = (in[i + j] >> 8) & 0xFF; ++ bluesArr[j] = (in[i + j] >> 0) & 0xFF; ++ } ++ ++ IntVector alphas = IntVector.fromArray(I_SPEC, alphasArr, 0); ++ FloatVector reds = FloatVector.fromArray(F_SPEC, redsArr, 0); ++ FloatVector greens = FloatVector.fromArray(F_SPEC, greensArr, 0); ++ FloatVector blues = FloatVector.fromArray(F_SPEC, bluesArr, 0); ++ IntVector resultIndex = IntVector.zero(I_SPEC); ++ VectorMask modificationMask = VectorMask.fromLong(I_SPEC, 0xffffffff); ++ ++ modificationMask = modificationMask.and(alphas.lt(128).not()); ++ FloatVector bestDistances = FloatVector.broadcast(F_SPEC, Float.MAX_VALUE); ++ ++ for (int c = 4; c < MapPalette.colors.length; c++) { ++ // We're using 32-bit floats here because it's 2x faster and nobody will know the difference. ++ // For correctness, the original algorithm uses 64-bit floats instead. Completely unnecessary. ++ FloatVector compReds = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getRed()); ++ FloatVector compGreens = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getGreen()); ++ FloatVector compBlues = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getBlue()); ++ ++ FloatVector rMean = reds.add(compReds).div(2.0f); ++ FloatVector rDiff = reds.sub(compReds); ++ FloatVector gDiff = greens.sub(compGreens); ++ FloatVector bDiff = blues.sub(compBlues); ++ ++ FloatVector weightR = rMean.div(256.0f).add(2); ++ FloatVector weightG = FloatVector.broadcast(F_SPEC, 4.0f); ++ FloatVector weightB = FloatVector.broadcast(F_SPEC, 255.0f).sub(rMean).div(256.0f).add(2.0f); ++ ++ FloatVector distance = weightR.mul(rDiff).mul(rDiff).add(weightG.mul(gDiff).mul(gDiff)).add(weightB.mul(bDiff).mul(bDiff)); ++ ++ // Now we compare to the best distance we've found. ++ // This mask contains a "1" if better, and a "0" otherwise. ++ VectorMask bestDistanceMask = distance.lt(bestDistances); ++ bestDistances = bestDistances.blend(distance, bestDistanceMask); // Update the best distances ++ ++ // Update the result array ++ // We also AND with the modification mask because we don't want to interfere if the alpha value isn't large enough. ++ resultIndex = resultIndex.blend(c, bestDistanceMask.cast(I_SPEC).and(modificationMask)); // Update the results ++ } ++ ++ for (int j = 0; j < speciesLength; j++) { ++ int index = resultIndex.lane(j); ++ out[i + j] = (byte) (index < 128 ? index : -129 + (index - 127)); ++ } ++ } ++ ++ // For the final ones, fall back to the regular method ++ for (; i < in.length; i++) { ++ out[i] = MapPalette.matchColor(new Color(in[i], true)); ++ } ++ } ++ ++} +diff --git a/src/main/java/org/bukkit/map/MapPalette.java b/src/main/java/org/bukkit/map/MapPalette.java +index 3a9aaca2e76411a9c27f9f5e0f22d060d5a66d06..9584e245144b561b4f6745b2f26a4f69a6f92891 100644 +--- a/src/main/java/org/bukkit/map/MapPalette.java ++++ b/src/main/java/org/bukkit/map/MapPalette.java +@@ -1,6 +1,7 @@ + package org.bukkit.map; + + import com.google.common.base.Preconditions; ++import gg.pufferfish.pufferfish.simd.SIMDDetection; // Pufferfish + import java.awt.Color; + import java.awt.Graphics2D; + import java.awt.Image; +@@ -40,7 +41,7 @@ public final class MapPalette { + } + + @NotNull +- static final Color[] colors = { ++ public static final Color[] colors = { // Pufferfish - public access + c(0, 0, 0, 0), c(0, 0, 0, 0), c(0, 0, 0, 0), c(0, 0, 0, 0), + c(89, 125, 39), c(109, 153, 48), c(127, 178, 56), c(67, 94, 29), + c(174, 164, 115), c(213, 201, 140), c(247, 233, 163), c(130, 123, 86), +@@ -211,9 +212,15 @@ public final class MapPalette { + temp.getRGB(0, 0, temp.getWidth(), temp.getHeight(), pixels, 0, temp.getWidth()); + + byte[] result = new byte[temp.getWidth() * temp.getHeight()]; ++ // Pufferfish start ++ if (!SIMDDetection.isEnabled) { + for (int i = 0; i < pixels.length; i++) { + result[i] = matchColor(new Color(pixels[i], true)); + } ++ } else { ++ gg.pufferfish.pufferfish.simd.VectorMapPalette.matchColorVectorized(pixels, result); ++ } ++ // Pufferfish end + return result; + } + +diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +index 2b8308989fce7f8a16907f8711b362e671fdbfb6..bd4d1a40f53784662174d426533ef4b5433a15b7 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -584,7 +584,9 @@ public final class SimplePluginManager implements PluginManager { + + // Paper start + private void handlePluginException(String msg, Throwable ex, Plugin plugin) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish + server.getLogger().log(Level.SEVERE, msg, ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish + callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin))); + } + // Paper end +@@ -654,9 +656,11 @@ public final class SimplePluginManager implements PluginManager { + )); + } + } catch (Throwable ex) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setEventContext(event, registration); // Pufferfish + // Paper start - error reporting + String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getDescription().getFullName(); + server.getLogger().log(Level.SEVERE, msg, ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removeEventContext(); // Pufferfish + if (!(event instanceof com.destroystokyo.paper.event.server.ServerExceptionEvent)) { // We don't want to cause an endless event loop + callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event))); + } +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +index 88d852c1a729ffd5951da803da424b31591c9f9a..90fdaee8b07df0acf8863103b47a1c68e38a3e4f 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +@@ -336,7 +336,9 @@ public final class JavaPluginLoader implements PluginLoader { + try { + jPlugin.setEnabled(true); + } catch (Throwable ex) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish + server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish + } + + // Perhaps abort here, rather than continue going, but as it stands, +@@ -361,7 +363,9 @@ public final class JavaPluginLoader implements PluginLoader { + try { + jPlugin.setEnabled(false); + } catch (Throwable ex) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish + server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish + } + + if (cloader instanceof PluginClassLoader) { +diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +index e89ab347b908cc92274dd5dd796a02249f899977..7329d2f27f3c2f89bb8f78771872472ec8b6bc22 100644 +--- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +@@ -47,6 +47,8 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm + private java.util.logging.Logger logger; // Paper - add field + private io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup classLoaderGroup; // Paper + ++ private boolean closed = false; // Pufferfish ++ + static { + ClassLoader.registerAsParallelCapable(); + } +@@ -181,6 +183,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm + throw new ClassNotFoundException(name); + } + ++ public boolean _airplane_hasClass(@NotNull String name) { return this.classes.containsKey(name); } // Pufferfish + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (name.startsWith("org.bukkit.") || name.startsWith("net.minecraft.")) { +@@ -188,7 +191,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm + } + Class result = classes.get(name); + +- if (result == null) { ++ if (result == null && !this.closed) { // Pufferfish + String path = name.replace('.', '/').concat(".class"); + JarEntry entry = jar.getJarEntry(path); + +@@ -235,6 +238,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm + this.setClass(name, result); // Paper + } + ++ if (result == null) throw new ClassNotFoundException(name); // Pufferfish + return result; + } + +@@ -249,6 +253,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm + // Paper end + super.close(); + } finally { ++ this.closed = true; // Pufferfish + jar.close(); + } + } diff --git a/patches/api/0002-Purpur-API-Changes.patch b/patches/api/0002-Purpur-API-Changes.patch new file mode 100644 index 0000000..cb32112 --- /dev/null +++ b/patches/api/0002-Purpur-API-Changes.patch @@ -0,0 +1,4092 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Mon, 6 Mar 2023 09:30:48 +0000 +Subject: [PATCH] Purpur API Changes + +Original: PurpurMC +Copyright (C) 2023 PurpurMC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +diff --git a/build.gradle.kts b/build.gradle.kts +index d351ae66db72208a78ee6c522201693f19f68ef5..754c700cbdf0e775d0c12ebe468d9f2bdcc22cc2 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -104,6 +104,8 @@ tasks.jar { + } + + tasks.withType { ++ (options as StandardJavadocDocletOptions).addStringOption("-add-modules", "jdk.incubator.vector") // Purpur - our javadocs need this for pufferfish's SIMD patch ++ (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") // Purpur - silence Paper's bajillion javadoc warnings + val options = options as StandardJavadocDocletOptions + options.overview = "src/main/javadoc/overview.html" + options.use() +diff --git a/src/main/java/co/aikar/timings/TimedEventExecutor.java b/src/main/java/co/aikar/timings/TimedEventExecutor.java +index 34e43e56ccc663e05b9cae36643e8df5eee5cb17..2c15b67149d014fdce2dd74a550013d83b6e44c8 100644 +--- a/src/main/java/co/aikar/timings/TimedEventExecutor.java ++++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java +@@ -76,9 +76,9 @@ public class TimedEventExecutor implements EventExecutor { + executor.execute(listener, event); + return; + } +- try (Timing ignored = timings.startTiming()){ ++ //try (Timing ignored = timings.startTiming()){ // Purpur + executor.execute(listener, event); +- } ++ //} // Purpur + } + + @Override +diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java +index a21e5ead5024fd0058c5e3302d8201dd249d32bc..505908e987a032a801ae03d7fb020dfb662bf81b 100644 +--- a/src/main/java/co/aikar/timings/Timing.java ++++ b/src/main/java/co/aikar/timings/Timing.java +@@ -36,6 +36,7 @@ public interface Timing extends AutoCloseable { + * @return Timing + */ + @NotNull ++ @io.papermc.paper.annotation.DoNotUse // Purpur + Timing startTiming(); + + /** +@@ -43,6 +44,7 @@ public interface Timing extends AutoCloseable { + * + * Will automatically be called when this Timing is used with try-with-resources + */ ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void stopTiming(); + + /** +@@ -53,6 +55,7 @@ public interface Timing extends AutoCloseable { + * @return Timing + */ + @NotNull ++ @io.papermc.paper.annotation.DoNotUse // Purpur + Timing startTimingIfSync(); + + /** +@@ -62,12 +65,14 @@ public interface Timing extends AutoCloseable { + * + * But only if we are on the primary thread. + */ ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void stopTimingIfSync(); + + /** + * @deprecated Doesn't do anything - Removed + */ + @Deprecated ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void abort(); + + /** +@@ -79,5 +84,6 @@ public interface Timing extends AutoCloseable { + TimingHandler getTimingHandler(); + + @Override ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void close(); + } +diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java +index dd72a34eaa4bedd9ea0b92eaa79091b00eb4dd09..759e9cbcedb50894821dcb6dcc602af4f0c3453e 100644 +--- a/src/main/java/co/aikar/timings/Timings.java ++++ b/src/main/java/co/aikar/timings/Timings.java +@@ -114,7 +114,7 @@ public final class Timings { + @NotNull + public static Timing ofStart(@NotNull Plugin plugin, @NotNull String name, @Nullable Timing groupHandler) { + Timing timing = of(plugin, name, groupHandler); +- timing.startTiming(); ++ //timing.startTiming(); // Purpur + return timing; + } + +diff --git a/src/main/java/co/aikar/timings/TimingsCommand.java b/src/main/java/co/aikar/timings/TimingsCommand.java +index 3132dc98d26c54c5e46162e53aaed195d7335c8d..b461b5c50f97f11cb9ef68abc520271bb72440fa 100644 +--- a/src/main/java/co/aikar/timings/TimingsCommand.java ++++ b/src/main/java/co/aikar/timings/TimingsCommand.java +@@ -44,7 +44,7 @@ public class TimingsCommand extends BukkitCommand { + public TimingsCommand(@NotNull String name) { + super(name); + this.description = "Manages Spigot Timings data to see performance of the server."; +- this.usageMessage = "/timings "; ++ this.usageMessage = "/timings";// "; // Purpur + this.setPermission("bukkit.command.timings"); + } + +@@ -53,6 +53,13 @@ public class TimingsCommand extends BukkitCommand { + if (!testPermission(sender)) { + return true; + } ++ if (true) { ++ net.kyori.adventure.text.minimessage.MiniMessage mm = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage(); ++ sender.sendMessage(mm.deserialize("Purpur has removed timings to save your performance. Please use /spark instead")); ++ sender.sendMessage(mm.deserialize("For more information, view its documentation at")); ++ sender.sendMessage(mm.deserialize("https://spark.lucko.me/docs/Command-Usage")); ++ return true; ++ } + if (args.length < 1) { + sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED)); + return true; +@@ -111,7 +118,7 @@ public class TimingsCommand extends BukkitCommand { + Preconditions.checkNotNull(args, "Arguments cannot be null"); + Preconditions.checkNotNull(alias, "Alias cannot be null"); + +- if (args.length == 1) { ++ if (false && args.length == 1) { // Purpur + return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, + new ArrayList(TIMINGS_SUBCOMMANDS.size())); + } +diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java b/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java +index 998f629852e1103767e005405d1f39c2251ecd28..3c05b03bb5ff3bfec6c69a5cc4b23f0633ab473f 100644 +--- a/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java ++++ b/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java +@@ -200,6 +200,18 @@ public interface VanillaGoal extends Goal { + GoalKey CLIMB_ON_TOP_OF_POWDER_SNOW = GoalKey.of(Mob.class, NamespacedKey.minecraft("climb_on_top_of_powder_snow")); + GoalKey WOLF_PANIC = GoalKey.of(Wolf.class, NamespacedKey.minecraft("wolf_panic")); + ++ // Purpur start ++ GoalKey MOB_HAS_RIDER = GoalKey.of(Mob.class, NamespacedKey.minecraft("has_rider")); ++ GoalKey HORSE_HAS_RIDER = GoalKey.of(AbstractHorse.class, NamespacedKey.minecraft("horse_has_rider")); ++ GoalKey LLAMA_HAS_RIDER = GoalKey.of(Llama.class, NamespacedKey.minecraft("llama_has_rider")); ++ GoalKey FIND_CRYSTAL = GoalKey.of(Phantom.class, NamespacedKey.minecraft("find_crystal")); ++ GoalKey ORBIT_CRYSTAL = GoalKey.of(Phantom.class, NamespacedKey.minecraft("orbit_crystal")); ++ GoalKey DROWNED_ATTACK_VILLAGER = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_attack_villager")); ++ GoalKey ZOMBIE_ATTACK_VILLAGER = GoalKey.of(Zombie.class, NamespacedKey.minecraft("zombie_attack_villager")); ++ GoalKey AVOID_RABID_WOLF = GoalKey.of(Wolf.class, NamespacedKey.minecraft("avoid_rabid_wolf")); ++ GoalKey RECEIVE_FLOWER = GoalKey.of(IronGolem.class, NamespacedKey.minecraft("receive_flower")); ++ // Purpur end ++ + /** + * @deprecated removed in 1.16 + */ +diff --git a/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java +index a736d7bcdc5861a01b66ba36158db1c716339346..22fc165fd9c95f0f3ae1be7a0857e48cc50fad5b 100644 +--- a/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java +@@ -26,6 +26,12 @@ public interface VersionFetcher { + @NotNull + Component getVersionMessage(@NotNull String serverVersion); + ++ // Purpur start ++ default int distance() { ++ return 0; ++ } ++ // Purpur end ++ + class DummyVersionFetcher implements VersionFetcher { + + @Override +diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java +index ac9b690fcccb60b587e5345f12f1383afd0a73a1..83571abfe9a2eb8736b481de35dfd7fd4c663f5a 100644 +--- a/src/main/java/org/bukkit/Bukkit.java ++++ b/src/main/java/org/bukkit/Bukkit.java +@@ -2464,4 +2464,127 @@ public final class Bukkit { + public static Server.Spigot spigot() { + return server.spigot(); + } ++ ++ // Purpur start ++ /** ++ * Get the name of this server ++ * @return the name of the server ++ */ ++ @NotNull ++ public static String getServerName() { ++ return server.getServerName(); ++ } ++ ++ /** ++ * Check if server is lagging according to laggy threshold setting ++ * ++ * @return True if lagging ++ */ ++ public static boolean isLagging() { ++ return server.isLagging(); ++ } ++ ++ /** ++ * Add an Item as fuel for furnaces ++ * ++ * @param material The material that will be the fuel ++ * @param burnTime The time (in ticks) this item will burn for ++ */ ++ public static void addFuel(@NotNull Material material, int burnTime) { ++ server.addFuel(material, burnTime); ++ } ++ ++ /** ++ * Remove an item as fuel for furnaces ++ * ++ * @param material The material that will no longer be a fuel ++ */ ++ public static void removeFuel(@NotNull Material material) { ++ server.removeFuel(material); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration) { ++ server.sendBlockHighlight(location, duration); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, int argb) { ++ server.sendBlockHighlight(location, duration, argb); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text) { ++ server.sendBlockHighlight(location, duration, text); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb) { ++ server.sendBlockHighlight(location, duration, text, argb); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency) { ++ server.sendBlockHighlight(location, duration, color, transparency); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency) { ++ server.sendBlockHighlight(location, duration, text, color, transparency); ++ } ++ ++ /** ++ * Clears all debug block highlights for all players on the server. ++ */ ++ public static void clearBlockHighlights() { ++ server.clearBlockHighlights(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/ChatColor.java b/src/main/java/org/bukkit/ChatColor.java +index f6eb30f53dad684f156102cf7147b2f00c82c71e..f1239a2618b08fa92e0e20692d1c3d20d1558502 100644 +--- a/src/main/java/org/bukkit/ChatColor.java ++++ b/src/main/java/org/bukkit/ChatColor.java +@@ -3,6 +3,7 @@ package org.bukkit; + import com.google.common.base.Preconditions; + import com.google.common.collect.Maps; + import java.util.Map; ++import java.util.regex.Matcher; + import java.util.regex.Pattern; + import org.jetbrains.annotations.Contract; + import org.jetbrains.annotations.NotNull; +@@ -413,4 +414,77 @@ public enum ChatColor { + BY_CHAR.put(color.code, color); + } + } ++ ++ // Purpur start ++ /** ++ * Convert legacy string into a string ready to be parsed by MiniMessage ++ * ++ * @param str Legacy string ++ * @return MiniMessage ready string ++ */ ++ @NotNull ++ public static String toMM(@NotNull String str) { ++ StringBuilder sb = new StringBuilder(str); ++ Matcher m = STRIP_COLOR_PATTERN.matcher(sb); ++ while (m.find()) { ++ sb.replace(m.start(), m.end(), sb.substring(m.start(), m.end()).toLowerCase()); ++ } ++ return sb.toString() ++ .replace("&0", "") ++ .replace("&1", "") ++ .replace("&2", "") ++ .replace("&3", "") ++ .replace("&4", "") ++ .replace("&5", "") ++ .replace("&6", "") ++ .replace("&7", "") ++ .replace("&8", "") ++ .replace("&9", "") ++ .replace("&a", "") ++ .replace("&b", "") ++ .replace("&c", "") ++ .replace("&d", "") ++ .replace("&e", "") ++ .replace("&f", "") ++ .replace("&k", "") ++ .replace("&l", "") ++ .replace("&m", "") ++ .replace("&n", "") ++ .replace("&o", "") ++ .replace("&r", ""); ++ } ++ ++ @NotNull ++ public static net.kyori.adventure.text.Component parseMM(@NotNull String string, @Nullable Object... args) { ++ return net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(String.format(string, args)); ++ } ++ ++ @Deprecated ++ public static final Pattern HEX_PATTERN = Pattern.compile("(#[A-Fa-f0-9]{6})"); ++ ++ @Deprecated ++ @Nullable ++ public static String replaceHex(@Nullable String str) { ++ if (str != null) { ++ java.util.regex.Matcher matcher = HEX_PATTERN.matcher(str); ++ while (matcher.find()) { ++ String group = matcher.group(1); ++ str = str.replace(group, net.md_5.bungee.api.ChatColor.of(group).toString()); ++ } ++ } ++ return str; ++ } ++ ++ @Deprecated ++ @Nullable ++ public static String color(@Nullable String str) { ++ return color(str, true); ++ } ++ ++ @Deprecated ++ @Nullable ++ public static String color(@Nullable String str, boolean parseHex) { ++ return str != null ? net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', parseHex ? replaceHex(str) : str) : str; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/Material.java b/src/main/java/org/bukkit/Material.java +index 6bbb47d1f9d8d45326232024e82a0ebaf764fae7..2a22d0639d63a3db4e7bded2d4e91b09bb93a95e 100644 +--- a/src/main/java/org/bukkit/Material.java ++++ b/src/main/java/org/bukkit/Material.java +@@ -10753,4 +10753,40 @@ public enum Material implements Keyed, Translatable, net.kyori.adventure.transla + public String getItemTranslationKey() { + return Bukkit.getUnsafe().getItemTranslationKey(this); + } ++ ++ // Purpur start ++ public boolean isArmor() { ++ switch (this) { ++ // ++ case LEATHER_BOOTS: ++ case LEATHER_CHESTPLATE: ++ case LEATHER_HELMET: ++ case LEATHER_LEGGINGS: ++ case CHAINMAIL_BOOTS: ++ case CHAINMAIL_CHESTPLATE: ++ case CHAINMAIL_HELMET: ++ case CHAINMAIL_LEGGINGS: ++ case IRON_BOOTS: ++ case IRON_CHESTPLATE: ++ case IRON_HELMET: ++ case IRON_LEGGINGS: ++ case GOLDEN_BOOTS: ++ case GOLDEN_CHESTPLATE: ++ case GOLDEN_HELMET: ++ case GOLDEN_LEGGINGS: ++ case DIAMOND_BOOTS: ++ case DIAMOND_CHESTPLATE: ++ case DIAMOND_HELMET: ++ case DIAMOND_LEGGINGS: ++ case NETHERITE_BOOTS: ++ case NETHERITE_CHESTPLATE: ++ case NETHERITE_HELMET: ++ case NETHERITE_LEGGINGS: ++ case TURTLE_HELMET: ++ return true; ++ default: ++ return false; ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/OfflinePlayer.java b/src/main/java/org/bukkit/OfflinePlayer.java +index 69b50eee42e8c52063033705bd23a5ef5231ed83..3578ab0c3a413d56bc39af43b5d3201d20d7d13a 100644 +--- a/src/main/java/org/bukkit/OfflinePlayer.java ++++ b/src/main/java/org/bukkit/OfflinePlayer.java +@@ -455,4 +455,114 @@ public interface OfflinePlayer extends ServerOperator, AnimalTamer, Configuratio + */ + @Nullable + public Location getLastDeathLocation(); ++ ++ // Purpur start - OfflinePlayer API ++ /** ++ * Determines if the OfflinePlayer is allowed to fly via jump key double-tap like ++ * in creative mode. ++ * ++ * @return True if the player is allowed to fly. ++ */ ++ public boolean getAllowFlight(); ++ ++ /** ++ * Sets if the OfflinePlayer is allowed to fly via jump key double-tap like in ++ * creative mode. ++ * ++ * @param flight If flight should be allowed. ++ */ ++ public void setAllowFlight(boolean flight); ++ ++ /** ++ * Checks to see if this player is currently flying or not. ++ * ++ * @return True if the player is flying, else false. ++ */ ++ public boolean isFlying(); ++ ++ /** ++ * Makes this player start or stop flying. ++ * ++ * @param value True to fly. ++ */ ++ public void setFlying(boolean value); ++ ++ /** ++ * Sets the speed at which a client will fly. Negative values indicate ++ * reverse directions. ++ * ++ * @param value The new speed, from -1 to 1. ++ * @throws IllegalArgumentException If new speed is less than -1 or ++ * greater than 1 ++ */ ++ public void setFlySpeed(float value) throws IllegalArgumentException; ++ ++ /** ++ * Sets the speed at which a client will walk. Negative values indicate ++ * reverse directions. ++ * ++ * @param value The new speed, from -1 to 1. ++ * @throws IllegalArgumentException If new speed is less than -1 or ++ * greater than 1 ++ */ ++ public void setWalkSpeed(float value) throws IllegalArgumentException; ++ ++ /** ++ * Gets the current allowed speed that a client can fly. ++ * ++ * @return The current allowed speed, from -1 to 1 ++ */ ++ public float getFlySpeed(); ++ ++ /** ++ * Gets the current allowed speed that a client can walk. ++ * ++ * @return The current allowed speed, from -1 to 1 ++ */ ++ public float getWalkSpeed(); ++ ++ /** ++ * Gets the entity's current position ++ * ++ * @return a new copy of Location containing the position of this offline player ++ */ ++ @Nullable ++ public Location getLocation(); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleport implementation. ++ * ++ * @param destination ++ * @return true if teleportation was successful ++ */ ++ public boolean teleportOffline(@NotNull org.bukkit.Location destination); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleport implementation. ++ * ++ * @param destination ++ * @param cause Teleport cause used if player is online ++ * @return true if teleportation was successful ++ */ ++ public boolean teleportOffline(@NotNull org.bukkit.Location destination, @NotNull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleportAsync implementation. ++ * ++ * @param destination ++ * @return true if teleportation successful ++ */ ++ @NotNull ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleportAsync implementation. ++ * ++ * @param destination ++ * @param cause Teleport cause used if player is online ++ * @return true if teleportation successful ++ */ ++ @NotNull ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination, @NotNull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause); ++ // Purpur end - OfflinePlayer API + } +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index 2204336d8800311b65e894739ab1b27273e7c6f2..e0f69edf603c2ec99bc92b16b18912272cc41bd9 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -1971,6 +1971,18 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + } + // Paper end + ++ // Purpur start ++ @NotNull ++ public org.bukkit.configuration.file.YamlConfiguration getPurpurConfig() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ ++ @NotNull ++ public java.util.Properties getServerProperties() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ // Purpur end ++ + /** + * Sends the component to the player + * +@@ -2139,4 +2151,105 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + */ + @NotNull org.bukkit.potion.PotionBrewer getPotionBrewer(); + // Paper end ++ ++ // Purpur start ++ /** ++ * Get the name of this server ++ * @return the name of the server ++ */ ++ @NotNull ++ String getServerName(); ++ ++ /** ++ * Check if server is lagging according to laggy threshold setting ++ * ++ * @return True if lagging ++ */ ++ boolean isLagging(); ++ ++ /** ++ * Add an Item as fuel for furnaces ++ * ++ * @param material The material that will be the fuel ++ * @param burnTime The time (in ticks) this item will burn for ++ */ ++ public void addFuel(@NotNull Material material, int burnTime); ++ ++ /** ++ * Remove an item as fuel for furnaces ++ * ++ * @param material The material that will no longer be a fuel ++ */ ++ public void removeFuel(@NotNull Material material); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Clears all debug block highlights for all players on the server. ++ */ ++ void clearBlockHighlights(); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index aa04d7df70d99402c91e920fd693d7d4fb655786..a38863ebd363f54994753937a10e041076846af0 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -4010,6 +4010,86 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient + @Nullable + public DragonBattle getEnderDragonBattle(); + ++ // Purpur start ++ /** ++ * Gets the local difficulty (based on inhabited time) at a location ++ * ++ * @param location Location to check ++ * @return The local difficulty ++ */ ++ public float getLocalDifficultyAt(@NotNull Location location); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Clears all debug block highlights for all players on this world. ++ */ ++ void clearBlockHighlights(); ++ // Purpur end ++ + /** + * Represents various map environment types that a world may be + */ +diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java +index ac9a28922f8a556944a4c3649d74c32c622f0cb0..5349f16136d9348c374a7dfe5b89a71dfcb0e66d 100644 +--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java ++++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java +@@ -143,6 +143,19 @@ public class SimpleCommandMap implements CommandMap { + return false; + } + ++ // Purpur start ++ String[] parsedArgs = Arrays.copyOfRange(args, 1, args.length); ++ org.purpurmc.purpur.event.ExecuteCommandEvent event = new org.purpurmc.purpur.event.ExecuteCommandEvent(sender, target, sentCommandLabel, parsedArgs); ++ if (!event.callEvent()) { ++ return true; // cancelled ++ } ++ ++ sender = event.getSender(); ++ target = event.getCommand(); ++ sentCommandLabel = event.getLabel(); ++ parsedArgs = event.getArgs(); ++ // Purpur end ++ + // Paper start - Plugins do weird things to workaround normal registration + if (target.timings == null) { + target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target); +@@ -150,10 +163,10 @@ public class SimpleCommandMap implements CommandMap { + // Paper end + + try { +- try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources ++ //try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources // Purpur + // Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false) +- target.execute(sender, sentCommandLabel, Arrays.copyOfRange(args, 1, args.length)); +- } // target.timings.stopTiming(); // Spigot // Paper ++ target.execute(sender, sentCommandLabel, parsedArgs); // Purpur ++ //} // target.timings.stopTiming(); // Spigot // Paper // Purpur + } catch (CommandException ex) { + server.getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper + //target.timings.stopTiming(); // Spigot // Paper +diff --git a/src/main/java/org/bukkit/command/defaults/VersionCommand.java b/src/main/java/org/bukkit/command/defaults/VersionCommand.java +index e40f017f87d6b6b4770501b106c76dc69ec69abb..eac5830986cd0638950bbb1e6f10a30e246e09a7 100644 +--- a/src/main/java/org/bukkit/command/defaults/VersionCommand.java ++++ b/src/main/java/org/bukkit/command/defaults/VersionCommand.java +@@ -198,7 +198,7 @@ public class VersionCommand extends BukkitCommand { + String version = Bukkit.getVersion(); + // Paper start + if (version.startsWith("null")) { // running from ide? +- setVersionMessage(net.kyori.adventure.text.Component.text("Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.YELLOW)); ++ setVersionMessage(net.kyori.adventure.text.Component.text("* Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.RED)); // Purpur + return; + } + setVersionMessage(getVersionFetcher().getVersionMessage(version)); +@@ -239,9 +239,11 @@ public class VersionCommand extends BukkitCommand { + // Paper start + private void setVersionMessage(final @NotNull net.kyori.adventure.text.Component msg) { + lastCheck = System.currentTimeMillis(); +- final net.kyori.adventure.text.Component message = net.kyori.adventure.text.TextComponent.ofChildren( +- net.kyori.adventure.text.Component.text(Bukkit.getVersionMessage(), net.kyori.adventure.text.format.NamedTextColor.WHITE), +- net.kyori.adventure.text.Component.newline(), ++ // Purpur start ++ int distance = getVersionFetcher().distance(); ++ final net.kyori.adventure.text.Component message = net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.separator(net.kyori.adventure.text.Component.newline()), ++ ChatColor.parseMM("Current: %s%s*", distance == 0 ? "" : distance > 0 ? "" : "", Bukkit.getVersion()), ++ // Purpur end + msg + ); + this.versionMessage = net.kyori.adventure.text.Component.text() +diff --git a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java +index cb9e8b53da300a911f84e15ee9be2261cf1cc340..c8a603bb614f891d8eb43acd3eddd0504816566f 100644 +--- a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java ++++ b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java +@@ -227,6 +227,28 @@ public enum EnchantmentTarget { + public boolean includes(@NotNull Material item) { + return BREAKABLE.includes(item) || (WEARABLE.includes(item) && !item.equals(Material.ELYTRA)) || item.equals(Material.COMPASS); + } ++ // Purpur start ++ }, ++ ++ /** ++ * Allow the Enchantment to be placed on bows and crossbows. ++ */ ++ BOW_AND_CROSSBOW { ++ @Override ++ public boolean includes(@NotNull Material item) { ++ return item.equals(Material.BOW) || item.equals(Material.CROSSBOW); ++ } ++ }, ++ ++ /** ++ * Allow the Enchantment to be placed on shears. ++ */ ++ WEAPON_AND_SHEARS { ++ @Override ++ public boolean includes(@NotNull Material item) { ++ return WEAPON.includes(item) || item.equals(Material.SHEARS); ++ } ++ // Purpur end + }; + + /** +diff --git a/src/main/java/org/bukkit/entity/Endermite.java b/src/main/java/org/bukkit/entity/Endermite.java +index 138d2530de2410f4a9424dabd3e5ce0cd1c1dcd2..10a8d64ad2da0be2c14f34c3e7d1957c6f2883d1 100644 +--- a/src/main/java/org/bukkit/entity/Endermite.java ++++ b/src/main/java/org/bukkit/entity/Endermite.java +@@ -3,25 +3,21 @@ package org.bukkit.entity; + public interface Endermite extends Monster { + + /** +- * Gets whether this Endermite was spawned by a player. ++ * Gets whether this Endermite was spawned by a player from throwing an ender pearl + * +- * An Endermite spawned by a player will be attacked by nearby Enderman. ++ * An Endermite spawned by a player might be attacked by nearby Enderman depending on purpur.yml settings + * + * @return player spawned status +- * @deprecated this functionality no longer exists + */ +- @Deprecated + boolean isPlayerSpawned(); + + /** +- * Sets whether this Endermite was spawned by a player. ++ * Sets whether this Endermite was spawned by a player from throwing an ender pearl + * +- * An Endermite spawned by a player will be attacked by nearby Enderman. ++ * An Endermite spawned by a player might be attacked by nearby Enderman depending on purpur.yml settings + * + * @param playerSpawned player spawned status +- * @deprecated this functionality no longer exists + */ +- @Deprecated + void setPlayerSpawned(boolean playerSpawned); + // Paper start + /** +diff --git a/src/main/java/org/bukkit/entity/Entity.java b/src/main/java/org/bukkit/entity/Entity.java +index 11cf1bb585e2754bda443b776e9fcaf0a6cc289e..649babbfdd495e8c9471c2f6518d2eb9d9568ba4 100644 +--- a/src/main/java/org/bukkit/entity/Entity.java ++++ b/src/main/java/org/bukkit/entity/Entity.java +@@ -927,4 +927,55 @@ public interface Entity extends Metadatable, CommandSender, Nameable, Persistent + */ + boolean wouldCollideUsing(@NotNull BoundingBox boundingBox); + // Paper End - Collision API ++ ++ // Purpur start ++ /** ++ * Get the riding player ++ * ++ * @return Riding player ++ */ ++ @Nullable ++ Player getRider(); ++ ++ /** ++ * Check if entity is being ridden ++ * ++ * @return True if being ridden ++ */ ++ boolean hasRider(); ++ ++ /** ++ * Check if entity is ridable ++ * ++ * @return True if ridable ++ */ ++ boolean isRidable(); ++ ++ /** ++ * Check if entity is ridable in water ++ * ++ * @return True if ridable in water ++ */ ++ boolean isRidableInWater(); ++ ++ /** ++ * Checks if the entity is in daylight ++ * ++ * @return True if in daylight ++ */ ++ boolean isInDaylight(); ++ ++ /** ++ * Checks if the entity is fire immune ++ * ++ * @return True if fire immune ++ */ ++ boolean isImmuneToFire(); ++ ++ /** ++ * Sets if the entity is fire immune ++ * Set this to null to restore the entity type default ++ */ ++ void setImmuneToFire(@Nullable Boolean fireImmune); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/IronGolem.java b/src/main/java/org/bukkit/entity/IronGolem.java +index 655e37cb3a09610a3f3df805d6dcad17d722da62..09fd716c8fc9ea34a1cbf87bcbe22df035422a51 100644 +--- a/src/main/java/org/bukkit/entity/IronGolem.java ++++ b/src/main/java/org/bukkit/entity/IronGolem.java +@@ -19,4 +19,20 @@ public interface IronGolem extends Golem { + * player created, false if you want it to be a natural village golem. + */ + public void setPlayerCreated(boolean playerCreated); ++ ++ // Purpur start ++ /** ++ * Get the player that summoned this iron golem ++ * ++ * @return UUID of summoner ++ */ ++ @org.jetbrains.annotations.Nullable java.util.UUID getSummoner(); ++ ++ /** ++ * Set the player that summoned this iron golem ++ * ++ * @param summoner UUID of summoner ++ */ ++ void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Item.java b/src/main/java/org/bukkit/entity/Item.java +index 58017fce436cdbda255f7172fbdadb726d4b113c..05600fc8bf2a61aca8094029bc4c208a710da952 100644 +--- a/src/main/java/org/bukkit/entity/Item.java ++++ b/src/main/java/org/bukkit/entity/Item.java +@@ -153,4 +153,62 @@ public interface Item extends Entity, io.papermc.paper.entity.Frictional { // Pa + */ + public void setHealth(int health); + // Paper end ++ ++ // Purpur start ++ /** ++ * Set whether or not this item is immune to cactus ++ * ++ * @param immuneToCactus True to make immune to cactus ++ */ ++ void setImmuneToCactus(boolean immuneToCactus); ++ ++ /** ++ * Check if item is immune to cactus ++ * ++ * @return True if immune to cactus ++ */ ++ boolean isImmuneToCactus(); ++ ++ /** ++ * Set whether or not this item is immune to explosions ++ * ++ * @param immuneToExplosion True to make immune to explosions ++ */ ++ void setImmuneToExplosion(boolean immuneToExplosion); ++ ++ /** ++ * Check if item is immune to explosions ++ * ++ * @return True if immune to explosions ++ */ ++ boolean isImmuneToExplosion(); ++ ++ /** ++ * Set whether or not this item is immune to fire ++ * ++ * @param immuneToFire True to make immune to fire ++ */ ++ void setImmuneToFire(boolean immuneToFire); ++ ++ /** ++ * Check if item is immune to fire ++ * ++ * @return True if immune to fire ++ */ ++ boolean isImmuneToFire(); ++ ++ /** ++ * Set whether or not this item is immune to lightning ++ * ++ * @param immuneToLightning True to make immune to lightning ++ */ ++ void setImmuneToLightning(boolean immuneToLightning); ++ ++ /** ++ * Check if item is immune to lightning ++ * ++ * @return True if immune to lightning ++ */ ++ boolean isImmuneToLightning(); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/LivingEntity.java b/src/main/java/org/bukkit/entity/LivingEntity.java +index d045f41d929c6101060caf3a9fb48c8ffc036f16..44246068b51e47342d767f15af6881f29461589c 100644 +--- a/src/main/java/org/bukkit/entity/LivingEntity.java ++++ b/src/main/java/org/bukkit/entity/LivingEntity.java +@@ -1146,4 +1146,41 @@ public interface LivingEntity extends Attributable, Damageable, ProjectileSource + */ + void setBodyYaw(float bodyYaw); + // Paper end ++ ++ // Purpur start ++ /** ++ * Gets the distance (in blocks) this entity can safely fall without taking damage ++ * ++ * @return Safe fall distance ++ */ ++ float getSafeFallDistance(); ++ ++ /** ++ * Set the distance (in blocks) this entity can safely fall without taking damage ++ * ++ * @param safeFallDistance Safe fall distance ++ */ ++ void setSafeFallDistance(float safeFallDistance); ++ ++ /** ++ * Play item break animation for the item in specified equipment slot ++ * ++ * @param slot Equipment slot to play break animation for ++ */ ++ void broadcastItemBreak(@NotNull org.bukkit.inventory.EquipmentSlot slot); ++ ++ /** ++ * If this mob will burn in the sunlight ++ * ++ * @return True if mob will burn in sunlight ++ */ ++ boolean shouldBurnInDay(); ++ ++ /** ++ * Set if this mob should burn in the sunlight ++ * ++ * @param shouldBurnInDay True to burn in sunlight ++ */ ++ void setShouldBurnInDay(boolean shouldBurnInDay); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Llama.java b/src/main/java/org/bukkit/entity/Llama.java +index bc84b892cae5fe7019a3ad481e9da79956efa1fe..48eb5b00c460cccde29d327cef1d63fc04d6a829 100644 +--- a/src/main/java/org/bukkit/entity/Llama.java ++++ b/src/main/java/org/bukkit/entity/Llama.java +@@ -119,4 +119,20 @@ public interface Llama extends ChestedHorse, RangedEntity { // Paper + @org.jetbrains.annotations.Nullable + Llama getCaravanTail(); + // Paper end ++ ++ // Purpur start ++ /** ++ * Check if this Llama should attempt to join a caravan ++ * ++ * @return True if Llama is allowed to join a caravan ++ */ ++ boolean shouldJoinCaravan(); ++ ++ /** ++ * Set if this Llama should attempt to join a caravan ++ * ++ * @param shouldJoinCaravan True to allow joining a caravan ++ */ ++ void setShouldJoinCaravan(boolean shouldJoinCaravan); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java +index 7e7a21b21afdf40d7516ce1a5f5c1c5d6048984a..5ff9f0d9bc2bc08af251a677a8c98d6f288da8a8 100644 +--- a/src/main/java/org/bukkit/entity/Player.java ++++ b/src/main/java/org/bukkit/entity/Player.java +@@ -2978,4 +2978,139 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM + @Override + Spigot spigot(); + // Spigot end ++ ++ // Purpur start ++ /** ++ * Allows you to get if player uses Purpur Client ++ * ++ * @return True if Player uses Purpur Client ++ */ ++ public boolean usesPurpurClient(); ++ ++ /** ++ * Check if player is AFK ++ * ++ * @return True if AFK ++ */ ++ boolean isAfk(); ++ ++ /** ++ * Set player as AFK ++ * ++ * @param setAfk Whether to set AFK or not ++ */ ++ void setAfk(boolean setAfk); ++ ++ /** ++ * Reset the idle timer back to 0 ++ */ ++ void resetIdleTimer(); ++ ++ /** ++ * Check if player is invulnerable from recently spawning or accepting a resource pack ++ * ++ * @return True if invulnerable ++ */ ++ boolean isSpawnInvulnerable(); ++ ++ /** ++ * Get invulnerable ticks remaining ++ * ++ * @return Invulnerable ticks ++ */ ++ int getSpawnInvulnerableTicks(); ++ ++ /** ++ * Set invulnerable ticks remaining ++ * ++ * @param invulnerableTicks Invulnerable ticks remaining ++ */ ++ void setSpawnInvulnerableTicks(int invulnerableTicks); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Clears all debug block highlights ++ */ ++ void clearBlockHighlights(); ++ ++ /** ++ * Sends a player the death screen with a specified death message. ++ * ++ * @param message The death message to show the player ++ */ ++ void sendDeathScreen(@NotNull Component message); ++ ++ /** ++ * Sends a player the death screen with a specified death message, ++ * along with the entity that caused the death. ++ * ++ * @param message The death message to show the player ++ * @param killer The entity that killed the player ++ */ ++ void sendDeathScreen(@NotNull Component message, @Nullable Entity killer); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Snowman.java b/src/main/java/org/bukkit/entity/Snowman.java +index 10f8f6d45ae9280651c3ebddd1f90acbd7d6ff29..34f9d1b5d66ca96c71a94ebc981752e40019e575 100644 +--- a/src/main/java/org/bukkit/entity/Snowman.java ++++ b/src/main/java/org/bukkit/entity/Snowman.java +@@ -23,4 +23,20 @@ public interface Snowman extends Golem, RangedEntity { // Paper + * @param derpMode True to remove the pumpkin, false to add a pumpkin + */ + void setDerp(boolean derpMode); ++ ++ // Purpur start ++ /** ++ * Get the player that summoned this snowman ++ * ++ * @return UUID of summoner ++ */ ++ @org.jetbrains.annotations.Nullable java.util.UUID getSummoner(); ++ ++ /** ++ * Set the player that summoned this snowman ++ * ++ * @param summoner UUID of summoner ++ */ ++ void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Villager.java b/src/main/java/org/bukkit/entity/Villager.java +index c61e7e41aeb3d4f5f4ac47da8890051d8e97340d..12b08318f78c8144cc809dbccf0feabdd31f0ee2 100644 +--- a/src/main/java/org/bukkit/entity/Villager.java ++++ b/src/main/java/org/bukkit/entity/Villager.java +@@ -328,4 +328,14 @@ public interface Villager extends AbstractVillager { + */ + public void clearReputations(); + // Paper end ++ ++ // Purpur start ++ ++ /** ++ * Check if villager is currently lobotomized ++ * ++ * @return True if lobotomized ++ */ ++ boolean isLobotomized(); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Wither.java b/src/main/java/org/bukkit/entity/Wither.java +index a1b42ae35dda2da90ba00a2d6666514f7c5b11dd..3ccd61bf91d7746393589b0b35674361c2f1d133 100644 +--- a/src/main/java/org/bukkit/entity/Wither.java ++++ b/src/main/java/org/bukkit/entity/Wither.java +@@ -79,4 +79,20 @@ public interface Wither extends Monster, Boss, com.destroystokyo.paper.entity.Ra + */ + void setCanTravelThroughPortals(boolean value); + // Paper end ++ ++ // Purpur start ++ /** ++ * Get the player that summoned this wither ++ * ++ * @return UUID of summoner ++ */ ++ @org.jetbrains.annotations.Nullable java.util.UUID getSummoner(); ++ ++ /** ++ * Set the player that summoned this wither ++ * ++ * @param summoner UUID of summoner ++ */ ++ void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Wolf.java b/src/main/java/org/bukkit/entity/Wolf.java +index 84db38388bf7a58e66d6cd29620b4fe64b0a897e..82ebd99549ce9f9e6427a50cef424e9007735708 100644 +--- a/src/main/java/org/bukkit/entity/Wolf.java ++++ b/src/main/java/org/bukkit/entity/Wolf.java +@@ -69,4 +69,20 @@ public interface Wolf extends Tameable, Sittable, io.papermc.paper.entity.Collar + * @param interested Whether the wolf is interested + */ + public void setInterested(boolean interested); ++ ++ // Purpur start ++ /** ++ * Checks if this wolf is rabid ++ * ++ * @return whether the wolf is rabid ++ */ ++ public boolean isRabid(); ++ ++ /** ++ * Sets this wolf to be rabid or not ++ * ++ * @param rabid whether the wolf should be rabid ++ */ ++ public void setRabid(boolean rabid); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java +index 01c5e8b71338fbb4b1605e45bf2a2e705188f6b5..118d53ec9d1dc9c01cedfbedaf0b8edcbda7b3a5 100644 +--- a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java ++++ b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java +@@ -217,6 +217,12 @@ public class EntityPotionEffectEvent extends EntityEvent implements Cancellable + * When all effects are removed due to a bucket of milk. + */ + MILK, ++ // Purpur start ++ /** ++ * When a player wears full netherite armor ++ */ ++ NETHERITE_ARMOR, ++ // Purpur end + /** + * When a player gets bad omen after killing a patrol captain. + */ +diff --git a/src/main/java/org/bukkit/event/inventory/InventoryType.java b/src/main/java/org/bukkit/event/inventory/InventoryType.java +index 8d7ad84c2bdafa8c8a385fe31acb887a883194ff..11230fb25270b06700efc61954556bfcd0135699 100644 +--- a/src/main/java/org/bukkit/event/inventory/InventoryType.java ++++ b/src/main/java/org/bukkit/event/inventory/InventoryType.java +@@ -141,7 +141,7 @@ public enum InventoryType { + CHISELED_BOOKSHELF(6, "Chiseled Bookshelf"), + ; + +- private final int size; ++ private int size; public void setDefaultSize(int size) { this.size = size; } // Purpur - remove file and add setter + private final String title; + private final boolean isCreatable; + +diff --git a/src/main/java/org/bukkit/inventory/AnvilInventory.java b/src/main/java/org/bukkit/inventory/AnvilInventory.java +index c60be4fd24c7fdf65251dd6169e5e1ac3b588d95..569deccd2f1cf21da9b5906433ac493c1f2081be 100644 +--- a/src/main/java/org/bukkit/inventory/AnvilInventory.java ++++ b/src/main/java/org/bukkit/inventory/AnvilInventory.java +@@ -123,4 +123,14 @@ public interface AnvilInventory extends Inventory { + setItem(2, result); + } + // Paper end ++ ++ // Purpur start ++ boolean canBypassCost(); ++ ++ void setBypassCost(boolean bypassCost); ++ ++ boolean canDoUnsafeEnchants(); ++ ++ void setDoUnsafeEnchants(boolean canDoUnsafeEnchants); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/inventory/ItemStack.java b/src/main/java/org/bukkit/inventory/ItemStack.java +index 449d6e1995eedbfaeffdc5d1f1c2295378006aa8..11261c659e3a378f468f4a19e2c24c1bb1f95a2b 100644 +--- a/src/main/java/org/bukkit/inventory/ItemStack.java ++++ b/src/main/java/org/bukkit/inventory/ItemStack.java +@@ -17,6 +17,18 @@ import org.bukkit.inventory.meta.ItemMeta; + import org.bukkit.material.MaterialData; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; ++// Purpur start ++import com.google.common.collect.Multimap; ++import java.util.Collection; ++import org.bukkit.attribute.Attribute; ++import org.bukkit.attribute.AttributeModifier; ++import org.bukkit.block.data.BlockData; ++import org.bukkit.inventory.meta.BlockDataMeta; ++import org.bukkit.inventory.meta.Repairable; ++import org.bukkit.persistence.PersistentDataContainer; ++import org.bukkit.persistence.PersistentDataHolder; ++import com.destroystokyo.paper.Namespaced; ++// Purpur end + + /** + * Represents a stack of items. +@@ -986,4 +998,626 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat + return livingEntity.damageItemStack(this, amount); + } + // Paper end ++ ++ // Purpur start ++ /** ++ * Gets the display name that is set. ++ *

++ * Plugins should check that hasDisplayName() returns true ++ * before calling this method. ++ * ++ * @return the display name that is set ++ */ ++ @NotNull ++ public String getDisplayName() { ++ return getItemMeta().getDisplayName(); ++ } ++ ++ /** ++ * Sets the display name. ++ * ++ * @param name the name to set ++ */ ++ public void setDisplayName(@Nullable String name) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setDisplayName(name); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for existence of a display name. ++ * ++ * @return true if this has a display name ++ */ ++ public boolean hasDisplayName() { ++ return hasItemMeta() && getItemMeta().hasDisplayName(); ++ } ++ ++ /** ++ * Gets the localized display name that is set. ++ *

++ * Plugins should check that hasLocalizedName() returns true ++ * before calling this method. ++ * ++ * @return the localized name that is set ++ */ ++ @NotNull ++ public String getLocalizedName() { ++ return getItemMeta().getLocalizedName(); ++ } ++ ++ /** ++ * Sets the localized name. ++ * ++ * @param name the name to set ++ */ ++ public void setLocalizedName(@Nullable String name) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setLocalizedName(name); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for existence of a localized name. ++ * ++ * @return true if this has a localized name ++ */ ++ public boolean hasLocalizedName() { ++ return hasItemMeta() && getItemMeta().hasLocalizedName(); ++ } ++ ++ /** ++ * Checks for existence of lore. ++ * ++ * @return true if this has lore ++ */ ++ public boolean hasLore() { ++ return hasItemMeta() && getItemMeta().hasLore(); ++ } ++ ++ /** ++ * Checks for existence of the specified enchantment. ++ * ++ * @param ench enchantment to check ++ * @return true if this enchantment exists for this meta ++ */ ++ public boolean hasEnchant(@NotNull Enchantment ench) { ++ return hasItemMeta() && getItemMeta().hasEnchant(ench); ++ } ++ ++ /** ++ * Checks for the level of the specified enchantment. ++ * ++ * @param ench enchantment to check ++ * @return The level that the specified enchantment has, or 0 if none ++ */ ++ public int getEnchantLevel(@NotNull Enchantment ench) { ++ return getItemMeta().getEnchantLevel(ench); ++ } ++ ++ /** ++ * Returns a copy the enchantments in this ItemMeta.
++ * Returns an empty map if none. ++ * ++ * @return An immutable copy of the enchantments ++ */ ++ @NotNull ++ public Map getEnchants() { ++ return getItemMeta().getEnchants(); ++ } ++ ++ /** ++ * Adds the specified enchantment to this item meta. ++ * ++ * @param ench Enchantment to add ++ * @param level Level for the enchantment ++ * @param ignoreLevelRestriction this indicates the enchantment should be ++ * applied, ignoring the level limit ++ * @return true if the item meta changed as a result of this call, false ++ * otherwise ++ */ ++ public boolean addEnchant(@NotNull Enchantment ench, int level, boolean ignoreLevelRestriction) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.addEnchant(ench, level, ignoreLevelRestriction); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Removes the specified enchantment from this item meta. ++ * ++ * @param ench Enchantment to remove ++ * @return true if the item meta changed as a result of this call, false ++ * otherwise ++ */ ++ public boolean removeEnchant(@NotNull Enchantment ench) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeEnchant(ench); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Checks for the existence of any enchantments. ++ * ++ * @return true if an enchantment exists on this meta ++ */ ++ public boolean hasEnchants() { ++ return hasItemMeta() && getItemMeta().hasEnchants(); ++ } ++ ++ /** ++ * Checks if the specified enchantment conflicts with any enchantments in ++ * this ItemMeta. ++ * ++ * @param ench enchantment to test ++ * @return true if the enchantment conflicts, false otherwise ++ */ ++ public boolean hasConflictingEnchant(@NotNull Enchantment ench) { ++ return hasItemMeta() && getItemMeta().hasConflictingEnchant(ench); ++ } ++ ++ /** ++ * Sets the custom model data. ++ *

++ * CustomModelData is an integer that may be associated client side with a ++ * custom item model. ++ * ++ * @param data the data to set, or null to clear ++ */ ++ public void setCustomModelData(@Nullable Integer data) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setCustomModelData(data); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the custom model data that is set. ++ *

++ * CustomModelData is an integer that may be associated client side with a ++ * custom item model. ++ *

++ * Plugins should check that hasCustomModelData() returns true ++ * before calling this method. ++ * ++ * @return the localized name that is set ++ */ ++ public int getCustomModelData() { ++ return getItemMeta().getCustomModelData(); ++ } ++ ++ /** ++ * Checks for existence of custom model data. ++ *

++ * CustomModelData is an integer that may be associated client side with a ++ * custom item model. ++ * ++ * @return true if this has custom model data ++ */ ++ public boolean hasCustomModelData() { ++ return hasItemMeta() && getItemMeta().hasCustomModelData(); ++ } ++ ++ /** ++ * Returns whether the item has block data currently attached to it. ++ * ++ * @return whether block data is already attached ++ */ ++ public boolean hasBlockData() { ++ return hasItemMeta() && ((BlockDataMeta) getItemMeta()).hasBlockData(); ++ } ++ ++ /** ++ * Returns the currently attached block data for this item or creates a new ++ * one if one doesn't exist. ++ * ++ * The state is a copy, it must be set back (or to another item) with ++ * {@link #setBlockData(BlockData)} ++ * ++ * @param material the material we wish to get this data in the context of ++ * @return the attached data or new data ++ */ ++ @NotNull ++ public BlockData getBlockData(@NotNull Material material) { ++ return ((BlockDataMeta) getItemMeta()).getBlockData(material); ++ } ++ ++ /** ++ * Attaches a copy of the passed block data to the item. ++ * ++ * @param blockData the block data to attach to the block. ++ * @throws IllegalArgumentException if the blockData is null or invalid for ++ * this item. ++ */ ++ public void setBlockData(@NotNull BlockData blockData) { ++ ItemMeta itemMeta = getItemMeta(); ++ ((BlockDataMeta) itemMeta).setBlockData(blockData); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the repair penalty ++ * ++ * @return the repair penalty ++ */ ++ public int getRepairCost() { ++ return ((Repairable) getItemMeta()).getRepairCost(); ++ } ++ ++ /** ++ * Sets the repair penalty ++ * ++ * @param cost repair penalty ++ */ ++ public void setRepairCost(int cost) { ++ ItemMeta itemMeta = getItemMeta(); ++ ((Repairable) itemMeta).setRepairCost(cost); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks to see if this has a repair penalty ++ * ++ * @return true if this has a repair penalty ++ */ ++ public boolean hasRepairCost() { ++ return hasItemMeta() && ((Repairable) getItemMeta()).hasRepairCost(); ++ } ++ ++ /** ++ * Return if the unbreakable tag is true. An unbreakable item will not lose ++ * durability. ++ * ++ * @return true if the unbreakable tag is true ++ */ ++ public boolean isUnbreakable() { ++ return hasItemMeta() && getItemMeta().isUnbreakable(); ++ } ++ ++ /** ++ * Sets the unbreakable tag. An unbreakable item will not lose durability. ++ * ++ * @param unbreakable true if set unbreakable ++ */ ++ public void setUnbreakable(boolean unbreakable) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setUnbreakable(unbreakable); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for the existence of any AttributeModifiers. ++ * ++ * @return true if any AttributeModifiers exist ++ */ ++ public boolean hasAttributeModifiers() { ++ return hasItemMeta() && getItemMeta().hasAttributeModifiers(); ++ } ++ ++ /** ++ * Return an immutable copy of all Attributes and ++ * their modifiers in this ItemMeta.
++ * Returns null if none exist. ++ * ++ * @return an immutable {@link Multimap} of Attributes ++ * and their AttributeModifiers, or null if none exist ++ */ ++ @Nullable ++ public Multimap getAttributeModifiers() { ++ return getItemMeta().getAttributeModifiers(); ++ } ++ ++ /** ++ * Return an immutable copy of all {@link Attribute}s and their ++ * {@link AttributeModifier}s for a given {@link EquipmentSlot}.
++ * Any {@link AttributeModifier} that does have have a given ++ * {@link EquipmentSlot} will be returned. This is because ++ * AttributeModifiers without a slot are active in any slot.
++ * If there are no attributes set for the given slot, an empty map ++ * will be returned. ++ * ++ * @param slot the {@link EquipmentSlot} to check ++ * @return the immutable {@link Multimap} with the ++ * respective Attributes and modifiers, or an empty map ++ * if no attributes are set. ++ */ ++ @NotNull ++ public Multimap getAttributeModifiers(@Nullable EquipmentSlot slot) { ++ return getItemMeta().getAttributeModifiers(slot); ++ } ++ ++ /** ++ * Return an immutable copy of all {@link AttributeModifier}s ++ * for a given {@link Attribute} ++ * ++ * @param attribute the {@link Attribute} ++ * @return an immutable collection of {@link AttributeModifier}s ++ * or null if no AttributeModifiers exist for the Attribute. ++ * @throws NullPointerException if Attribute is null ++ */ ++ @Nullable ++ public Collection getAttributeModifiers(@NotNull Attribute attribute) { ++ return getItemMeta().getAttributeModifiers(attribute); ++ } ++ ++ /** ++ * Add an Attribute and it's Modifier. ++ * AttributeModifiers can now support {@link EquipmentSlot}s. ++ * If not set, the {@link AttributeModifier} will be active in ALL slots. ++ *
++ * Two {@link AttributeModifier}s that have the same {@link java.util.UUID} ++ * cannot exist on the same Attribute. ++ * ++ * @param attribute the {@link Attribute} to modify ++ * @param modifier the {@link AttributeModifier} specifying the modification ++ * @return true if the Attribute and AttributeModifier were ++ * successfully added ++ * @throws NullPointerException if Attribute is null ++ * @throws NullPointerException if AttributeModifier is null ++ * @throws IllegalArgumentException if AttributeModifier already exists ++ */ ++ public boolean addAttributeModifier(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.addAttributeModifier(attribute, modifier); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Set all {@link Attribute}s and their {@link AttributeModifier}s. ++ * To clear all currently set Attributes and AttributeModifiers use ++ * null or an empty Multimap. ++ * If not null nor empty, this will filter all entries that are not-null ++ * and add them to the ItemStack. ++ * ++ * @param attributeModifiers the new Multimap containing the Attributes ++ * and their AttributeModifiers ++ */ ++ public void setAttributeModifiers(@Nullable Multimap attributeModifiers) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setAttributeModifiers(attributeModifiers); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Remove all {@link AttributeModifier}s associated with the given ++ * {@link Attribute}. ++ * This will return false if nothing was removed. ++ * ++ * @param attribute attribute to remove ++ * @return true if all modifiers were removed from a given ++ * Attribute. Returns false if no attributes were ++ * removed. ++ * @throws NullPointerException if Attribute is null ++ */ ++ public boolean removeAttributeModifier(@NotNull Attribute attribute) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeAttributeModifier(attribute); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Remove all {@link Attribute}s and {@link AttributeModifier}s for a ++ * given {@link EquipmentSlot}.
++ * If the given {@link EquipmentSlot} is null, this will remove all ++ * {@link AttributeModifier}s that do not have an EquipmentSlot set. ++ * ++ * @param slot the {@link EquipmentSlot} to clear all Attributes and ++ * their modifiers for ++ * @return true if all modifiers were removed that match the given ++ * EquipmentSlot. ++ */ ++ public boolean removeAttributeModifier(@Nullable EquipmentSlot slot) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeAttributeModifier(slot); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Remove a specific {@link Attribute} and {@link AttributeModifier}. ++ * AttributeModifiers are matched according to their {@link java.util.UUID}. ++ * ++ * @param attribute the {@link Attribute} to remove ++ * @param modifier the {@link AttributeModifier} to remove ++ * @return if any attribute modifiers were remove ++ * ++ * @throws NullPointerException if the Attribute is null ++ * @throws NullPointerException if the AttributeModifier is null ++ * ++ * @see AttributeModifier#getUniqueId() ++ */ ++ public boolean removeAttributeModifier(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeAttributeModifier(attribute, modifier); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Returns a custom tag container capable of storing tags on the object. ++ * ++ * Note that the tags stored on this container are all stored under their ++ * own custom namespace therefore modifying default tags using this ++ * {@link PersistentDataHolder} is impossible. ++ * ++ * @return the persistent metadata container ++ */ ++ @NotNull ++ public PersistentDataContainer getPersistentDataContainer() { ++ return getItemMeta().getPersistentDataContainer(); ++ } ++ ++ /** ++ * Checks to see if this item has damage ++ * ++ * @return true if this has damage ++ */ ++ public boolean hasDamage() { ++ return hasItemMeta() && ((Damageable) getItemMeta()).hasDamage(); ++ } ++ ++ /** ++ * Gets the damage ++ * ++ * @return the damage ++ */ ++ public int getDamage() { ++ return ((Damageable) getItemMeta()).getDamage(); ++ } ++ ++ /** ++ * Sets the damage ++ * ++ * @param damage item damage ++ */ ++ public void setDamage(int damage) { ++ ItemMeta itemMeta = getItemMeta(); ++ ((Damageable) itemMeta).setDamage(damage); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the collection of namespaced keys that the item can destroy in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @return Set of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ @NotNull ++ public java.util.Set getDestroyableKeys() { ++ return getItemMeta().getDestroyableKeys(); ++ } ++ ++ /** ++ * Sets the collection of namespaced keys that the item can destroy in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @param canDestroy Collection of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ public void setDestroyableKeys(@NotNull Collection canDestroy) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setDestroyableKeys(canDestroy); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the collection of namespaced keys that the item can be placed on in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @return Set of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ @NotNull ++ public java.util.Set getPlaceableKeys() { ++ return getItemMeta().getPlaceableKeys(); ++ } ++ ++ /** ++ * Sets the set of namespaced keys that the item can be placed on in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @param canPlaceOn Collection of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ @NotNull ++ public void setPlaceableKeys(@NotNull Collection canPlaceOn) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setPlaceableKeys(canPlaceOn); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for the existence of any keys that the item can be placed on ++ * ++ * @return true if this item has placeable keys ++ */ ++ public boolean hasPlaceableKeys() { ++ return hasItemMeta() && getItemMeta().hasPlaceableKeys(); ++ } ++ ++ /** ++ * Checks for the existence of any keys that the item can destroy ++ * ++ * @return true if this item has destroyable keys ++ */ ++ public boolean hasDestroyableKeys() { ++ return hasItemMeta() && getItemMeta().hasDestroyableKeys(); ++ } ++ ++ /** ++ * Repairs this item by 1 durability ++ */ ++ public void repair() { ++ repair(1); ++ } ++ ++ /** ++ * Damages this item by 1 durability ++ * ++ * @return True if damage broke the item ++ */ ++ public boolean damage() { ++ return damage(1); ++ } ++ ++ /** ++ * Repairs this item's durability by amount ++ * ++ * @param amount Amount of durability to repair ++ */ ++ public void repair(int amount) { ++ damage(-amount); ++ } ++ ++ /** ++ * Damages this item's durability by amount ++ * ++ * @param amount Amount of durability to damage ++ * @return True if damage broke the item ++ */ ++ public boolean damage(int amount) { ++ return damage(amount, false); ++ } ++ ++ /** ++ * Damages this item's durability by amount ++ * ++ * @param amount Amount of durability to damage ++ * @param ignoreUnbreaking Ignores unbreaking enchantment ++ * @return True if damage broke the item ++ */ ++ public boolean damage(int amount, boolean ignoreUnbreaking) { ++ Damageable damageable = (Damageable) getItemMeta(); ++ if (amount > 0) { ++ int unbreaking = getEnchantLevel(Enchantment.DURABILITY); ++ int reduce = 0; ++ for (int i = 0; unbreaking > 0 && i < amount; ++i) { ++ if (reduceDamage(java.util.concurrent.ThreadLocalRandom.current(), unbreaking)) { ++ ++reduce; ++ } ++ } ++ amount -= reduce; ++ if (amount <= 0) { ++ return isBroke(damageable.getDamage()); ++ } ++ } ++ int damage = damageable.getDamage() + amount; ++ damageable.setDamage(damage); ++ setItemMeta((ItemMeta) damageable); ++ return isBroke(damage); ++ } ++ ++ public boolean isBroke(int damage) { ++ if (damage > getType().getMaxDurability()) { ++ if (getAmount() > 0) { ++ // ensure it "breaks" ++ setAmount(0); ++ } ++ return true; ++ } ++ return false; ++ } ++ ++ private boolean reduceDamage(java.util.Random random, int unbreaking) { ++ if (getType().isArmor()) { ++ return random.nextFloat() < 0.6F; ++ } ++ return random.nextInt(unbreaking + 1) > 0; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/inventory/RecipeChoice.java b/src/main/java/org/bukkit/inventory/RecipeChoice.java +index 90208bc96085f05a3b657b9467b1670d00b03104..c59d5e4ef9641fd73463b177239226866272745b 100644 +--- a/src/main/java/org/bukkit/inventory/RecipeChoice.java ++++ b/src/main/java/org/bukkit/inventory/RecipeChoice.java +@@ -10,6 +10,7 @@ import java.util.function.Predicate; + import org.bukkit.Material; + import org.bukkit.Tag; + import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; // Purpur + + /** + * Represents a potential item match within a recipe. All choices within a +@@ -152,6 +153,7 @@ public interface RecipeChoice extends Predicate, Cloneable { + public static class ExactChoice implements RecipeChoice { + + private List choices; ++ private Predicate predicate; // Purpur + + public ExactChoice(@NotNull ItemStack stack) { + this(Arrays.asList(stack)); +@@ -196,6 +198,7 @@ public interface RecipeChoice extends Predicate, Cloneable { + + @Override + public boolean test(@NotNull ItemStack t) { ++ if (predicate != null) return predicate.test(t); // Purpur + for (ItemStack match : choices) { + if (t.isSimilar(match)) { + return true; +@@ -205,6 +208,17 @@ public interface RecipeChoice extends Predicate, Cloneable { + return false; + } + ++ // Purpur start ++ @Nullable ++ public Predicate getPredicate() { ++ return predicate; ++ } ++ ++ public void setPredicate(@Nullable Predicate predicate) { ++ this.predicate = predicate; ++ } ++ // Purpur end ++ + @Override + public int hashCode() { + int hash = 7; +diff --git a/src/main/java/org/bukkit/permissions/PermissibleBase.java b/src/main/java/org/bukkit/permissions/PermissibleBase.java +index cd3296fea01648592d2af89b3d80135acb6d0958..45797a6fbae1d8edc4211cb30def24ad4f59bd49 100644 +--- a/src/main/java/org/bukkit/permissions/PermissibleBase.java ++++ b/src/main/java/org/bukkit/permissions/PermissibleBase.java +@@ -168,7 +168,7 @@ public class PermissibleBase implements Permissible { + + for (Permission perm : defaults) { + String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH); +- permissions.put(name, new PermissionAttachmentInfo(parent, name, null, true)); ++ permissions.put(name, new PermissionAttachmentInfo(parent, name, null, perm.getDefault().getValue(isOp()))); // Purpur + Bukkit.getServer().getPluginManager().subscribeToPermission(name, parent); + calculateChildPermissions(perm.getChildren(), false, null); + } +@@ -196,7 +196,7 @@ public class PermissibleBase implements Permissible { + String name = entry.getKey(); + + Permission perm = Bukkit.getServer().getPluginManager().getPermission(name); +- boolean value = entry.getValue() ^ invert; ++ boolean value = (entry.getValue() == null && perm != null ? perm.getDefault().getValue(isOp()) : entry.getValue()) ^ invert; // Purpur + String lname = name.toLowerCase(java.util.Locale.ENGLISH); + + permissions.put(lname, new PermissionAttachmentInfo(parent, lname, attachment, value)); +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +index 90fdaee8b07df0acf8863103b47a1c68e38a3e4f..6319f6c1428d0e272681984c1b15ec05a3272423 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +@@ -55,6 +55,7 @@ public final class JavaPluginLoader implements PluginLoader { + private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")}; + private final List loaders = new CopyOnWriteArrayList(); + private final LibraryLoader libraryLoader; ++ public static boolean SuppressLibraryLoaderLogger = false; // Purpur + + /** + * This class was not meant to be constructed explicitly +diff --git a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java +index e4b6f278a811acbb0070e311c5c3bdaff7b00474..ee83ecb054099cb85168a9499dfe967a0a9ec796 100644 +--- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java +@@ -65,6 +65,7 @@ public class LibraryLoader + @Override + public void transferStarted(@NotNull TransferEvent event) throws TransferCancelledException + { ++ if (!JavaPluginLoader.SuppressLibraryLoaderLogger) // Purpur + logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() ); + } + } ); +@@ -80,6 +81,7 @@ public class LibraryLoader + { + return null; + } ++ if (!JavaPluginLoader.SuppressLibraryLoaderLogger) // Purpur + logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[] + { + java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), desc.getLibraries().size() // Paper - use configured log prefix +@@ -118,6 +120,7 @@ public class LibraryLoader + } + + jarFiles.add( url ); ++ if (!JavaPluginLoader.SuppressLibraryLoaderLogger) // Purpur + logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[] + { + java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), file // Paper - use configured log prefix +diff --git a/src/main/java/org/bukkit/potion/PotionEffect.java b/src/main/java/org/bukkit/potion/PotionEffect.java +index 24e36cdf580da885ac64002673a786b9c5a3f787..d20cc4d4f5b37a3de9cb3cf47af7a908e9dbc2fc 100644 +--- a/src/main/java/org/bukkit/potion/PotionEffect.java ++++ b/src/main/java/org/bukkit/potion/PotionEffect.java +@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap; + import java.util.Map; + import java.util.NoSuchElementException; + import org.bukkit.Color; ++import org.bukkit.NamespacedKey; + import org.bukkit.configuration.serialization.ConfigurationSerializable; + import org.bukkit.configuration.serialization.SerializableAs; + import org.bukkit.entity.LivingEntity; +@@ -26,12 +27,14 @@ public class PotionEffect implements ConfigurationSerializable { + private static final String AMBIENT = "ambient"; + private static final String PARTICLES = "has-particles"; + private static final String ICON = "has-icon"; ++ private static final String KEY = "namespacedKey"; // Purpur + private final int amplifier; + private final int duration; + private final PotionEffectType type; + private final boolean ambient; + private final boolean particles; + private final boolean icon; ++ @Nullable private final NamespacedKey key; // Purpur + + /** + * Creates a potion effect. +@@ -44,6 +47,36 @@ public class PotionEffect implements ConfigurationSerializable { + * @param icon the icon status, see {@link PotionEffect#hasIcon()} + */ + public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, boolean icon) { ++ // Purpur start ++ this(type, duration, amplifier, ambient, particles, icon, null); ++ } ++ ++ /** ++ * Create a potion effect. ++ * @param duration measured in ticks, see {@link ++ * PotionEffect#getDuration()} ++ * @param amplifier the amplifier, see {@link PotionEffect#getAmplifier()} ++ * @param ambient the ambient status, see {@link PotionEffect#isAmbient()} ++ * @param particles the particle status, see {@link PotionEffect#hasParticles()} ++ * @param key the namespacedKey, see {@link PotionEffect#getKey()} ++ */ ++ public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, @Nullable NamespacedKey key) { ++ this(type, duration, amplifier, ambient, particles, particles, key); ++ } ++ ++ /** ++ * Creates a potion effect. ++ * @param type effect type ++ * @param duration measured in ticks, see {@link ++ * PotionEffect#getDuration()} ++ * @param amplifier the amplifier, see {@link PotionEffect#getAmplifier()} ++ * @param ambient the ambient status, see {@link PotionEffect#isAmbient()} ++ * @param particles the particle status, see {@link PotionEffect#hasParticles()} ++ * @param icon the icon status, see {@link PotionEffect#hasIcon()} ++ * @param key the namespacedKey, see {@link PotionEffect#getKey()} ++ */ ++ public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, boolean icon, @Nullable NamespacedKey key) { ++ // Purpur end + Preconditions.checkArgument(type != null, "effect type cannot be null"); + this.type = type; + this.duration = duration; +@@ -51,6 +84,7 @@ public class PotionEffect implements ConfigurationSerializable { + this.ambient = ambient; + this.particles = particles; + this.icon = icon; ++ this.key = key; // Purpur - add key + } + + /** +@@ -98,36 +132,43 @@ public class PotionEffect implements ConfigurationSerializable { + * @param map the map to deserialize from + */ + public PotionEffect(@NotNull Map map) { +- this(getEffectType(map), getInt(map, DURATION), getInt(map, AMPLIFIER), getBool(map, AMBIENT, false), getBool(map, PARTICLES, true), getBool(map, ICON, getBool(map, PARTICLES, true))); ++ this(getEffectType(map), getInt(map, DURATION), getInt(map, AMPLIFIER), getBool(map, AMBIENT, false), getBool(map, PARTICLES, true), getBool(map, ICON, getBool(map, PARTICLES, true)), getKey(map)); // Purpur - getKey + } + + // Paper start + @NotNull + public PotionEffect withType(@NotNull PotionEffectType type) { +- return new PotionEffect(type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withDuration(int duration) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withAmplifier(int amplifier) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withAmbient(boolean ambient) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withParticles(boolean particles) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withIcon(boolean icon) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + // Paper end + ++ // Purpur start ++ @NotNull ++ public PotionEffect withKey(@Nullable NamespacedKey key) { ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); ++ } ++ // Purpur end ++ + @NotNull + private static PotionEffectType getEffectType(@NotNull Map map) { + int type = getInt(map, TYPE); +@@ -154,17 +195,33 @@ public class PotionEffect implements ConfigurationSerializable { + return def; + } + ++ // Purpur start ++ @Nullable ++ private static NamespacedKey getKey(@NotNull Map map) { ++ Object key = map.get(KEY); ++ if (key instanceof String stringKey) { ++ return NamespacedKey.fromString(stringKey); ++ } ++ return null; ++ } ++ // Purpur end ++ + @Override + @NotNull + public Map serialize() { +- return ImmutableMap.builder() +- .put(TYPE, type.getId()) +- .put(DURATION, duration) +- .put(AMPLIFIER, amplifier) +- .put(AMBIENT, ambient) +- .put(PARTICLES, particles) +- .put(ICON, icon) +- .build(); ++ // Purpur start - add key, don't serialize if null. ++ ImmutableMap.Builder builder = ImmutableMap.builder() ++ .put(TYPE, type.getId()) ++ .put(DURATION, duration) ++ .put(AMPLIFIER, amplifier) ++ .put(AMBIENT, ambient) ++ .put(PARTICLES, particles) ++ .put(ICON, icon); ++ if(key != null) { ++ builder.put(KEY, key.toString()); ++ } ++ return builder.build(); ++ // Purpur end + } + + /** +@@ -188,7 +245,7 @@ public class PotionEffect implements ConfigurationSerializable { + return false; + } + PotionEffect that = (PotionEffect) obj; +- return this.type.equals(that.type) && this.ambient == that.ambient && this.amplifier == that.amplifier && this.duration == that.duration && this.particles == that.particles && this.icon == that.icon; ++ return this.type.equals(that.type) && this.ambient == that.ambient && this.amplifier == that.amplifier && this.duration == that.duration && this.particles == that.particles && this.icon == that.icon && this.key == that.key; // Purpur - add key + } + + /** +@@ -256,6 +313,24 @@ public class PotionEffect implements ConfigurationSerializable { + return icon; + } + ++ ++ // Purpur start ++ /** ++ * @return if the key isn't the default namespacedKey ++ */ ++ public boolean hasKey() { ++ return key != null; ++ } ++ ++ /** ++ * @return the key attached to the potion ++ */ ++ @Nullable ++ public NamespacedKey getKey() { ++ return key; ++ } ++ // Purpur end ++ + @Override + public int hashCode() { + int hash = 1; +@@ -270,6 +345,6 @@ public class PotionEffect implements ConfigurationSerializable { + + @Override + public String toString() { +- return type.getName() + (ambient ? ":(" : ":") + duration + "t-x" + amplifier + (ambient ? ")" : ""); ++ return type.getName() + (ambient ? ":(" : ":") + duration + "t-x" + amplifier + (ambient ? ")" : "") + (hasKey() ? "(" + key + ")" : ""); // Purpur - add key if not null + } + } +diff --git a/src/main/java/org/bukkit/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/util/permissions/CommandPermissions.java +index 7763d6101ac61900db1e2310966b99584539fd0e..d5a42707d365ffd72532bbb1a59a1ca7145f9918 100644 +--- a/src/main/java/org/bukkit/util/permissions/CommandPermissions.java ++++ b/src/main/java/org/bukkit/util/permissions/CommandPermissions.java +@@ -18,6 +18,7 @@ public final class CommandPermissions { + DefaultPermissions.registerPermission(PREFIX + "plugins", "Allows the user to view the list of plugins running on this server", PermissionDefault.TRUE, commands); + DefaultPermissions.registerPermission(PREFIX + "reload", "Allows the user to reload the server settings", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(PREFIX + "version", "Allows the user to view the version of the server", PermissionDefault.TRUE, commands); ++ DefaultPermissions.registerPermission(PREFIX + "purpur", "Allows the user to use the purpur command", PermissionDefault.OP, commands); // Purpur + + commands.recalculatePermissibles(); + return commands; +diff --git a/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java b/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java +index e1a4ddf2c07cdd242fa8054a0152522fe4039e85..10627d2a11251a8cb01bbc3f6242d66f3505a16e 100644 +--- a/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java ++++ b/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java +@@ -31,7 +31,7 @@ public final class DefaultPermissions { + + if (withLegacy) { + Permission legacy = new Permission(LEGACY_PREFIX + result.getName(), result.getDescription(), PermissionDefault.FALSE); +- legacy.getChildren().put(result.getName(), true); ++ legacy.getChildren().put(result.getName(), null); // Purpur + registerPermission(perm, false); + } + +@@ -40,7 +40,7 @@ public final class DefaultPermissions { + + @NotNull + public static Permission registerPermission(@NotNull Permission perm, @NotNull Permission parent) { +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return registerPermission(perm); + } + +@@ -53,7 +53,7 @@ public final class DefaultPermissions { + @NotNull + public static Permission registerPermission(@NotNull String name, @Nullable String desc, @NotNull Permission parent) { + Permission perm = registerPermission(name, desc); +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return perm; + } + +@@ -66,7 +66,7 @@ public final class DefaultPermissions { + @NotNull + public static Permission registerPermission(@NotNull String name, @Nullable String desc, @Nullable PermissionDefault def, @NotNull Permission parent) { + Permission perm = registerPermission(name, desc, def); +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return perm; + } + +@@ -79,7 +79,7 @@ public final class DefaultPermissions { + @NotNull + public static Permission registerPermission(@NotNull String name, @Nullable String desc, @Nullable PermissionDefault def, @Nullable Map children, @NotNull Permission parent) { + Permission perm = registerPermission(name, desc, def, children); +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return perm; + } + +@@ -89,6 +89,8 @@ public final class DefaultPermissions { + CommandPermissions.registerPermissions(parent); + BroadcastPermissions.registerPermissions(parent); + ++ PurpurPermissions.registerPermissions(); // Purpur ++ + parent.recalculatePermissibles(); + } + } +diff --git a/src/main/java/org/bukkit/util/permissions/PurpurPermissions.java b/src/main/java/org/bukkit/util/permissions/PurpurPermissions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..baec4c87d7ea4d54934ca22fd1eb7b46dd69061b +--- /dev/null ++++ b/src/main/java/org/bukkit/util/permissions/PurpurPermissions.java +@@ -0,0 +1,87 @@ ++package org.bukkit.util.permissions; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.EntityType; ++import org.bukkit.entity.Mob; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class PurpurPermissions { ++ private static final String ROOT = "purpur"; ++ private static final String PREFIX = ROOT + "."; ++ private static final Set mobs = new HashSet<>(); ++ ++ static { ++ for (EntityType mob : EntityType.values()) { ++ Class clazz = mob.getEntityClass(); ++ if (clazz != null && Mob.class.isAssignableFrom(clazz)) { ++ mobs.add(mob.getName()); ++ } ++ } ++ } ++ ++ @NotNull ++ public static Permission registerPermissions() { ++ Permission purpur = DefaultPermissions.registerPermission(ROOT, "Gives the user the ability to use all Purpur utilities and commands", PermissionDefault.FALSE); ++ ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.six", "Gives the user six rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.five", "Gives the user five rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.four", "Gives the user four rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.three", "Gives the user three rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.two", "Gives the user two rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.one", "Gives the user one row of enderchest space", PermissionDefault.FALSE, purpur); ++ ++ DefaultPermissions.registerPermission(PREFIX + "debug.f3n", "Allows the user to use F3+N keybind to swap gamemodes", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "joinfullserver", "Allows the user to join a full server", PermissionDefault.OP, purpur); ++ ++ DefaultPermissions.registerPermission(PREFIX + "drop.spawners", "Allows the user to drop spawner cage when broken with diamond pickaxe with silk touch", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "place.spawners", "Allows the user to place spawner cage in the world", PermissionDefault.FALSE, purpur); ++ ++ DefaultPermissions.registerPermission(PREFIX + "mending_shift_click", "Allows the user to use shift-right-click to mend items", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "inventory_totem", "Uses a totem from anywhere in the user's inventory on death", PermissionDefault.FALSE, purpur); ++ ++ Permission anvil = DefaultPermissions.registerPermission(PREFIX + "anvil", "Allows the user to use all anvil color and format abilities", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.color", "Allows the user to use color codes in an anvil", PermissionDefault.FALSE, anvil); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.minimessage", "Allows the user to use minimessage tags in an anvil", PermissionDefault.FALSE, anvil); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.remove_italics", "Allows the user to remove italics in an anvil", PermissionDefault.FALSE, anvil); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.format", "Allows the user to use format codes in an anvil", PermissionDefault.FALSE, anvil); ++ anvil.recalculatePermissibles(); ++ ++ Permission book = DefaultPermissions.registerPermission(PREFIX + "book", "Allows the user to use color codes on books", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "book.color.edit", "Allows the user to use color codes on books when editing", PermissionDefault.FALSE, book); ++ DefaultPermissions.registerPermission(PREFIX + "book.color.sign", "Allows the user to use color codes on books when signing", PermissionDefault.FALSE, book); ++ book.recalculatePermissibles(); ++ ++ Permission sign = DefaultPermissions.registerPermission(PREFIX + "sign", "Allows the user to use all sign abilities", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "sign.edit", "Allows the user to click signs to open sign editor", PermissionDefault.FALSE, sign); ++ DefaultPermissions.registerPermission(PREFIX + "sign.color", "Allows the user to use color codes on signs", PermissionDefault.FALSE, sign); ++ DefaultPermissions.registerPermission(PREFIX + "sign.style", "Allows the user to use style codes on signs", PermissionDefault.FALSE, sign); ++ DefaultPermissions.registerPermission(PREFIX + "sign.magic", "Allows the user to use magic/obfuscate code on signs", PermissionDefault.FALSE, sign); ++ sign.recalculatePermissibles(); ++ ++ Permission ride = DefaultPermissions.registerPermission("allow.ride", "Allows the user to ride all mobs", PermissionDefault.FALSE, purpur); ++ for (String mob : mobs) { ++ DefaultPermissions.registerPermission("allow.ride." + mob, "Allows the user to ride " + mob, PermissionDefault.FALSE, ride); ++ } ++ ride.recalculatePermissibles(); ++ ++ Permission special = DefaultPermissions.registerPermission("allow.special", "Allows the user to use all mobs special abilities", PermissionDefault.FALSE, purpur); ++ for (String mob : mobs) { ++ DefaultPermissions.registerPermission("allow.special." + mob, "Allows the user to use " + mob + " special ability", PermissionDefault.FALSE, special); ++ } ++ special.recalculatePermissibles(); ++ ++ Permission powered = DefaultPermissions.registerPermission("allow.powered", "Allows the user to toggle all mobs powered state", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission("allow.powered.creeper", "Allows the user to toggle creeper powered state", PermissionDefault.FALSE, powered); ++ powered.recalculatePermissibles(); ++ ++ DefaultPermissions.registerPermission(PREFIX + "portal.instant", "Allows the user to bypass portal wait time", PermissionDefault.FALSE, purpur); ++ ++ purpur.recalculatePermissibles(); ++ return purpur; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java b/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bc590c4d49d32f4365a50ceb5785e798702a8179 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java +@@ -0,0 +1,130 @@ ++package org.purpurmc.purpur.event; ++ ++import com.google.common.base.Preconditions; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * This event is called whenever someone runs a command ++ */ ++public class ExecuteCommandEvent extends Event implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancel = false; ++ private CommandSender sender; ++ private Command command; ++ private String label; ++ private String[] args; ++ ++ public ExecuteCommandEvent(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @Nullable String[] args) { ++ this.sender = sender; ++ this.command = command; ++ this.label = label; ++ this.args = args; ++ } ++ ++ /** ++ * Gets the command that the player is attempting to execute. ++ * ++ * @return Command the player is attempting to execute ++ */ ++ @NotNull ++ public Command getCommand() { ++ return command; ++ } ++ ++ /** ++ * Sets the command that the player will execute. ++ * ++ * @param command New command that the player will execute ++ * @throws IllegalArgumentException if command is null or empty ++ */ ++ public void setCommand(@NotNull Command command) throws IllegalArgumentException { ++ Preconditions.checkArgument(command != null, "Command cannot be null"); ++ this.command = command; ++ } ++ ++ /** ++ * Gets the sender that this command will be executed as. ++ * ++ * @return Sender this command will be executed as ++ */ ++ @NotNull ++ public CommandSender getSender() { ++ return sender; ++ } ++ ++ /** ++ * Sets the sender that this command will be executed as. ++ * ++ * @param sender New sender which this event will execute as ++ * @throws IllegalArgumentException if the sender provided is null ++ */ ++ public void setSender(@NotNull final CommandSender sender) throws IllegalArgumentException { ++ Preconditions.checkArgument(sender != null, "Sender cannot be null"); ++ this.sender = sender; ++ } ++ ++ /** ++ * Get the label used to execute this command ++ * ++ * @return Label used to execute this command ++ */ ++ @NotNull ++ public String getLabel() { ++ return label; ++ } ++ ++ /** ++ * Set the label used to execute this command ++ * ++ * @param label Label used ++ */ ++ public void setLabel(@NotNull String label) { ++ this.label = label; ++ } ++ ++ /** ++ * Get the args passed to the command ++ * ++ * @return Args passed to the command ++ */ ++ @NotNull ++ public String[] getArgs() { ++ return args; ++ } ++ ++ /** ++ * Set the args passed to the command ++ * ++ * @param args Args passed to the command ++ */ ++ public void setArgs(@NotNull String[] args) { ++ this.args = args; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java b/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..25e92af7710316ed2afedf846a59dbd672869b51 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java +@@ -0,0 +1,70 @@ ++package org.purpurmc.purpur.event; ++ ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.PlayerEvent; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++public class PlayerAFKEvent extends PlayerEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private final boolean setAfk; ++ private boolean shouldKick; ++ private String broadcast; ++ private boolean cancel; ++ ++ public PlayerAFKEvent(@NotNull Player player, boolean setAfk, boolean shouldKick, @Nullable String broadcast, boolean async) { ++ super(player, async); ++ this.setAfk = setAfk; ++ this.shouldKick = shouldKick; ++ this.broadcast = broadcast; ++ } ++ ++ /** ++ * Whether player is going afk or coming back ++ * ++ * @return True if going afk. False is coming back ++ */ ++ public boolean isGoingAfk() { ++ return setAfk; ++ } ++ ++ public boolean shouldKick() { ++ return shouldKick; ++ } ++ ++ public void setShouldKick(boolean shouldKick) { ++ this.shouldKick = shouldKick; ++ } ++ ++ @Nullable ++ public String getBroadcastMsg() { ++ return broadcast; ++ } ++ ++ public void setBroadcastMsg(@Nullable String broadcast) { ++ this.broadcast = broadcast; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/PlayerSetSpawnerTypeWithEggEvent.java b/src/main/java/org/purpurmc/purpur/event/PlayerSetSpawnerTypeWithEggEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..519809eab5d926dc7b0a7bad5d446d0defc099dc +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/PlayerSetSpawnerTypeWithEggEvent.java +@@ -0,0 +1,85 @@ ++package org.purpurmc.purpur.event; ++ ++import org.bukkit.block.Block; ++import org.bukkit.block.CreatureSpawner; ++import org.bukkit.entity.EntityType; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.PlayerEvent; ++import org.jetbrains.annotations.NotNull; ++ ++public class PlayerSetSpawnerTypeWithEggEvent extends PlayerEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Block block; ++ private final CreatureSpawner spawner; ++ private EntityType type; ++ private boolean cancel; ++ ++ public PlayerSetSpawnerTypeWithEggEvent(@NotNull Player player, @NotNull Block block, @NotNull CreatureSpawner spawner, @NotNull EntityType type) { ++ super(player); ++ this.block = block; ++ this.spawner = spawner; ++ this.type = type; ++ } ++ ++ /** ++ * Get the spawner Block in the world ++ * ++ * @return Spawner Block ++ */ ++ @NotNull ++ public Block getBlock() { ++ return block; ++ } ++ ++ /** ++ * Get the spawner state ++ * ++ * @return Spawner state ++ */ ++ @NotNull ++ public CreatureSpawner getSpawner() { ++ return spawner; ++ } ++ ++ /** ++ * Gets the EntityType being set on the spawner ++ * ++ * @return EntityType being set ++ */ ++ @NotNull ++ public EntityType getEntityType() { ++ return type; ++ } ++ ++ /** ++ * Sets the EntityType being set on the spawner ++ * ++ * @param type EntityType to set ++ */ ++ public void setEntityType(@NotNull EntityType type) { ++ this.type = type; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/PreBlockExplodeEvent.java b/src/main/java/org/purpurmc/purpur/event/PreBlockExplodeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7db0db7f3afbccdb07390d1bcada109e9e6b30b +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/PreBlockExplodeEvent.java +@@ -0,0 +1,52 @@ ++package org.purpurmc.purpur.event; ++ ++import org.bukkit.block.Block; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.block.BlockExplodeEvent; ++import org.jetbrains.annotations.NotNull; ++import java.util.Collections; ++ ++/** ++ * Called before a block's explosion is processed ++ */ ++public class PreBlockExplodeEvent extends BlockExplodeEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancelled; ++ private final float yield; ++ ++ public PreBlockExplodeEvent(@NotNull final Block what, final float yield) { ++ super(what, Collections.emptyList(), yield); ++ this.yield = yield; ++ this.cancelled = false; ++ } ++ ++ /** ++ * Returns the percentage of blocks to drop from this explosion ++ * ++ * @return The yield. ++ */ ++ public float getYield() { ++ return yield; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++ ++ @Override ++ public @NotNull HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/BeeFoundFlowerEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/BeeFoundFlowerEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..833f46d1941f377765132fc528c45567ee0290d2 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/BeeFoundFlowerEvent.java +@@ -0,0 +1,48 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Bee; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Called when a bee targets a flower ++ */ ++public class BeeFoundFlowerEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Location location; ++ ++ public BeeFoundFlowerEvent(@NotNull Bee bee, @Nullable Location location) { ++ super(bee); ++ this.location = location; ++ } ++ ++ @Override ++ @NotNull ++ public Bee getEntity() { ++ return (Bee) super.getEntity(); ++ } ++ ++ /** ++ * Returns the location of the flower that the bee targets ++ * ++ * @return The location of the flower ++ */ ++ @Nullable ++ public Location getLocation() { ++ return location; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/BeeStartedPollinatingEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/BeeStartedPollinatingEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ae0bb654745724889c67fae9072ae90ea3778ba4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/BeeStartedPollinatingEvent.java +@@ -0,0 +1,47 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Bee; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a bee starts pollinating ++ */ ++public class BeeStartedPollinatingEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Location location; ++ ++ public BeeStartedPollinatingEvent(@NotNull Bee bee, @NotNull Location location) { ++ super(bee); ++ this.location = location; ++ } ++ ++ @Override ++ @NotNull ++ public Bee getEntity() { ++ return (Bee) super.getEntity(); ++ } ++ ++ /** ++ * Returns the location of the flower that the bee pollinates ++ * ++ * @return The location of the flower ++ */ ++ @NotNull ++ public Location getLocation() { ++ return this.location; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/BeeStopPollinatingEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/BeeStopPollinatingEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ff3c9f075be2f624af8b0ce5fffc5ea69a41f32e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/BeeStopPollinatingEvent.java +@@ -0,0 +1,60 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Bee; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Called when a bee stops pollinating ++ */ ++public class BeeStopPollinatingEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Location location; ++ private final boolean success; ++ ++ public BeeStopPollinatingEvent(@NotNull Bee bee, @Nullable Location location, boolean success) { ++ super(bee); ++ this.location = location; ++ this.success = success; ++ } ++ ++ @Override ++ @NotNull ++ public Bee getEntity() { ++ return (Bee) super.getEntity(); ++ } ++ ++ /** ++ * Returns the location of the flower that the bee stopped pollinating ++ * ++ * @return The location of the flower ++ */ ++ @Nullable ++ public Location getLocation() { ++ return location; ++ } ++ ++ /** ++ * Returns whether the bee successfully pollinated the flower ++ * ++ * @return True if the pollination was successful ++ */ ++ public boolean wasSuccessful() { ++ return success; ++ } ++ ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/EntityTeleportHinderedEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/EntityTeleportHinderedEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c66eb163877e872f234d86dc244cab7efeb818cd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/EntityTeleportHinderedEvent.java +@@ -0,0 +1,117 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Fired when an entity is hindered from teleporting. ++ */ ++public class EntityTeleportHinderedEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ @NotNull ++ private final Reason reason; ++ ++ @Nullable ++ private final TeleportCause teleportCause; ++ ++ private boolean retry = false; ++ ++ public EntityTeleportHinderedEvent(@NotNull Entity what, @NotNull Reason reason, ++ @Nullable TeleportCause teleportCause) { ++ super(what); ++ this.reason = reason; ++ this.teleportCause = teleportCause; ++ } ++ ++ /** ++ * @return why the teleport was hindered. ++ */ ++ @NotNull ++ public Reason getReason() { ++ return reason; ++ } ++ ++ /** ++ * @return why the teleport occurred if cause was given, otherwise {@code null}. ++ */ ++ @Nullable ++ public TeleportCause getTeleportCause() { ++ return teleportCause; ++ } ++ ++ /** ++ * Whether the teleport should be retried. ++ *

++ * Note that this can put the server in a never-ending loop of trying to teleport someone resulting in a stack ++ * overflow. Do not retry more than necessary. ++ *

++ * ++ * @return whether the teleport should be retried. ++ */ ++ public boolean shouldRetry() { ++ return retry; ++ } ++ ++ /** ++ * Sets whether the teleport should be retried. ++ *

++ * Note that this can put the server in a never-ending loop of trying to teleport someone resulting in a stack ++ * overflow. Do not retry more than necessary. ++ *

++ * ++ * @param retry whether the teleport should be retried. ++ */ ++ public void setShouldRetry(boolean retry) { ++ this.retry = retry; ++ } ++ ++ /** ++ * Calls the event and tests if should retry. ++ * ++ * @return whether the teleport should be retried. ++ */ ++ @Override ++ public boolean callEvent() { ++ super.callEvent(); ++ return shouldRetry(); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++ ++ /** ++ * Reason for hindrance in teleports. ++ */ ++ public enum Reason { ++ /** ++ * The teleported entity is a passenger of another entity. ++ */ ++ IS_PASSENGER, ++ ++ /** ++ * The teleported entity has passengers. ++ */ ++ IS_VEHICLE, ++ ++ /** ++ * The teleport event was cancelled. ++ *

++ * This is only caused by players teleporting. ++ *

++ */ ++ EVENT_CANCELLED, ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/GoatRamEntityEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/GoatRamEntityEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f62c14f3d4999e9112c1c73642aa337d97b94b5a +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/GoatRamEntityEvent.java +@@ -0,0 +1,59 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Goat; ++import org.bukkit.entity.LivingEntity; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a goat rams an entity ++ */ ++public class GoatRamEntityEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private final LivingEntity rammedEntity; ++ private boolean cancelled; ++ ++ public GoatRamEntityEvent(@NotNull Goat goat, @NotNull LivingEntity rammedEntity) { ++ super(goat); ++ this.rammedEntity = rammedEntity; ++ } ++ ++ /** ++ * Returns the entity that was rammed by the goat ++ * ++ * @return The rammed entity ++ */ ++ @NotNull ++ public LivingEntity getRammedEntity() { ++ return this.rammedEntity; ++ } ++ ++ @Override ++ @NotNull ++ public Goat getEntity() { ++ return (Goat) super.getEntity(); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/LlamaJoinCaravanEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/LlamaJoinCaravanEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8849bb0becb16db907fa648cca2e98ab9d957c75 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/LlamaJoinCaravanEvent.java +@@ -0,0 +1,61 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Llama; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a Llama tries to join a caravan. ++ *

++ * Cancelling the event will not let the Llama join. To prevent future attempts ++ * at joining a caravan use {@link Llama#setShouldJoinCaravan(boolean)}. ++ */ ++public class LlamaJoinCaravanEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean canceled; ++ private final Llama head; ++ ++ public LlamaJoinCaravanEvent(@NotNull Llama llama, @NotNull Llama head) { ++ super(llama); ++ this.head = head; ++ } ++ ++ @Override ++ @NotNull ++ public Llama getEntity() { ++ return (Llama) entity; ++ } ++ ++ /** ++ * Get the Llama that this Llama is about to follow ++ * ++ * @return Llama about to be followed ++ */ ++ @NotNull ++ public Llama getHead() { ++ return head; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return canceled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ canceled = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/LlamaLeaveCaravanEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/LlamaLeaveCaravanEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c268c35b541a222d50875c29770c846a8ffcc4f8 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/LlamaLeaveCaravanEvent.java +@@ -0,0 +1,34 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Llama; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a Llama leaves a caravan ++ */ ++public class LlamaLeaveCaravanEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ public LlamaLeaveCaravanEvent(@NotNull Llama llama) { ++ super(llama); ++ } ++ ++ @Override ++ @NotNull ++ public Llama getEntity() { ++ return (Llama) entity; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/MonsterEggSpawnEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/MonsterEggSpawnEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..82f8a0ea22f07954d516935fc9f73f6aa0f65aa6 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/MonsterEggSpawnEvent.java +@@ -0,0 +1,67 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.HumanEntity; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++public class MonsterEggSpawnEvent extends Event implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean canceled; ++ ++ private final Player player; ++ private Entity entity; ++ private final ItemStack item; ++ ++ public MonsterEggSpawnEvent(@Nullable HumanEntity player, @NotNull Entity entity, @NotNull ItemStack item) { ++ this.player = (Player) player; ++ this.entity = entity; ++ this.item = item; ++ } ++ ++ @Nullable ++ public Player getPlayer() { ++ return player; ++ } ++ ++ @NotNull ++ public Entity getEntity() { ++ return entity; ++ } ++ ++ public void setEntity(@Nullable Entity entity) { ++ if (entity == null) { ++ canceled = true; ++ return; ++ } ++ this.entity = entity; ++ } ++ ++ @NotNull ++ public ItemStack getItem() { ++ return item; ++ } ++ ++ public boolean isCancelled() { ++ return canceled; ++ } ++ ++ public void setCancelled(boolean cancel) { ++ canceled = cancel; ++ } ++ ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/PreEntityExplodeEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/PreEntityExplodeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2d4f68228861492baaea0bcc604dfef623b337ba +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/PreEntityExplodeEvent.java +@@ -0,0 +1,64 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityExplodeEvent; ++import org.jetbrains.annotations.NotNull; ++import java.util.Collections; ++ ++/** ++ * Called before an entity's explosion is processed ++ */ ++public class PreEntityExplodeEvent extends EntityExplodeEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancelled; ++ private final float yield; ++ private final Location location; ++ ++ public PreEntityExplodeEvent(@NotNull org.bukkit.entity.Entity what, @NotNull final Location location, final float yield) { ++ super(what, location, Collections.emptyList(), yield); ++ this.cancelled = false; ++ this.yield = yield; ++ this.location = location; ++ } ++ ++ /** ++ * Returns the percentage of blocks to drop from this explosion ++ * ++ * @return The yield. ++ */ ++ public float getYield() { ++ return yield; ++ } ++ ++ /** ++ * Returns the location where the explosion happened. ++ * ++ * @return The location of the explosion ++ */ ++ @NotNull ++ public Location getLocation() { ++ return location; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++ ++ @Override ++ public @NotNull HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/RidableMoveEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/RidableMoveEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a037df01b07af9ffb98b67aca412c1d34fade03b +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/RidableMoveEvent.java +@@ -0,0 +1,103 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import com.google.common.base.Preconditions; ++import org.bukkit.Location; ++import org.bukkit.entity.Mob; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Triggered when a ridable mob moves with a rider ++ */ ++public class RidableMoveEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean canceled; ++ private final Player rider; ++ private Location from; ++ private Location to; ++ ++ public RidableMoveEvent(@NotNull Mob entity, @NotNull Player rider, @NotNull Location from, @NotNull Location to) { ++ super(entity); ++ this.rider = rider; ++ this.from = from; ++ this.to = to; ++ } ++ ++ @Override ++ @NotNull ++ public Mob getEntity() { ++ return (Mob) entity; ++ } ++ ++ @NotNull ++ public Player getRider() { ++ return rider; ++ } ++ ++ public boolean isCancelled() { ++ return canceled; ++ } ++ ++ public void setCancelled(boolean cancel) { ++ canceled = cancel; ++ } ++ ++ /** ++ * Gets the location this entity moved from ++ * ++ * @return Location the entity moved from ++ */ ++ @NotNull ++ public Location getFrom() { ++ return from; ++ } ++ ++ /** ++ * Sets the location to mark as where the entity moved from ++ * ++ * @param from New location to mark as the entity's previous location ++ */ ++ public void setFrom(@NotNull Location from) { ++ validateLocation(from); ++ this.from = from; ++ } ++ ++ /** ++ * Gets the location this entity moved to ++ * ++ * @return Location the entity moved to ++ */ ++ @NotNull ++ public Location getTo() { ++ return to; ++ } ++ ++ /** ++ * Sets the location that this entity will move to ++ * ++ * @param to New Location this entity will move to ++ */ ++ public void setTo(@NotNull Location to) { ++ validateLocation(to); ++ this.to = to; ++ } ++ ++ private void validateLocation(@NotNull Location loc) { ++ Preconditions.checkArgument(loc != null, "Cannot use null location!"); ++ Preconditions.checkArgument(loc.getWorld() != null, "Cannot use null location with null world!"); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/RidableSpacebarEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/RidableSpacebarEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3d3a7d898e3278ce998d713dafbb4b354dad7fc7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/RidableSpacebarEvent.java +@@ -0,0 +1,37 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++public class RidableSpacebarEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancelled; ++ ++ public RidableSpacebarEvent(@NotNull Entity entity) { ++ super(entity); ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ cancelled = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/inventory/AnvilTakeResultEvent.java b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilTakeResultEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b363c91a29f826910db22f2643decf996a067ab5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilTakeResultEvent.java +@@ -0,0 +1,52 @@ ++package org.purpurmc.purpur.event.inventory; ++ ++import org.bukkit.entity.HumanEntity; ++import org.bukkit.entity.Player; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.inventory.InventoryEvent; ++import org.bukkit.inventory.AnvilInventory; ++import org.bukkit.inventory.InventoryView; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a player takes the result item out of an anvil ++ */ ++public class AnvilTakeResultEvent extends InventoryEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Player player; ++ private final ItemStack result; ++ ++ public AnvilTakeResultEvent(@NotNull HumanEntity player, @NotNull InventoryView view, @NotNull ItemStack result) { ++ super(view); ++ this.player = (Player) player; ++ this.result = result; ++ } ++ ++ @NotNull ++ public Player getPlayer() { ++ return player; ++ } ++ ++ @NotNull ++ public ItemStack getResult() { ++ return result; ++ } ++ ++ @NotNull ++ @Override ++ public AnvilInventory getInventory() { ++ return (AnvilInventory) super.getInventory(); ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/inventory/AnvilUpdateResultEvent.java b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilUpdateResultEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fd6a5a3589d436c2aaf988fd305899695799d3bb +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilUpdateResultEvent.java +@@ -0,0 +1,35 @@ ++package org.purpurmc.purpur.event.inventory; ++ ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.inventory.InventoryEvent; ++import org.bukkit.inventory.AnvilInventory; ++import org.bukkit.inventory.InventoryView; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when anvil slots change, triggering the result slot to be updated ++ */ ++public class AnvilUpdateResultEvent extends InventoryEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ public AnvilUpdateResultEvent(@NotNull InventoryView view) { ++ super(view); ++ } ++ ++ @NotNull ++ @Override ++ public AnvilInventory getInventory() { ++ return (AnvilInventory) super.getInventory(); ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/inventory/GrindstoneTakeResultEvent.java b/src/main/java/org/purpurmc/purpur/event/inventory/GrindstoneTakeResultEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eebb5d124456b8209d1b8e8cc4cb772dd3714f04 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/inventory/GrindstoneTakeResultEvent.java +@@ -0,0 +1,72 @@ ++package org.purpurmc.purpur.event.inventory; ++ ++import org.bukkit.entity.HumanEntity; ++import org.bukkit.entity.Player; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.inventory.InventoryEvent; ++import org.bukkit.inventory.GrindstoneInventory; ++import org.bukkit.inventory.InventoryView; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a player takes the result item out of a Grindstone ++ */ ++public class GrindstoneTakeResultEvent extends InventoryEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Player player; ++ private final ItemStack result; ++ private int experienceAmount; ++ ++ public GrindstoneTakeResultEvent(@NotNull HumanEntity player, @NotNull InventoryView view, @NotNull ItemStack result, int experienceAmount) { ++ super(view); ++ this.player = (Player) player; ++ this.result = result; ++ this.experienceAmount = experienceAmount; ++ } ++ ++ @NotNull ++ public Player getPlayer() { ++ return player; ++ } ++ ++ @NotNull ++ public ItemStack getResult() { ++ return result; ++ } ++ ++ @NotNull ++ @Override ++ public GrindstoneInventory getInventory() { ++ return (GrindstoneInventory) super.getInventory(); ++ } ++ ++ /** ++ * Get the amount of experience this transaction will give ++ * ++ * @return Amount of experience to give ++ */ ++ public int getExperienceAmount() { ++ return this.experienceAmount; ++ } ++ ++ /** ++ * Set the amount of experience this transaction will give ++ * ++ * @param experienceAmount Amount of experience to give ++ */ ++ public void setExperienceAmount(int experienceAmount) { ++ this.experienceAmount = experienceAmount; ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/packet/NetworkItemSerializeEvent.java b/src/main/java/org/purpurmc/purpur/event/packet/NetworkItemSerializeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0da73d2ea83a6055e34894ba1c7506fc8667712 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/packet/NetworkItemSerializeEvent.java +@@ -0,0 +1,48 @@ ++package org.purpurmc.purpur.event.packet; ++ ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Called when an item is about to be written to a packet. ++ */ ++public class NetworkItemSerializeEvent extends Event { ++ private ItemStack itemStack; ++ ++ public NetworkItemSerializeEvent(@NotNull ItemStack itemStack) { ++ super(!org.bukkit.Bukkit.isPrimaryThread()); ++ this.itemStack = itemStack; ++ } ++ ++ /** ++ * @return The item that is about to be serialized. Not mutable ++ */ ++ @NotNull ++ public ItemStack getItemStack() { ++ return itemStack; ++ } ++ ++ /** ++ * Sets the item that will be serialized. ++ * ++ * @param itemStack The item ++ */ ++ public void setItemStack(@Nullable ItemStack itemStack) { ++ this.itemStack = itemStack; ++ } ++ ++ private static final HandlerList handlers = new HandlerList(); ++ ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/player/PlayerBookTooLargeEvent.java b/src/main/java/org/purpurmc/purpur/event/player/PlayerBookTooLargeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c88394336bc9ab0f66a2af24d393f4a176a234d5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/player/PlayerBookTooLargeEvent.java +@@ -0,0 +1,65 @@ ++package org.purpurmc.purpur.event.player; ++ ++import org.bukkit.Bukkit; ++import org.bukkit.entity.Player; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.PlayerEvent; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a player tries to bypass book limitations ++ */ ++public class PlayerBookTooLargeEvent extends PlayerEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final ItemStack book; ++ private boolean kickPlayer = true; ++ ++ /** ++ * @param player The player ++ * @param book The book ++ */ ++ public PlayerBookTooLargeEvent(@NotNull Player player, @NotNull ItemStack book) { ++ super(player, !Bukkit.isPrimaryThread()); ++ this.book = book; ++ } ++ ++ /** ++ * Get the book containing the wanted edits ++ * ++ * @return The book ++ */ ++ @NotNull ++ public ItemStack getBook() { ++ return book; ++ } ++ ++ /** ++ * Whether server should kick the player or not ++ * ++ * @return True to kick player ++ */ ++ public boolean shouldKickPlayer() { ++ return kickPlayer; ++ } ++ ++ /** ++ * Whether server should kick the player or not ++ * ++ * @param kickPlayer True to kick player ++ */ ++ public void setShouldKickPlayer(boolean kickPlayer) { ++ this.kickPlayer = kickPlayer; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/language/Language.java b/src/main/java/org/purpurmc/purpur/language/Language.java +new file mode 100644 +index 0000000000000000000000000000000000000000..38483d908ed830e97883733bee2370f87060f4c7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/language/Language.java +@@ -0,0 +1,60 @@ ++package org.purpurmc.purpur.language; ++ ++import net.kyori.adventure.translation.Translatable; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Represents a language that can translate translation keys ++ */ ++public abstract class Language { ++ private static Language language; ++ ++ /** ++ * Returns the default language of the server ++ */ ++ @NotNull ++ public static Language getLanguage() { ++ return language; ++ } ++ ++ public static void setLanguage(@NotNull Language language) { ++ if (Language.language != null) { ++ throw new UnsupportedOperationException("Cannot redefine singleton Language"); ++ } ++ Language.language = language; ++ } ++ ++ /** ++ * Checks if a certain translation key is translatable with this language ++ * @param key The translation key ++ * @return Whether this language can translate the key ++ */ ++ abstract public boolean has(@NotNull String key); ++ ++ /** ++ * Checks if a certain translation key is translatable with this language ++ * @param key The translation key ++ * @return Whether this language can translate the key ++ */ ++ public boolean has(@NotNull Translatable key) { ++ return has(key.translationKey()); ++ } ++ ++ /** ++ * Translates a translation key to this language ++ * @param key The translation key ++ * @return The translated key, or the translation key if it couldn't be translated ++ */ ++ @NotNull ++ abstract public String getOrDefault(@NotNull String key); ++ ++ /** ++ * Translates a translation key to this language ++ * @param key The translation key ++ * @return The translated key, or the translation key if it couldn't be translated ++ */ ++ @NotNull ++ public String getOrDefault(@NotNull Translatable key) { ++ return getOrDefault(key.translationKey()); ++ } ++} +diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java +index 123647bb10fc89508437d7a0bd3fd31d58ee7c82..ce209668dd7f11b284bd7f5688191a0b3ae86a09 100644 +--- a/src/main/java/org/spigotmc/CustomTimingsHandler.java ++++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java +@@ -61,7 +61,7 @@ public final class CustomTimingsHandler { + handler = timing; + } + +- public void startTiming() { handler.startTiming(); } +- public void stopTiming() { handler.stopTiming(); } ++ public void startTiming() { /*handler.startTiming();*/ } // Purpur ++ public void stopTiming() { /*handler.stopTiming();*/ } // Purpur + + } +diff --git a/src/test/java/org/bukkit/AnnotationTest.java b/src/test/java/org/bukkit/AnnotationTest.java +index 2cb81e6d253d70388da15c9d07b630277d486c70..fa7229ae5e00e36b0dc8b4cf15d3c99892cc3291 100644 +--- a/src/test/java/org/bukkit/AnnotationTest.java ++++ b/src/test/java/org/bukkit/AnnotationTest.java +@@ -47,6 +47,10 @@ public class AnnotationTest { + "org/bukkit/plugin/java/PluginClassLoader", + // Generic functional interface + "org/bukkit/util/Consumer", ++ // Purpur start ++ "gg/pufferfish/pufferfish/sentry/SentryContext", ++ "gg/pufferfish/pufferfish/sentry/SentryContext$State", ++ // Purpur end + // Paper start + "io/papermc/paper/util/TransformingRandomAccessList", + "io/papermc/paper/util/TransformingRandomAccessList$TransformedListIterator", diff --git a/patches/api/0003-Plazma-Configurations.patch b/patches/api/0003-Plazma-Configurations.patch new file mode 100644 index 0000000..ed30ed5 --- /dev/null +++ b/patches/api/0003-Plazma-Configurations.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Thu, 22 Dec 2022 20:20:03 +0900 +Subject: [PATCH] Plazma Configurations + + +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index e0f69edf603c2ec99bc92b16b18912272cc41bd9..188cd7b3270422719297a6fd3ecec4a20d9b4f3c 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -1971,6 +1971,13 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + } + // Paper end + ++ // Plazma start ++ @NotNull ++ public org.bukkit.configuration.file.YamlConfiguration getPlazmaConfiguration() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ // Plazma end ++ + // Purpur start + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getPurpurConfig() { diff --git a/patches/api/0004-Bump-Bungeecord-Chat-API-to-1.19-R0.1-SNAPSHOT.patch b/patches/api/0004-Bump-Bungeecord-Chat-API-to-1.19-R0.1-SNAPSHOT.patch new file mode 100644 index 0000000..941a745 --- /dev/null +++ b/patches/api/0004-Bump-Bungeecord-Chat-API-to-1.19-R0.1-SNAPSHOT.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Thu, 22 Dec 2022 20:29:45 +0900 +Subject: [PATCH] Bump Bungeecord Chat API to 1.19-R0.1-SNAPSHOT + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 5c8dd4d3313a791d1fee00ec5d4bc595b76b7d6d..a85f1db096555a6077dd77cbd7572442d3adc848 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -25,7 +25,7 @@ dependencies { + // api dependencies are listed transitively to API consumers + api("com.google.guava:guava:31.1-jre") + api("com.google.code.gson:gson:2.10") +- api("net.md-5:bungeecord-chat:1.16-R0.4-deprecated+build.6") // Paper ++ api("net.md-5:bungeecord-chat:1.19-R0.1-SNAPSHOT") // Paper // Plazma + api("org.yaml:snakeyaml:1.33") + // Paper start + api("com.googlecode.json-simple:json-simple:1.1.1") { diff --git a/patches/api/0005-Publish-Packages.patch b/patches/api/0005-Publish-Packages.patch new file mode 100644 index 0000000..0b9b304 --- /dev/null +++ b/patches/api/0005-Publish-Packages.patch @@ -0,0 +1,34 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Fri, 6 Jan 2023 17:11:31 +0900 +Subject: [PATCH] Publish Packages + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 2df01c65821d918673819115a743202bf6d9748a..a52b75ab7fdf055b2dc17e1d71bc552152e97a38 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -154,3 +154,23 @@ tasks.check { + dependsOn(scanJar) + } + // Paper end ++ ++// Plazma start ++publishing { ++ repositories { ++ maven { ++ name = "githubPackage" ++ url = uri("https://maven.pkg.github.com/PlazmaMC/Plazma") ++ ++ credentials.username = System.getenv("GITHUB_USERNAME") ++ credentials.password = System.getenv("GITHUB_TOKEN") ++ } ++ ++ publications { ++ register("gpr") { ++ from(components["java"]) ++ } ++ } ++ } ++} ++// Plazma end diff --git a/patches/server/0001-Pufferfish-Server-Changes.patch b/patches/server/0001-Pufferfish-Server-Changes.patch new file mode 100644 index 0000000..36f54d5 --- /dev/null +++ b/patches/server/0001-Pufferfish-Server-Changes.patch @@ -0,0 +1,3675 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Mon, 6 Mar 2023 09:28:59 +0000 +Subject: [PATCH] Pufferfish Server Changes + +Original: Kevin Raneri +Copyright (C) 2023 Pufferfish Studios LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +diff --git a/build.gradle.kts b/build.gradle.kts +index 4cc7d15f43b2547a723ea231bcf98bc13bdb85bf..af8a561e4d59d98ebb6d7a8fbcb072e361b10058 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -7,8 +7,12 @@ plugins { + } + + dependencies { +- implementation(project(":paper-api")) +- implementation(project(":paper-mojangapi")) ++ implementation(project(":pufferfish-api")) // Pufferfish // Paper ++ // Pufferfish start ++ implementation("io.papermc.paper:paper-mojangapi:1.19.2-R0.1-SNAPSHOT") { ++ exclude("io.papermc.paper", "paper-api") ++ } ++ // Pufferfish end + // Paper start + implementation("org.jline:jline-terminal-jansi:3.21.0") + implementation("net.minecrell:terminalconsoleappender:1.3.0") +@@ -42,6 +46,13 @@ dependencies { + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3") + runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.3") + ++ // Pufferfish start ++ implementation("org.yaml:snakeyaml:1.32") ++ implementation ("me.carleslc.Simple-YAML:Simple-Yaml:1.8.2") { ++ exclude(group="org.yaml", module="snakeyaml") ++ } ++ // Pufferfish end ++ + testImplementation("io.github.classgraph:classgraph:4.8.47") // Paper - mob goal test + testImplementation("junit:junit:4.13.2") + testImplementation("org.hamcrest:hamcrest-library:1.3") +@@ -50,6 +61,14 @@ dependencies { + } + + val craftbukkitPackageVersion = "1_19_R2" // Paper ++ ++// Pufferfish Start ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("--add-modules=jdk.incubator.vector") ++} ++// Pufferfish End ++ + tasks.jar { + archiveClassifier.set("dev") + +@@ -62,7 +81,7 @@ tasks.jar { + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", + "Implementation-Title" to "CraftBukkit", +- "Implementation-Version" to "git-Paper-$implementationVersion", ++ "Implementation-Version" to "git-Pufferfish-$implementationVersion", // Pufferfish + "Implementation-Vendor" to date, // Paper + "Specification-Title" to "Bukkit", + "Specification-Version" to project.version, +diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java +index 06bff37e4c1fddd3be6343049a66787c63fb420c..2cc44fbf8e5bd436b6d4e19f6c06b351e750cb31 100644 +--- a/src/main/java/co/aikar/timings/TimingsExport.java ++++ b/src/main/java/co/aikar/timings/TimingsExport.java +@@ -241,7 +241,8 @@ public class TimingsExport extends Thread { + parent.put("config", createObject( + pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)), + pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)), +- pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)) ++ pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Pufferfish ++ pair("pufferfish", mapAsJSON(gg.pufferfish.pufferfish.PufferfishConfig.getConfigCopy(), null)) // Pufferfish + )); + + new TimingsExport(listeners, parent, history).start(); +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 4b002e8b75d117b726b0de274a76d3596fce015b..061716934ba0a1f01e4d85d664034f72b3c7a765 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); ++ Metrics metrics = new Metrics("Pufferfish", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -603,15 +603,7 @@ public class Metrics { + + metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); + metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); +- final String paperVersion; +- final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion(); +- if (implVersion != null) { +- final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1); +- paperVersion = "git-Paper-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); +- } else { +- paperVersion = "unknown"; +- } +- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); ++ metrics.addCustomChart(new Metrics.SimplePie("pufferfish_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java +index 41b9405d6759d865e0d14dd4f95163e9690e967d..091b1ae822e1c0517e59572e7a9bda11e998c0ee 100644 +--- a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java ++++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java +@@ -26,7 +26,7 @@ public abstract class AreaMap { + + // we use linked for better iteration. + // map of: coordinate to set of objects in coordinate +- protected final Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); ++ protected Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); // Pufferfish - not actually final + protected final PooledLinkedHashSets pooledHashSets; + + protected final ChangeCallback addCallback; +@@ -160,7 +160,8 @@ public abstract class AreaMap { + protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final E object); + + // expensive op, only for debug +- protected void validate(final E object, final int viewDistance) { ++ protected void validate0(final E object, final int viewDistance) { // Pufferfish - rename this thing just in case it gets used I'd rather a compile time error. ++ if (true) throw new UnsupportedOperationException(); // Pufferfish - not going to put in the effort to fix this if it doesn't ever get used. + int entiesGot = 0; + int expectedEntries = (2 * viewDistance + 1); + expectedEntries *= expectedEntries; +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java +index 46954db7ecd35ac4018fdf476df7c8020d7ce6c8..1ad890a244bdf6df48a8db68cb43450e08c788a6 100644 +--- a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java ++++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java +@@ -5,7 +5,7 @@ import net.minecraft.server.level.ServerPlayer; + /** + * @author Spottedleaf + */ +-public final class PlayerAreaMap extends AreaMap { ++public class PlayerAreaMap extends AreaMap { // Pufferfish - not actually final + + public PlayerAreaMap() { + super(); +diff --git a/src/main/java/gg/airplane/structs/FluidDirectionCache.java b/src/main/java/gg/airplane/structs/FluidDirectionCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aa8467b9dda1f7707e41f50ac7b3e9d7343723ec +--- /dev/null ++++ b/src/main/java/gg/airplane/structs/FluidDirectionCache.java +@@ -0,0 +1,136 @@ ++package gg.airplane.structs; ++ ++import it.unimi.dsi.fastutil.HashCommon; ++ ++/** ++ * This is a replacement for the cache used in FluidTypeFlowing. ++ * The requirements for the previous cache were: ++ * - Store 200 entries ++ * - Look for the flag in the cache ++ * - If it exists, move to front of cache ++ * - If it doesn't exist, remove last entry in cache and insert in front ++ * ++ * This class accomplishes something similar, however has a few different ++ * requirements put into place to make this more optimize: ++ * ++ * - maxDistance is the most amount of entries to be checked, instead ++ * of having to check the entire list. ++ * - In combination with that, entries are all tracked by age and how ++ * frequently they're used. This enables us to remove old entries, ++ * without constantly shifting any around. ++ * ++ * Usage of the previous map would have to reset the head every single usage, ++ * shifting the entire map. Here, nothing happens except an increment when ++ * the cache is hit, and when it needs to replace an old element only a single ++ * element is modified. ++ */ ++public class FluidDirectionCache { ++ ++ private static class FluidDirectionEntry { ++ private final T data; ++ private final boolean flag; ++ private int uses = 0; ++ private int age = 0; ++ ++ private FluidDirectionEntry(T data, boolean flag) { ++ this.data = data; ++ this.flag = flag; ++ } ++ ++ public int getValue() { ++ return this.uses - (this.age >> 1); // age isn't as important as uses ++ } ++ ++ public void incrementUses() { ++ this.uses = this.uses + 1 & Integer.MAX_VALUE; ++ } ++ ++ public void incrementAge() { ++ this.age = this.age + 1 & Integer.MAX_VALUE; ++ } ++ } ++ ++ private final FluidDirectionEntry[] entries; ++ private final int mask; ++ private final int maxDistance; // the most amount of entries to check for a value ++ ++ public FluidDirectionCache(int size) { ++ int arraySize = HashCommon.nextPowerOfTwo(size); ++ this.entries = new FluidDirectionEntry[arraySize]; ++ this.mask = arraySize - 1; ++ this.maxDistance = Math.min(arraySize, 4); ++ } ++ ++ public Boolean getValue(T data) { ++ FluidDirectionEntry curr; ++ int pos; ++ ++ if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) { ++ return null; ++ } else if (data.equals(curr.data)) { ++ curr.incrementUses(); ++ return curr.flag; ++ } ++ ++ int checked = 1; // start at 1 because we already checked the first spot above ++ ++ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { ++ if (data.equals(curr.data)) { ++ curr.incrementUses(); ++ return curr.flag; ++ } else if (++checked >= this.maxDistance) { ++ break; ++ } ++ } ++ ++ return null; ++ } ++ ++ public void putValue(T data, boolean flag) { ++ FluidDirectionEntry curr; ++ int pos; ++ ++ if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) { ++ this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add ++ return; ++ } else if (data.equals(curr.data)) { ++ curr.incrementUses(); ++ return; ++ } ++ ++ int checked = 1; // start at 1 because we already checked the first spot above ++ ++ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { ++ if (data.equals(curr.data)) { ++ curr.incrementUses(); ++ return; ++ } else if (++checked >= this.maxDistance) { ++ this.forceAdd(data, flag); ++ return; ++ } ++ } ++ ++ this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add ++ } ++ ++ private void forceAdd(T data, boolean flag) { ++ int expectedPos = HashCommon.mix(data.hashCode()) & this.mask; ++ ++ int toRemovePos = expectedPos; ++ FluidDirectionEntry entryToRemove = this.entries[toRemovePos]; ++ ++ for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) { ++ int pos = i & this.mask; ++ FluidDirectionEntry entry = this.entries[pos]; ++ if (entry.getValue() < entryToRemove.getValue()) { ++ toRemovePos = pos; ++ entryToRemove = entry; ++ } ++ ++ entry.incrementAge(); // use this as a mechanism to age the other entries ++ } ++ ++ // remove the least used/oldest entry ++ this.entries[toRemovePos] = new FluidDirectionEntry(data, flag); ++ } ++} +diff --git a/src/main/java/gg/airplane/structs/ItemListWithBitset.java b/src/main/java/gg/airplane/structs/ItemListWithBitset.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1b7a4ee47f4445d7f2ac91d3a73ae113edbdddb2 +--- /dev/null ++++ b/src/main/java/gg/airplane/structs/ItemListWithBitset.java +@@ -0,0 +1,114 @@ ++package gg.airplane.structs; ++ ++import net.minecraft.core.NonNullList; ++import net.minecraft.world.item.ItemStack; ++import org.apache.commons.lang.Validate; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.AbstractList; ++import java.util.Arrays; ++import java.util.List; ++ ++public class ItemListWithBitset extends AbstractList { ++ public static ItemListWithBitset fromList(List list) { ++ if (list instanceof ItemListWithBitset ours) { ++ return ours; ++ } ++ return new ItemListWithBitset(list); ++ } ++ ++ private static ItemStack[] createArray(int size) { ++ ItemStack[] array = new ItemStack[size]; ++ Arrays.fill(array, ItemStack.EMPTY); ++ return array; ++ } ++ ++ private final ItemStack[] items; ++ ++ private long bitSet = 0; ++ private final long allBits; ++ ++ private static class OurNonNullList extends NonNullList { ++ protected OurNonNullList(List delegate) { ++ super(delegate, ItemStack.EMPTY); ++ } ++ } ++ ++ public final NonNullList nonNullList = new OurNonNullList(this); ++ ++ private ItemListWithBitset(List list) { ++ this(list.size()); ++ ++ for (int i = 0; i < list.size(); i++) { ++ this.set(i, list.get(i)); ++ } ++ } ++ ++ public ItemListWithBitset(int size) { ++ Validate.isTrue(size < Long.BYTES * 8, "size is too large"); ++ ++ this.items = createArray(size); ++ this.allBits = ((1L << size) - 1); ++ } ++ ++ public boolean isCompletelyEmpty() { ++ return this.bitSet == 0; ++ } ++ ++ public boolean hasFullStacks() { ++ return (this.bitSet & this.allBits) == allBits; ++ } ++ ++ @Override ++ public ItemStack set(int index, @NotNull ItemStack itemStack) { ++ ItemStack existing = this.items[index]; ++ ++ this.items[index] = itemStack; ++ ++ if (itemStack == ItemStack.EMPTY) { ++ this.bitSet &= ~(1L << index); ++ } else { ++ this.bitSet |= 1L << index; ++ } ++ ++ return existing; ++ } ++ ++ @NotNull ++ @Override ++ public ItemStack get(int var0) { ++ return this.items[var0]; ++ } ++ ++ @Override ++ public int size() { ++ return this.items.length; ++ } ++ ++ @Override ++ public void clear() { ++ Arrays.fill(this.items, ItemStack.EMPTY); ++ } ++ ++ // these are unsupported for block inventories which have a static size ++ @Override ++ public void add(int var0, ItemStack var1) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public ItemStack remove(int var0) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public String toString() { ++ return "ItemListWithBitset{" + ++ "items=" + Arrays.toString(items) + ++ ", bitSet=" + Long.toString(bitSet, 2) + ++ ", allBits=" + Long.toString(allBits, 2) + ++ ", size=" + this.items.length + ++ '}'; ++ } ++} +diff --git a/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java b/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a7f297ebb569f7c1f205e967ca485be70013a714 +--- /dev/null ++++ b/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java +@@ -0,0 +1,119 @@ ++package gg.airplane.structs; ++ ++import it.unimi.dsi.fastutil.HashCommon; ++ ++/** ++ * A replacement for the cache used in Biome. ++ */ ++public class Long2FloatAgingCache { ++ ++ private static class AgingEntry { ++ private long data; ++ private float value; ++ private int uses = 0; ++ private int age = 0; ++ ++ private AgingEntry(long data, float value) { ++ this.data = data; ++ this.value = value; ++ } ++ ++ public void replace(long data, float flag) { ++ this.data = data; ++ this.value = flag; ++ } ++ ++ public int getValue() { ++ return this.uses - (this.age >> 1); // age isn't as important as uses ++ } ++ ++ public void incrementUses() { ++ this.uses = this.uses + 1 & Integer.MAX_VALUE; ++ } ++ ++ public void incrementAge() { ++ this.age = this.age + 1 & Integer.MAX_VALUE; ++ } ++ } ++ ++ private final AgingEntry[] entries; ++ private final int mask; ++ private final int maxDistance; // the most amount of entries to check for a value ++ ++ public Long2FloatAgingCache(int size) { ++ int arraySize = HashCommon.nextPowerOfTwo(size); ++ this.entries = new AgingEntry[arraySize]; ++ this.mask = arraySize - 1; ++ this.maxDistance = Math.min(arraySize, 4); ++ } ++ ++ public float getValue(long data) { ++ AgingEntry curr; ++ int pos; ++ ++ if ((curr = this.entries[pos = HashCommon.mix(HashCommon.long2int(data)) & this.mask]) == null) { ++ return Float.NaN; ++ } else if (data == curr.data) { ++ curr.incrementUses(); ++ return curr.value; ++ } ++ ++ int checked = 1; // start at 1 because we already checked the first spot above ++ ++ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { ++ if (data == curr.data) { ++ curr.incrementUses(); ++ return curr.value; ++ } else if (++checked >= this.maxDistance) { ++ break; ++ } ++ } ++ ++ return Float.NaN; ++ } ++ ++ public void putValue(long data, float value) { ++ AgingEntry curr; ++ int pos; ++ ++ if ((curr = this.entries[pos = HashCommon.mix(HashCommon.long2int(data)) & this.mask]) == null) { ++ this.entries[pos] = new AgingEntry(data, value); // add ++ return; ++ } else if (data == curr.data) { ++ curr.incrementUses(); ++ return; ++ } ++ ++ int checked = 1; // start at 1 because we already checked the first spot above ++ ++ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { ++ if (data == curr.data) { ++ curr.incrementUses(); ++ return; ++ } else if (++checked >= this.maxDistance) { ++ this.forceAdd(data, value); ++ return; ++ } ++ } ++ ++ this.entries[pos] = new AgingEntry(data, value); // add ++ } ++ ++ private void forceAdd(long data, float value) { ++ int expectedPos = HashCommon.mix(HashCommon.long2int(data)) & this.mask; ++ AgingEntry entryToRemove = this.entries[expectedPos]; ++ ++ for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) { ++ int pos = i & this.mask; ++ AgingEntry entry = this.entries[pos]; ++ if (entry.getValue() < entryToRemove.getValue()) { ++ entryToRemove = entry; ++ } ++ ++ entry.incrementAge(); // use this as a mechanism to age the other entries ++ } ++ ++ // remove the least used/oldest entry ++ entryToRemove.replace(data, value); ++ } ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..020368da69b9a492155f6de6297f74732f4ab6ea +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java +@@ -0,0 +1,68 @@ ++package gg.pufferfish.pufferfish; ++ ++import java.io.IOException; ++import java.util.Collections; ++import java.util.List; ++import java.util.stream.Collectors; ++import java.util.stream.Stream; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.md_5.bungee.api.ChatColor; ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.Bukkit; ++import org.bukkit.Location; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++ ++public class PufferfishCommand extends Command { ++ ++ public PufferfishCommand() { ++ super("pufferfish"); ++ this.description = "Pufferfish related commands"; ++ this.usageMessage = "/pufferfish [reload | version]"; ++ this.setPermission("bukkit.command.pufferfish"); ++ } ++ ++ public static void init() { ++ MinecraftServer.getServer().server.getCommandMap().register("pufferfish", "Pufferfish", new PufferfishCommand()); ++ } ++ ++ @Override ++ public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { ++ if (args.length == 1) { ++ return Stream.of("reload", "version") ++ .filter(arg -> arg.startsWith(args[0].toLowerCase())) ++ .collect(Collectors.toList()); ++ } ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public boolean execute(CommandSender sender, String commandLabel, String[] args) { ++ if (!testPermission(sender)) return true; ++ String prefix = ChatColor.of("#12fff6") + "" + ChatColor.BOLD + "Pufferfish ยป " + ChatColor.of("#e8f9f9"); ++ ++ if (args.length != 1) { ++ sender.sendMessage(prefix + "Usage: " + usageMessage); ++ args = new String[]{"version"}; ++ } ++ ++ if (args[0].equalsIgnoreCase("reload")) { ++ MinecraftServer console = MinecraftServer.getServer(); ++ try { ++ PufferfishConfig.load(); ++ } catch (IOException e) { ++ sender.sendMessage(Component.text("Failed to reload.", NamedTextColor.RED)); ++ e.printStackTrace(); ++ return true; ++ } ++ console.server.reloadCount++; ++ ++ Command.broadcastCommandMessage(sender, prefix + "Pufferfish configuration has been reloaded."); ++ } else if (args[0].equalsIgnoreCase("version")) { ++ Command.broadcastCommandMessage(sender, prefix + "This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")"); ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6e441a1a28ba72a8b1cc09fe5fca71b3c70627d4 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java +@@ -0,0 +1,285 @@ ++package gg.pufferfish.pufferfish; ++ ++import gg.pufferfish.pufferfish.simd.SIMDDetection; ++import java.io.File; ++import java.io.IOException; ++import java.util.Collections; ++import net.minecraft.core.registries.BuiltInRegistries; ++import java.util.Locale; ++import java.util.Map; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.tags.TagKey; ++import org.apache.logging.log4j.Level; ++import org.bukkit.configuration.ConfigurationSection; ++import net.minecraft.world.entity.EntityType; ++import java.lang.reflect.Method; ++import java.lang.reflect.Modifier; ++import java.util.List; ++import net.minecraft.server.MinecraftServer; ++import org.apache.logging.log4j.Level; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.MemoryConfiguration; ++import org.jetbrains.annotations.Nullable; ++import org.simpleyaml.configuration.comments.CommentType; ++import org.simpleyaml.configuration.file.YamlFile; ++import org.simpleyaml.exceptions.InvalidConfigurationException; ++ ++public class PufferfishConfig { ++ ++ private static final YamlFile config = new YamlFile(); ++ private static int updates = 0; ++ ++ private static ConfigurationSection convertToBukkit(org.simpleyaml.configuration.ConfigurationSection section) { ++ ConfigurationSection newSection = new MemoryConfiguration(); ++ for (String key : section.getKeys(false)) { ++ if (section.isConfigurationSection(key)) { ++ newSection.set(key, convertToBukkit(section.getConfigurationSection(key))); ++ } else { ++ newSection.set(key, section.get(key)); ++ } ++ } ++ return newSection; ++ } ++ ++ public static ConfigurationSection getConfigCopy() { ++ return convertToBukkit(config); ++ } ++ ++ public static int getUpdates() { ++ return updates; ++ } ++ ++ public static void load() throws IOException { ++ File configFile = new File("pufferfish.yml"); ++ ++ if (configFile.exists()) { ++ try { ++ config.load(configFile); ++ } catch (InvalidConfigurationException e) { ++ throw new IOException(e); ++ } ++ } ++ ++ getString("info.version", "1.0"); ++ setComment("info", ++ "Pufferfish Configuration", ++ "Check out Pufferfish Host for maximum performance server hosting: https://pufferfish.host", ++ "Join our Discord for support: https://discord.gg/reZw4vQV9H", ++ "Download new builds at https://ci.pufferfish.host/job/Pufferfish"); ++ ++ for (Method method : PufferfishConfig.class.getDeclaredMethods()) { ++ if (Modifier.isStatic(method.getModifiers()) && Modifier.isPrivate(method.getModifiers()) && method.getParameterCount() == 0 && ++ method.getReturnType() == Void.TYPE && !method.getName().startsWith("lambda")) { ++ method.setAccessible(true); ++ try { ++ method.invoke(null); ++ } catch (Throwable t) { ++ MinecraftServer.LOGGER.warn("Failed to load configuration option from " + method.getName(), t); ++ } ++ } ++ } ++ ++ updates++; ++ ++ config.save(configFile); ++ ++ // Attempt to detect vectorization ++ try { ++ SIMDDetection.isEnabled = SIMDDetection.canEnable(PufferfishLogger.LOGGER); ++ SIMDDetection.versionLimited = SIMDDetection.getJavaVersion() != 17 && SIMDDetection.getJavaVersion() != 18 && SIMDDetection.getJavaVersion() != 19; ++ } catch (NoClassDefFoundError | Exception ignored) { ++ ignored.printStackTrace(); ++ } ++ ++ if (SIMDDetection.isEnabled) { ++ PufferfishLogger.LOGGER.info("SIMD operations detected as functional. Will replace some operations with faster versions."); ++ } else if (SIMDDetection.versionLimited) { ++ PufferfishLogger.LOGGER.warning("Will not enable SIMD! These optimizations are only safely supported on Java 17, Java 18, and Java 19."); ++ } else { ++ PufferfishLogger.LOGGER.warning("SIMD operations are available for your server, but are not configured!"); ++ PufferfishLogger.LOGGER.warning("To enable additional optimizations, add \"--add-modules=jdk.incubator.vector\" to your startup flags, BEFORE the \"-jar\"."); ++ PufferfishLogger.LOGGER.warning("If you have already added this flag, then SIMD operations are not supported on your JVM or CPU."); ++ PufferfishLogger.LOGGER.warning("Debug: Java: " + System.getProperty("java.version") + ", test run: " + SIMDDetection.testRun); ++ } ++ } ++ ++ private static void setComment(String key, String... comment) { ++ if (config.contains(key)) { ++ config.setComment(key, String.join("\n", comment), CommentType.BLOCK); ++ } ++ } ++ ++ private static void ensureDefault(String key, Object defaultValue, String... comment) { ++ if (!config.contains(key)) { ++ config.set(key, defaultValue); ++ config.setComment(key, String.join("\n", comment), CommentType.BLOCK); ++ } ++ } ++ ++ private static boolean getBoolean(String key, boolean defaultValue, String... comment) { ++ return getBoolean(key, null, defaultValue, comment); ++ } ++ ++ private static boolean getBoolean(String key, @Nullable String oldKey, boolean defaultValue, String... comment) { ++ ensureDefault(key, defaultValue, comment); ++ return config.getBoolean(key, defaultValue); ++ } ++ ++ private static int getInt(String key, int defaultValue, String... comment) { ++ return getInt(key, null, defaultValue, comment); ++ } ++ ++ private static int getInt(String key, @Nullable String oldKey, int defaultValue, String... comment) { ++ ensureDefault(key, defaultValue, comment); ++ return config.getInt(key, defaultValue); ++ } ++ ++ private static double getDouble(String key, double defaultValue, String... comment) { ++ return getDouble(key, null, defaultValue, comment); ++ } ++ ++ private static double getDouble(String key, @Nullable String oldKey, double defaultValue, String... comment) { ++ ensureDefault(key, defaultValue, comment); ++ return config.getDouble(key, defaultValue); ++ } ++ ++ private static String getString(String key, String defaultValue, String... comment) { ++ return getOldString(key, null, defaultValue, comment); ++ } ++ ++ private static String getOldString(String key, @Nullable String oldKey, String defaultValue, String... comment) { ++ ensureDefault(key, defaultValue, comment); ++ return config.getString(key, defaultValue); ++ } ++ ++ private static List getStringList(String key, List defaultValue, String... comment) { ++ return getStringList(key, null, defaultValue, comment); ++ } ++ ++ private static List getStringList(String key, @Nullable String oldKey, List defaultValue, String... comment) { ++ ensureDefault(key, defaultValue, comment); ++ return config.getStringList(key); ++ } ++ ++ public static String sentryDsn; ++ private static void sentry() { ++ String sentryEnvironment = System.getenv("SENTRY_DSN"); ++ String sentryConfig = getString("sentry-dsn", "", "Sentry DSN for improved error logging, leave blank to disable", "Obtain from https://sentry.io/"); ++ ++ sentryDsn = sentryEnvironment == null ? sentryConfig : sentryEnvironment; ++ if (sentryDsn != null && !sentryDsn.isBlank()) { ++ gg.pufferfish.pufferfish.sentry.SentryManager.init(); ++ } ++ } ++ ++ public static boolean enableBooks; ++ private static void books() { ++ enableBooks = getBoolean("enable-books", true, ++ "Whether or not books should be writeable.", ++ "Servers that anticipate being a target for duping may want to consider", ++ "disabling this option.", ++ "This can be overridden per-player with the permission pufferfish.usebooks"); ++ } ++ ++ public static boolean enableSuffocationOptimization; ++ private static void suffocationOptimization() { ++ enableSuffocationOptimization = getBoolean("enable-suffocation-optimization", true, ++ "Optimizes the suffocation check by selectively skipping", ++ "the check in a way that still appears vanilla. This should", ++ "be left enabled on most servers, but is provided as a", ++ "configuration option if the vanilla deviation is undesirable."); ++ } ++ ++ public static boolean enableAsyncMobSpawning; ++ public static boolean asyncMobSpawningInitialized; ++ private static void asyncMobSpawning() { ++ boolean temp = getBoolean("enable-async-mob-spawning", true, ++ "Whether or not asynchronous mob spawning should be enabled.", ++ "On servers with many entities, this can improve performance by up to 15%. You must have", ++ "paper's per-player-mob-spawns setting set to true for this to work.", ++ "One quick note - this does not actually spawn mobs async (that would be very unsafe).", ++ "This just offloads some expensive calculations that are required for mob spawning."); ++ ++ // This prevents us from changing the value during a reload. ++ if (!asyncMobSpawningInitialized) { ++ asyncMobSpawningInitialized = true; ++ enableAsyncMobSpawning = temp; ++ } ++ } ++ ++ public static int maxProjectileLoadsPerTick; ++ public static int maxProjectileLoadsPerProjectile; ++ private static void projectileLoading() { ++ maxProjectileLoadsPerTick = getInt("projectile.max-loads-per-tick", 10, "Controls how many chunks are allowed", "to be sync loaded by projectiles in a tick."); ++ maxProjectileLoadsPerProjectile = getInt("projectile.max-loads-per-projectile", 10, "Controls how many chunks a projectile", "can load in its lifetime before it gets", "automatically removed."); ++ ++ setComment("projectile", "Optimizes projectile settings"); ++ } ++ ++ ++ public static boolean dearEnabled; ++ public static int startDistance; ++ public static int startDistanceSquared; ++ public static int maximumActivationPrio; ++ public static int activationDistanceMod; ++ ++ private static void dynamicActivationOfBrains() throws IOException { ++ dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", true); ++ startDistance = getInt("dab.start-distance", "activation-range.start-distance", 12, ++ "This value determines how far away an entity has to be", ++ "from the player to start being effected by DEAR."); ++ startDistanceSquared = startDistance * startDistance; ++ maximumActivationPrio = getInt("dab.max-tick-freq", "activation-range.max-tick-freq", 20, ++ "This value defines how often in ticks, the furthest entity", ++ "will get their pathfinders and behaviors ticked. 20 = 1s"); ++ activationDistanceMod = getInt("dab.activation-dist-mod", "activation-range.activation-dist-mod", 8, ++ "This value defines how much distance modifies an entity's", ++ "tick frequency. freq = (distanceToPlayer^2) / (2^value)", ++ "If you want further away entities to tick less often, use 7.", ++ "If you want further away entities to tick more often, try 9."); ++ ++ for (EntityType entityType : BuiltInRegistries.ENTITY_TYPE) { ++ entityType.dabEnabled = true; // reset all, before setting the ones to true ++ } ++ getStringList("dab.blacklisted-entities", "activation-range.blacklisted-entities", Collections.emptyList(), "A list of entities to ignore for activation") ++ .forEach(name -> EntityType.byString(name).ifPresentOrElse(entityType -> { ++ entityType.dabEnabled = false; ++ }, () -> MinecraftServer.LOGGER.warn("Unknown entity \"" + name + "\""))); ++ ++ setComment("dab", "Optimizes entity brains when", "they're far away from the player"); ++ } ++ ++ public static Map projectileTimeouts; ++ private static void projectileTimeouts() { ++ // Set some defaults ++ getInt("entity_timeouts.SNOWBALL", -1); ++ getInt("entity_timeouts.LLAMA_SPIT", -1); ++ setComment("entity_timeouts", ++ "These values define a entity's maximum lifespan. If an", ++ "entity is in this list and it has survived for longer than", ++ "that number of ticks, then it will be removed. Setting a value to", ++ "-1 disables this feature."); ++ ++ for (EntityType entityType : BuiltInRegistries.ENTITY_TYPE) { ++ String type = EntityType.getKey(entityType).getPath().toUpperCase(Locale.ROOT); ++ entityType.ttl = config.getInt("entity_timeouts." + type, -1); ++ } ++ } ++ ++ public static boolean throttleInactiveGoalSelectorTick; ++ private static void inactiveGoalSelectorThrottle() { ++ getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", true, ++ "Throttles the AI goal selector in entity inactive ticks.", ++ "This can improve performance by a few percent, but has minor gameplay implications."); ++ } ++ ++ ++ public static boolean disableMethodProfiler; ++ public static boolean disableOutOfOrderChat; ++ private static void miscSettings() { ++ disableMethodProfiler = getBoolean("misc.disable-method-profiler", true); ++ disableOutOfOrderChat = getBoolean("misc.disable-out-of-order-chat", false); ++ setComment("misc", "Settings for things that don't belong elsewhere"); ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java +new file mode 100644 +index 0000000000000000000000000000000000000000..53f2df00c6809618a9ee3d2ea72e85e8052fbcf1 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java +@@ -0,0 +1,16 @@ ++package gg.pufferfish.pufferfish; ++ ++import java.util.logging.Level; ++import java.util.logging.Logger; ++import org.bukkit.Bukkit; ++ ++public class PufferfishLogger extends Logger { ++ public static final PufferfishLogger LOGGER = new PufferfishLogger(); ++ ++ private PufferfishLogger() { ++ super("Pufferfish", null); ++ ++ setParent(Bukkit.getLogger()); ++ setLevel(Level.ALL); ++ } ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e877921370f6009a4bd204d9b17d2d58834b8822 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java +@@ -0,0 +1,136 @@ ++package gg.pufferfish.pufferfish; ++ ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.NamedTextColor.GREEN; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++import com.destroystokyo.paper.VersionHistoryManager; ++import com.destroystokyo.paper.util.VersionFetcher; ++import com.google.gson.Gson; ++import com.google.gson.JsonObject; ++import java.io.IOException; ++import java.net.URI; ++import java.net.http.HttpClient; ++import java.net.http.HttpRequest; ++import java.net.http.HttpResponse; ++import java.nio.charset.StandardCharsets; ++import java.util.concurrent.TimeUnit; ++import java.util.logging.Level; ++import java.util.logging.Logger; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.JoinConfiguration; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import org.bukkit.craftbukkit.CraftServer; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++public class PufferfishVersionFetcher implements VersionFetcher { ++ ++ private static final Logger LOGGER = Logger.getLogger("PufferfishVersionFetcher"); ++ private static final HttpClient client = HttpClient.newHttpClient(); ++ ++ private static final URI JENKINS_URI = URI.create("https://ci.pufferfish.host/job/Pufferfish-1.19/lastSuccessfulBuild/buildNumber"); ++ private static final String GITHUB_FORMAT = "https://api.github.com/repos/pufferfish-gg/Pufferfish/compare/ver/1.19...%s"; ++ ++ private static final HttpResponse.BodyHandler JSON_OBJECT_BODY_HANDLER = responseInfo -> HttpResponse.BodySubscribers ++ .mapping( ++ HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), ++ string -> new Gson().fromJson(string, JsonObject.class) ++ ); ++ ++ @Override ++ public long getCacheTime() { ++ return TimeUnit.MINUTES.toMillis(30); ++ } ++ ++ @Override ++ public @NotNull Component getVersionMessage(final @NotNull String serverVersion) { ++ final String[] parts = CraftServer.class.getPackage().getImplementationVersion().split("-"); ++ @NotNull Component component; ++ ++ if (parts.length != 3) { ++ component = text("Unknown server version.", RED); ++ } else { ++ final String versionString = parts[2]; ++ ++ try { ++ component = this.fetchJenkinsVersion(Integer.parseInt(versionString)); ++ } catch (NumberFormatException e) { ++ component = this.fetchGithubVersion(versionString.substring(1, versionString.length() - 1)); ++ } ++ } ++ ++ final @Nullable Component history = this.getHistory(); ++ return history != null ? Component ++ .join(JoinConfiguration.noSeparators(), component, Component.newline(), this.getHistory()) : component; ++ } ++ ++ private @NotNull Component fetchJenkinsVersion(final int versionNumber) { ++ final HttpRequest request = HttpRequest.newBuilder(JENKINS_URI).build(); ++ try { ++ final HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ++ if (response.statusCode() != 200) { ++ return text("Received invalid status code (" + response.statusCode() + ") from server.", RED); ++ } ++ ++ int latestVersionNumber; ++ try { ++ latestVersionNumber = Integer.parseInt(response.body()); ++ } catch (NumberFormatException e) { ++ LOGGER.log(Level.WARNING, "Received invalid response from Jenkins \"" + response.body() + "\"."); ++ return text("Received invalid response from server.", RED); ++ } ++ ++ final int versionDiff = latestVersionNumber - versionNumber; ++ return this.getResponseMessage(versionDiff); ++ } catch (IOException | InterruptedException e) { ++ LOGGER.log(Level.WARNING, "Failed to look up version from Jenkins", e); ++ return text("Failed to retrieve version from server.", RED); ++ } ++ } ++ ++ // Based off code contributed by Techcable in Paper/GH-65 ++ private @NotNull Component fetchGithubVersion(final @NotNull String hash) { ++ final URI uri = URI.create(String.format(GITHUB_FORMAT, hash)); ++ final HttpRequest request = HttpRequest.newBuilder(uri).build(); ++ try { ++ final HttpResponse response = client.send(request, JSON_OBJECT_BODY_HANDLER); ++ if (response.statusCode() != 200) { ++ return text("Received invalid status code (" + response.statusCode() + ") from server.", RED); ++ } ++ ++ final JsonObject obj = response.body(); ++ final int versionDiff = obj.get("behind_by").getAsInt(); ++ ++ return this.getResponseMessage(versionDiff); ++ } catch (IOException | InterruptedException e) { ++ LOGGER.log(Level.WARNING, "Failed to look up version from GitHub", e); ++ return text("Failed to retrieve version from server.", RED); ++ } ++ } ++ ++ private @NotNull Component getResponseMessage(final int versionDiff) { ++ return switch (Math.max(-1, Math.min(1, versionDiff))) { ++ case -1 -> text("You are running an unsupported version of Pufferfish.", RED); ++ case 0 -> text("You are on the latest version!", GREEN); ++ default -> text("You are running " + versionDiff + " version" + (versionDiff == 1 ? "" : "s") + " beyond. " + ++ "Please update your server when possible to maintain stability, security, and receive the latest optimizations.", ++ RED); ++ }; ++ } ++ ++ private @Nullable Component getHistory() { ++ final VersionHistoryManager.VersionData data = VersionHistoryManager.INSTANCE.getVersionData(); ++ if (data == null) { ++ return null; ++ } ++ ++ final String oldVersion = data.getOldVersion(); ++ if (oldVersion == null) { ++ return null; ++ } ++ ++ return Component.text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java +new file mode 100644 +index 0000000000000000000000000000000000000000..731ef11c7a025ae95ed8a757b530d834733d0621 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java +@@ -0,0 +1,135 @@ ++package gg.pufferfish.pufferfish.sentry; ++ ++import com.google.common.reflect.TypeToken; ++import com.google.gson.Gson; ++import io.sentry.Breadcrumb; ++import io.sentry.Sentry; ++import io.sentry.SentryEvent; ++import io.sentry.SentryLevel; ++import io.sentry.protocol.Message; ++import io.sentry.protocol.User; ++import java.util.Map; ++import org.apache.logging.log4j.Level; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Marker; ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.Logger; ++import org.apache.logging.log4j.core.appender.AbstractAppender; ++import org.apache.logging.log4j.core.filter.AbstractFilter; ++ ++public class PufferfishSentryAppender extends AbstractAppender { ++ ++ private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(PufferfishSentryAppender.class); ++ private static final Gson GSON = new Gson(); ++ ++ public PufferfishSentryAppender() { ++ super("PufferfishSentryAdapter", new SentryFilter(), null); ++ } ++ ++ @Override ++ public void append(LogEvent logEvent) { ++ if (logEvent.getThrown() != null && logEvent.getLevel().isMoreSpecificThan(Level.WARN)) { ++ try { ++ logException(logEvent); ++ } catch (Exception e) { ++ logger.warn("Failed to log event with sentry", e); ++ } ++ } else { ++ try { ++ logBreadcrumb(logEvent); ++ } catch (Exception e) { ++ logger.warn("Failed to log event with sentry", e); ++ } ++ } ++ } ++ ++ private void logException(LogEvent e) { ++ SentryEvent event = new SentryEvent(e.getThrown()); ++ ++ Message sentryMessage = new Message(); ++ sentryMessage.setMessage(e.getMessage().getFormattedMessage()); ++ ++ event.setThrowable(e.getThrown()); ++ event.setLevel(getLevel(e.getLevel())); ++ event.setLogger(e.getLoggerName()); ++ event.setTransaction(e.getLoggerName()); ++ event.setExtra("thread_name", e.getThreadName()); ++ ++ boolean hasContext = e.getContextData() != null; ++ ++ if (hasContext && e.getContextData().containsKey("pufferfishsentry_playerid")) { ++ User user = new User(); ++ user.setId(e.getContextData().getValue("pufferfishsentry_playerid")); ++ user.setUsername(e.getContextData().getValue("pufferfishsentry_playername")); ++ event.setUser(user); ++ } ++ ++ if (hasContext && e.getContextData().containsKey("pufferfishsentry_pluginname")) { ++ event.setExtra("plugin.name", e.getContextData().getValue("pufferfishsentry_pluginname")); ++ event.setExtra("plugin.version", e.getContextData().getValue("pufferfishsentry_pluginversion")); ++ event.setTransaction(e.getContextData().getValue("pufferfishsentry_pluginname")); ++ } ++ ++ if (hasContext && e.getContextData().containsKey("pufferfishsentry_eventdata")) { ++ Map eventFields = GSON.fromJson((String) e.getContextData().getValue("pufferfishsentry_eventdata"), new TypeToken>() {}.getType()); ++ if (eventFields != null) { ++ event.setExtra("event", eventFields); ++ } ++ } ++ ++ Sentry.captureEvent(event); ++ } ++ ++ private void logBreadcrumb(LogEvent e) { ++ Breadcrumb breadcrumb = new Breadcrumb(); ++ ++ breadcrumb.setLevel(getLevel(e.getLevel())); ++ breadcrumb.setCategory(e.getLoggerName()); ++ breadcrumb.setType(e.getLoggerName()); ++ breadcrumb.setMessage(e.getMessage().getFormattedMessage()); ++ ++ Sentry.addBreadcrumb(breadcrumb); ++ } ++ ++ private SentryLevel getLevel(Level level) { ++ switch (level.getStandardLevel()) { ++ case TRACE: ++ case DEBUG: ++ return SentryLevel.DEBUG; ++ case WARN: ++ return SentryLevel.WARNING; ++ case ERROR: ++ return SentryLevel.ERROR; ++ case FATAL: ++ return SentryLevel.FATAL; ++ case INFO: ++ default: ++ return SentryLevel.INFO; ++ } ++ } ++ ++ private static class SentryFilter extends AbstractFilter { ++ ++ @Override ++ public Result filter(Logger logger, org.apache.logging.log4j.Level level, Marker marker, String msg, ++ Object... params) { ++ return this.filter(logger.getName()); ++ } ++ ++ @Override ++ public Result filter(Logger logger, org.apache.logging.log4j.Level level, Marker marker, Object msg, Throwable t) { ++ return this.filter(logger.getName()); ++ } ++ ++ @Override ++ public Result filter(LogEvent event) { ++ return this.filter(event == null ? null : event.getLoggerName()); ++ } ++ ++ private Result filter(String loggerName) { ++ return loggerName != null && loggerName.startsWith("gg.castaway.pufferfish.sentry") ? Result.DENY ++ : Result.NEUTRAL; ++ } ++ ++ } ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1b29210ad0bbb4ada150f23357f0c80d331c996d +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java +@@ -0,0 +1,40 @@ ++package gg.pufferfish.pufferfish.sentry; ++ ++import gg.pufferfish.pufferfish.PufferfishConfig; ++import io.sentry.Sentry; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++ ++public class SentryManager { ++ ++ private static final Logger logger = LogManager.getLogger(SentryManager.class); ++ ++ private SentryManager() { ++ ++ } ++ ++ private static boolean initialized = false; ++ ++ public static synchronized void init() { ++ if (initialized) { ++ return; ++ } ++ try { ++ initialized = true; ++ ++ Sentry.init(options -> { ++ options.setDsn(PufferfishConfig.sentryDsn); ++ options.setMaxBreadcrumbs(100); ++ }); ++ ++ PufferfishSentryAppender appender = new PufferfishSentryAppender(); ++ appender.start(); ++ ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(appender); ++ logger.info("Sentry logging started!"); ++ } catch (Exception e) { ++ logger.warn("Failed to initialize sentry!", e); ++ initialized = false; ++ } ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java b/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e5323d5d9af25c8a85c4b34a6be76cfc54384cf +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java +@@ -0,0 +1,73 @@ ++package gg.pufferfish.pufferfish.util; ++ ++import com.google.common.collect.Queues; ++import gg.pufferfish.pufferfish.PufferfishLogger; ++import java.util.Queue; ++import java.util.concurrent.locks.Condition; ++import java.util.concurrent.locks.Lock; ++import java.util.concurrent.locks.ReentrantLock; ++import java.util.logging.Level; ++ ++public class AsyncExecutor implements Runnable { ++ ++ private final Queue jobs = Queues.newArrayDeque(); ++ private final Lock mutex = new ReentrantLock(); ++ private final Condition cond = mutex.newCondition(); ++ private final Thread thread; ++ private volatile boolean killswitch = false; ++ ++ public AsyncExecutor(String threadName) { ++ this.thread = new Thread(this, threadName); ++ } ++ ++ public void start() { ++ thread.start(); ++ } ++ ++ public void kill() { ++ killswitch = true; ++ cond.signalAll(); ++ } ++ ++ public void submit(Runnable runnable) { ++ mutex.lock(); ++ try { ++ jobs.offer(runnable); ++ cond.signalAll(); ++ } finally { ++ mutex.unlock(); ++ } ++ } ++ ++ @Override ++ public void run() { ++ while (!killswitch) { ++ try { ++ Runnable runnable = takeRunnable(); ++ if (runnable != null) { ++ runnable.run(); ++ } ++ } catch (InterruptedException e) { ++ Thread.currentThread().interrupt(); ++ } catch (Exception e) { ++ PufferfishLogger.LOGGER.log(Level.SEVERE, e, () -> "Failed to execute async job for thread " + thread.getName()); ++ } ++ } ++ } ++ ++ private Runnable takeRunnable() throws InterruptedException { ++ mutex.lock(); ++ try { ++ while (jobs.isEmpty() && !killswitch) { ++ cond.await(); ++ } ++ ++ if (jobs.isEmpty()) return null; // We've set killswitch ++ ++ return jobs.remove(); ++ } finally { ++ mutex.unlock(); ++ } ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java b/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fdcb62d12164024a5f354d60cc863821a18d1b2a +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java +@@ -0,0 +1,31 @@ ++package gg.pufferfish.pufferfish.util; ++ ++import com.destroystokyo.paper.util.misc.PlayerAreaMap; ++import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; ++import java.util.concurrent.ConcurrentHashMap; ++import net.minecraft.server.level.ServerPlayer; ++ ++public final class AsyncPlayerAreaMap extends PlayerAreaMap { ++ ++ public AsyncPlayerAreaMap() { ++ super(); ++ this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); ++ } ++ ++ public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets) { ++ super(pooledHashSets); ++ this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); ++ } ++ ++ public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, ++ final ChangeCallback removeCallback) { ++ this(pooledHashSets, addCallback, removeCallback, null); ++ } ++ ++ public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, ++ final ChangeCallback removeCallback, final ChangeSourceCallback changeSourceCallback) { ++ super(pooledHashSets, addCallback, removeCallback, changeSourceCallback); ++ this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java b/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c1929840254a3e6d721816f4a20415bea1742580 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java +@@ -0,0 +1,20 @@ ++package gg.pufferfish.pufferfish.util; ++ ++import java.util.Iterator; ++import org.jetbrains.annotations.NotNull; ++ ++public class IterableWrapper implements Iterable { ++ ++ private final Iterator iterator; ++ ++ public IterableWrapper(Iterator iterator) { ++ this.iterator = iterator; ++ } ++ ++ @NotNull ++ @Override ++ public Iterator iterator() { ++ return iterator; ++ } ++ ++} +diff --git a/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java b/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..facd55463d44cb7e3d2ca6892982f5497b8dded1 +--- /dev/null ++++ b/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java +@@ -0,0 +1,40 @@ ++package gg.pufferfish.pufferfish.util; ++ ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import java.util.Map; ++import org.jetbrains.annotations.Nullable; ++ ++public class Long2ObjectOpenHashMapWrapper extends Long2ObjectOpenHashMap { ++ ++ private final Map backingMap; ++ ++ public Long2ObjectOpenHashMapWrapper(Map map) { ++ backingMap = map; ++ } ++ ++ @Override ++ public V put(Long key, V value) { ++ return backingMap.put(key, value); ++ } ++ ++ @Override ++ public V get(Object key) { ++ return backingMap.get(key); ++ } ++ ++ @Override ++ public V remove(Object key) { ++ return backingMap.remove(key); ++ } ++ ++ @Nullable ++ @Override ++ public V putIfAbsent(Long key, V value) { ++ return backingMap.putIfAbsent(key, value); ++ } ++ ++ @Override ++ public int size() { ++ return backingMap.size(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 8d442c5a498ecf288a0cc0c54889c6e2fda849ce..01bdf134fc21220ab7ecca51f2dcd51c0b466bba 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -7,6 +7,7 @@ import net.kyori.adventure.text.Component; + import net.kyori.adventure.text.format.NamedTextColor; + import net.minecraft.network.protocol.Packet; + import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; ++import org.bukkit.Bukkit; // Pufferfish + import org.checkerframework.checker.nullness.qual.Nullable; + import org.spongepowered.configurate.objectmapping.ConfigSerializable; + import org.spongepowered.configurate.objectmapping.meta.Comment; +@@ -16,6 +17,7 @@ import org.spongepowered.configurate.objectmapping.meta.Setting; + import java.util.List; + import java.util.Map; + import java.util.Objects; ++import java.util.logging.Level; // Pufferfish + + @SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) + public class GlobalConfiguration extends ConfigurationPart { +@@ -51,6 +53,7 @@ public class GlobalConfiguration extends ConfigurationPart { + + public class Timings extends ConfigurationPart.Post { + public boolean enabled = true; ++ public boolean reallyEnabled = false; + public boolean verbose = true; + public String url = "https://timings.aikar.co/"; + public boolean serverNamePrivacy = false; +@@ -64,6 +67,14 @@ public class GlobalConfiguration extends ConfigurationPart { + + @Override + public void postProcess() { ++ // Pufferfish start ++ if (enabled && !reallyEnabled) { ++ Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] To improve performance, timings have been disabled by default"); ++ Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] You can still use timings by using /timings on, but they will not start on server startup unless you set timings.really-enabled to true in paper.yml"); ++ Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] If you would like to disable this message, either set timings.really-enabled to true or timings.enabled to false."); ++ } ++ enabled = reallyEnabled; ++ // Pufferfish end + MinecraftTimings.processConfig(this); + } + } +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +index 6efb8b10f17c70b05128039376d254e6beda3841..57e8c6673c7cfe447a75f15506e8000062d813fe 100644 +--- a/src/main/java/io/papermc/paper/util/MCUtil.java ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -210,7 +210,7 @@ public final class MCUtil { + } + + public static long getCoordinateKey(final Entity entity) { +- return ((long)(MCUtil.fastFloor(entity.getZ()) >> 4) << 32) | ((MCUtil.fastFloor(entity.getX()) >> 4) & 0xFFFFFFFFL); ++ return ((long)(entity.blockPosition.getZ() >> 4) << 32) | ((entity.blockPosition.getX() >> 4) & 0xFFFFFFFFL); // Pufferfish - eliminate double->long cast in hotpath + } + + public static long getCoordinateKey(final ChunkPos pair) { +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 753a917d34a1e1c1521a8916bc8e44a6acd90a46..33a5e900c2cab99c311fa5f5b71a609cf8f802cb 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -309,6 +309,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +@@ -1654,7 +1656,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return "Pufferfish"; // Pufferfish - Pufferfish > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! + } + + public SystemReport fillSystemReport(SystemReport details) { +@@ -2238,6 +2240,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop passengers = parent.getPassengers(); ++ ++ for (int i = 0, size = passengers.size(); i < size; i++) { ++ Entity entity = passengers.get(i); ++ int range = entity.getType().clientTrackingRange() * 16; ++ range = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, range); // Paper ++ ++ if (range > highest) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic // Tuinity - not anymore! ++ highest = range; ++ } ++ ++ highest = getHighestRange(entity, highest); ++ } ++ ++ return highest; ++ } ++ + private int getEffectiveRange() { + int i = this.range; ++ // Pufferfish start - remove iterators and streams ++ /* + Iterator iterator = this.entity.getIndirectPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1637,6 +1657,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + i = j; + } + } ++ */ ++ i = getHighestRange(this.entity, i); ++ // Pufferfish end + + return this.scaledRange(i); + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index ca84eddbdb1e198b899750e5f6b3eafd25ce970f..c6f5d6756fa0e068a462d9c0ded12e0771abba37 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -76,6 +76,9 @@ public class ServerChunkCache extends ChunkSource { + final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); + + private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; ++ ++ public boolean firstRunSpawnCounts = true; // Pufferfish ++ public final java.util.concurrent.atomic.AtomicBoolean _pufferfish_spawnCountsReady = new java.util.concurrent.atomic.AtomicBoolean(false); // Pufferfish - optimize countmobs + + private static int getChunkCacheKey(int x, int z) { + return x & 3 | ((z & 3) << 2); +@@ -703,6 +706,7 @@ public class ServerChunkCache extends ChunkSource { + ProfilerFiller gameprofilerfiller = this.level.getProfiler(); + + gameprofilerfiller.push("pollingChunks"); ++ this.level.resetIceAndSnowTick(); // Pufferfish - reset ice & snow tick random + int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit + +@@ -712,18 +716,25 @@ public class ServerChunkCache extends ChunkSource { + // Paper start - per player mob spawning + NaturalSpawner.SpawnState spawnercreature_d; // moved down + if ((this.spawnFriendlies || this.spawnEnemies) && this.chunkMap.playerMobDistanceMap != null) { // don't count mobs when animals and monsters are disabled +- // re-set mob counts +- for (ServerPlayer player : this.level.players) { +- Arrays.fill(player.mobCounts, 0); ++ // Pufferfish start - moved down when async processing ++ if (!gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning) { ++ // re-set mob counts ++ for (ServerPlayer player : this.level.players) { ++ Arrays.fill(player.mobCounts, 0); ++ } ++ lastSpawnState = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true); + } +- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true); ++ // Pufferfish end + } else { +- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ // Pufferfish start - this is only implemented for per-player mob spawning so this makes everything work if this setting is disabled. ++ lastSpawnState = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ _pufferfish_spawnCountsReady.set(true); ++ // Pufferfish end + } + // Paper end + this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings + +- this.lastSpawnState = spawnercreature_d; ++ //this.lastSpawnState = spawnercreature_d; // Pufferfish - this is managed asynchronously + gameprofilerfiller.popPush("filteringLoadedChunks"); + // Paper - moved down + this.level.timings.chunkTicks.startTiming(); // Paper +@@ -761,8 +772,8 @@ public class ServerChunkCache extends ChunkSource { + + if ((true || this.level.isNaturalSpawningAllowed(chunkcoordintpair)) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning // Paper - the chunk is known ticking + chunk1.incrementInhabitedTime(j); +- if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration +- NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); ++ if (flag2 && (!gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning || _pufferfish_spawnCountsReady.get()) && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration ++ NaturalSpawner.spawnForChunk(this.level, chunk1, lastSpawnState, this.spawnFriendlies, this.spawnEnemies, flag1); // Pufferfish + } + + if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - the chunk is known ticking +@@ -824,6 +835,30 @@ public class ServerChunkCache extends ChunkSource { + } + // Paper end - controlled flush for entity tracker packets + } ++ ++ // Pufferfish start - optimize mob spawning ++ if (gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning) { ++ for (ServerPlayer player : this.level.players) { ++ Arrays.fill(player.mobCounts, 0); ++ } ++ if (firstRunSpawnCounts) { ++ firstRunSpawnCounts = false; ++ _pufferfish_spawnCountsReady.set(true); ++ } ++ if (chunkMap.playerMobDistanceMap != null && _pufferfish_spawnCountsReady.getAndSet(false)) { ++ net.minecraft.server.MinecraftServer.getServer().mobSpawnExecutor.submit(() -> { ++ int mapped = distanceManager.getNaturalSpawnChunkCount(); ++ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator objectiterator = ++ level.entityTickList.entities.iterator(io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); ++ gg.pufferfish.pufferfish.util.IterableWrapper wrappedIterator = ++ new gg.pufferfish.pufferfish.util.IterableWrapper<>(objectiterator); ++ lastSpawnState = NaturalSpawner.createState(mapped, wrappedIterator, this::getFullChunk, null, true); ++ objectiterator.finishedIterating(); ++ _pufferfish_spawnCountsReady.set(true); ++ }); ++ } ++ } ++ // Pufferfish end + } + + private void getFullChunk(long pos, Consumer chunkConsumer) { +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 190e9761087baec5827d722a8281f0ffb6798341..50cf4d200bc2892f2140c9929193b4b20ad2bd17 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -170,6 +170,7 @@ public class ServerEntity { + boolean flag4 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L; + + if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround() && !(io.papermc.paper.configuration.GlobalConfiguration.get().collisions.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Paper - send full pos for hard colliding entities to prevent collision problems due to desync ++ if (flag2 || flag3 || this.entity instanceof AbstractArrow) { // Pufferfish + if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) { + if (flag2) { + packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.isOnGround()); +@@ -179,6 +180,7 @@ public class ServerEntity { + } else { + packet1 = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), (byte) i, (byte) j, this.entity.isOnGround()); + } ++ } // Pufferfish + } else { + this.wasOnGround = this.entity.isOnGround(); + this.teleportDelay = 0; +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index e84c67f02bce4c2f9c4eeca1b888d53377fb20d7..619ee9d8b99970fb6fce19438f29e09858412ac4 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -709,6 +709,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + org.spigotmc.ActivationRange.activateEntities(this); // Spigot + timings.entityTick.startTiming(); // Spigot + this.entityTickList.forEach((entity) -> { ++ entity.activatedPriorityReset = false; // Pufferfish - DAB + if (!entity.isRemoved()) { + if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed + entity.discard(); +@@ -728,7 +729,20 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + gameprofilerfiller.push("tick"); +- this.guardEntityTick(this::tickNonPassenger, entity); ++ // Pufferfish start - copied from this.guardEntityTick ++ try { ++ this.tickNonPassenger(entity); // Pufferfish - changed ++ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick ++ } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper ++ // Paper start - Prevent tile entity and entity crashes ++ final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level.getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); ++ MinecraftServer.LOGGER.error(msg, throwable); ++ getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); ++ entity.discard(); ++ // Paper end ++ } ++ // Pufferfish end + gameprofilerfiller.pop(); + } + } +@@ -793,9 +807,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // Paper start - optimise random block ticking + private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); +- private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); ++ // private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(); // Pufferfish - moved to super + // Paper end + ++ private int currentIceAndSnowTick = 0; protected void resetIceAndSnowTick() { this.currentIceAndSnowTick = this.randomTickRandom.nextInt(16); } // Pufferfish ++ + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + ChunkPos chunkcoordintpair = chunk.getPos(); + boolean flag = this.isRaining(); +@@ -806,7 +822,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.push("thunder"); + final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change + +- if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - disable thunder ++ if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && /*this.random.nextInt(this.spigotConfig.thunderChance) == 0 &&*/ chunk.shouldDoLightning(this.random)) { // Spigot // Paper - disable thunder // Pufferfish - replace random with shouldDoLightning + blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper + if (this.isRainingAt(blockposition)) { + DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); +@@ -836,7 +852,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.popPush("iceandsnow"); + int l; + +- if (!this.paperConfig().environment.disableIceAndSnow && this.random.nextInt(16) == 0) { // Paper - Disable ice and snow ++ if (!this.paperConfig().environment.disableIceAndSnow && (this.currentIceAndSnowTick++ & 15) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking // Pufferfish - optimize further random ticking + // Paper start - optimise chunk ticking + this.getRandomBlockPosition(j, 0, k, 15, blockposition); + int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition.getX() & 15, blockposition.getZ() & 15) + 1; +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index d9c29725e015cb4684ff2d547352505d2ad8a2fe..c6a1bde35274bdd0e008a6ca5af006a5d7bcd8bb 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -1204,6 +1204,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + @Override + public void handleEditBook(ServerboundEditBookPacket packet) { ++ if (!gg.pufferfish.pufferfish.PufferfishConfig.enableBooks && !this.player.getBukkitEntity().hasPermission("pufferfish.usebooks")) return; // Pufferfish + // Paper start + if (!this.cserver.isPrimaryThread()) { + List pageList = packet.getPages(); +@@ -2352,6 +2353,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + + private boolean updateChatOrder(Instant timestamp) { ++ if (gg.pufferfish.pufferfish.PufferfishConfig.disableOutOfOrderChat) return true; + Instant instant1; + + do { +diff --git a/src/main/java/net/minecraft/world/CompoundContainer.java b/src/main/java/net/minecraft/world/CompoundContainer.java +index 241fec02e6869c638d3a160819b32173a081467b..6a8f9e8f5bf108674c47018def28906e2d0a729c 100644 +--- a/src/main/java/net/minecraft/world/CompoundContainer.java ++++ b/src/main/java/net/minecraft/world/CompoundContainer.java +@@ -1,5 +1,6 @@ + package net.minecraft.world; + ++import net.minecraft.core.Direction; // Pufferfish + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.item.ItemStack; + +@@ -64,6 +65,23 @@ public class CompoundContainer implements Container { + this.container2 = second; + } + ++ // Pufferfish start ++ @Override ++ public boolean hasEmptySlot(Direction enumdirection) { ++ return this.container1.hasEmptySlot(null) || this.container2.hasEmptySlot(null); ++ } ++ ++ @Override ++ public boolean isCompletelyFull(Direction enumdirection) { ++ return this.container1.isCompletelyFull(null) && this.container2.isCompletelyFull(null); ++ } ++ ++ @Override ++ public boolean isCompletelyEmpty(Direction enumdirection) { ++ return this.container1.isCompletelyEmpty(null) && this.container2.isCompletelyEmpty(null); ++ } ++ // Pufferfish end ++ + @Override + public int getContainerSize() { + return this.container1.getContainerSize() + this.container2.getContainerSize(); +diff --git a/src/main/java/net/minecraft/world/Container.java b/src/main/java/net/minecraft/world/Container.java +index 540bc9500c35c0db719b00aa26f6fb3a1b08ed9f..806cb760822a99316b08ad95ff8922df717050e2 100644 +--- a/src/main/java/net/minecraft/world/Container.java ++++ b/src/main/java/net/minecraft/world/Container.java +@@ -2,6 +2,8 @@ package net.minecraft.world; + + import java.util.Set; + import java.util.function.Predicate; ++ ++import net.minecraft.core.Direction; // Pufferfish + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.item.Item; + import net.minecraft.world.item.ItemStack; +@@ -10,6 +12,63 @@ import org.bukkit.craftbukkit.entity.CraftHumanEntity; + // CraftBukkit end + + public interface Container extends Clearable { ++ // Pufferfish start - allow the inventory to override and optimize these frequent calls ++ default boolean hasEmptySlot(@org.jetbrains.annotations.Nullable Direction enumdirection) { // there is a slot with 0 items in it ++ if (this instanceof WorldlyContainer worldlyContainer) { ++ for (int i : worldlyContainer.getSlotsForFace(enumdirection)) { ++ if (this.getItem(i).isEmpty()) { ++ return true; ++ } ++ } ++ } else { ++ int size = this.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ if (this.getItem(i).isEmpty()) { ++ return true; ++ } ++ } ++ } ++ return false; ++ } ++ ++ default boolean isCompletelyFull(@org.jetbrains.annotations.Nullable Direction enumdirection) { // every stack is maxed ++ if (this instanceof WorldlyContainer worldlyContainer) { ++ for (int i : worldlyContainer.getSlotsForFace(enumdirection)) { ++ ItemStack itemStack = this.getItem(i); ++ if (itemStack.getCount() < itemStack.getMaxStackSize()) { ++ return false; ++ } ++ } ++ } else { ++ int size = this.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ ItemStack itemStack = this.getItem(i); ++ if (itemStack.getCount() < itemStack.getMaxStackSize()) { ++ return false; ++ } ++ } ++ } ++ return true; ++ } ++ ++ default boolean isCompletelyEmpty(@org.jetbrains.annotations.Nullable Direction enumdirection) { ++ if (this instanceof WorldlyContainer worldlyContainer) { ++ for (int i : worldlyContainer.getSlotsForFace(enumdirection)) { ++ if (!this.getItem(i).isEmpty()) { ++ return false; ++ } ++ } ++ } else { ++ int size = this.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ if (!this.getItem(i).isEmpty()) { ++ return false; ++ } ++ } ++ } ++ return true; ++ } ++ // Pufferfish end + + int LARGE_MAX_STACK_SIZE = 64; + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 1eaab1f6923e6aa34b643293347348e5cc19af3c..3073b34a0e0281b6b0330721bb0440147de28511 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -291,7 +291,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + public double yo; + public double zo; + private Vec3 position; +- private BlockPos blockPosition; ++ public BlockPos blockPosition; // Pufferfish - private->public + private ChunkPos chunkPosition; + private Vec3 deltaMovement; + private float yRot; +@@ -414,6 +414,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return this.originWorld; + } + // Paper end ++ // Pufferfish start ++ public boolean activatedPriorityReset = false; // DAB ++ public int activatedPriority = gg.pufferfish.pufferfish.PufferfishConfig.maximumActivationPrio; // golf score ++ public final BlockPos.MutableBlockPos cachedBlockPos = new BlockPos.MutableBlockPos(); // used where needed ++ // Pufferfish end ++ + public float getBukkitYaw() { + return this.yRot; + } +@@ -488,17 +494,36 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.isLegacyTrackingEntity = isLegacyTrackingEntity; + } + ++ private org.spigotmc.TrackingRange.TrackingRangeType getFurthestEntity(Entity entity, net.minecraft.server.level.ChunkMap chunkMap, org.spigotmc.TrackingRange.TrackingRangeType type, int range) { ++ List passengers = entity.getPassengers(); ++ for (int i = 0, size = passengers.size(); i < size; i++) { ++ Entity passenger = passengers.get(i); ++ org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; ++ int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); ++ if (passengerRange > range) { ++ type = passengerType; ++ range = passengerRange; ++ } ++ ++ type = this.getFurthestEntity(passenger, chunkMap, type, range); ++ } ++ ++ return type; ++ } ++ + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { + // determine highest range of passengers + if (this.passengers.isEmpty()) { + return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] + .getObjectsInRange(MCUtil.getCoordinateKey(this)); + } +- Iterable passengers = this.getIndirectPassengers(); ++ //Iterable passengers = this.getIndirectPassengers(); // Pufferfish + net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; + org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; + int range = chunkMap.getEntityTrackerRange(type.ordinal()); + ++ // Pufferfish start - use getFurthestEntity to skip getIndirectPassengers ++ /* + for (Entity passenger : passengers) { + org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; + int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); +@@ -507,6 +532,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + range = passengerRange; + } + } ++ */ ++ type = this.getFurthestEntity(this, chunkMap, type, range); ++ // Pufferfish end + + return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); + } +@@ -788,6 +816,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // CraftBukkit end + + public void baseTick() { ++ // Pufferfish start - entity TTL ++ if (type != EntityType.PLAYER && type.ttl >= 0 && this.tickCount >= type.ttl) { ++ remove(RemovalReason.DISCARDED); ++ return; ++ } ++ // Pufferfish end - entity TTL + this.level.getProfiler().push("entityBaseTick"); + if (firstTick && this instanceof net.minecraft.world.entity.NeutralMob neutralMob) neutralMob.tickInitialPersistentAnger(level); // Paper - Update last hurt when ticking + this.feetBlockState = null; +@@ -4092,16 +4126,18 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { +- if (this.touchingUnloadedChunk()) { ++ if (false && this.touchingUnloadedChunk()) { // Pufferfish - cost of a lookup here is the same cost as below, so skip + return false; + } else { + AABB axisalignedbb = this.getBoundingBox().deflate(0.001D); +- int i = Mth.floor(axisalignedbb.minX); +- int j = Mth.ceil(axisalignedbb.maxX); +- int k = Mth.floor(axisalignedbb.minY); +- int l = Mth.ceil(axisalignedbb.maxY); +- int i1 = Mth.floor(axisalignedbb.minZ); +- int j1 = Mth.ceil(axisalignedbb.maxZ); ++ // Pufferfish start - rename ++ int minBlockX = Mth.floor(axisalignedbb.minX); ++ int maxBlockX = Mth.ceil(axisalignedbb.maxX); ++ int minBlockY = Mth.floor(axisalignedbb.minY); ++ int maxBlockY = Mth.ceil(axisalignedbb.maxY); ++ int minBlockZ = Mth.floor(axisalignedbb.minZ); ++ int maxBlockZ = Mth.ceil(axisalignedbb.maxZ); ++ // Pufferfish end + double d1 = 0.0D; + boolean flag = this.isPushedByFluid(); + boolean flag1 = false; +@@ -4109,14 +4145,61 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + int k1 = 0; + BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); + +- for (int l1 = i; l1 < j; ++l1) { +- for (int i2 = k; i2 < l; ++i2) { +- for (int j2 = i1; j2 < j1; ++j2) { +- blockposition_mutableblockposition.set(l1, i2, j2); +- FluidState fluid = this.level.getFluidState(blockposition_mutableblockposition); ++ // Pufferfish start - based off CollisionUtil.getCollisionsForBlocksOrWorldBorder ++ final int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this.level); ++ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this.level); ++ final int minBlock = minSection << 4; ++ final int maxBlock = (maxSection << 4) | 15; ++ ++ // special cases: ++ if (minBlockY > maxBlock || maxBlockY < minBlock) { ++ // no point in checking ++ return false; ++ } ++ ++ int minYIterate = Math.max(minBlock, minBlockY); ++ int maxYIterate = Math.min(maxBlock, maxBlockY); ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk ++ int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 16; // coordinate in chunk ++ ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk ++ int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 16; // coordinate in chunk ++ ++ net.minecraft.world.level.chunk.ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(currChunkX, currChunkZ); ++ if (chunk == null) { ++ return false; // if we're touching an unloaded chunk then it's false ++ } ++ ++ net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); ++ ++ for (int currY = minYIterate; currY < maxYIterate; ++currY) { ++ net.minecraft.world.level.chunk.LevelChunkSection section = sections[(currY >> 4) - minSection]; ++ ++ if (section == null || section.hasOnlyAir() || section.fluidStateCount == 0) { // if no fluids, nothing in this section ++ // empty ++ // skip to next section ++ currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one ++ continue; ++ } ++ ++ net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; ++ ++ for (int currZ = minZ; currZ < maxZ; ++currZ) { ++ for (int currX = minX; currX < maxX; ++currX) { ++ FluidState fluid = blocks.get(currX & 15, currY & 15, currZ & 15).getFluidState(); + + if (fluid.is(tag)) { +- double d2 = (double) ((float) i2 + fluid.getHeight(this.level, blockposition_mutableblockposition)); ++ blockposition_mutableblockposition.set((currChunkX << 4) + currX, currY, (currChunkZ << 4) + currZ); ++ double d2 = (double) ((float) currY + fluid.getHeight(this.level, blockposition_mutableblockposition)); + + if (d2 >= axisalignedbb.minY) { + flag1 = true; +@@ -4138,9 +4221,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // CraftBukkit end + } + } ++ } ++ } + } + } + } ++ // Pufferfish end + + if (vec3d.length() > 0.0D) { + if (k1 > 0) { +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index 41ff954010c11d524ffb90abd22c29a1d8d8f5a0..ac7ee31f2bfe5d4139b793a698317db50b39fe40 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -293,6 +293,8 @@ public class EntityType implements FeatureElement, EntityTypeT + private final boolean canSpawnFarFromPlayer; + private final int clientTrackingRange; + private final int updateInterval; ++ public boolean dabEnabled = false; // Pufferfish ++ public int ttl = -1; // Pufferfish + @Nullable + private String descriptionId; + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 42eb78830855d7282b7f3f1bdbe85e632d489784..413652284e229a51e3eefe47f239e8fa9a09ccb2 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -143,7 +143,6 @@ import org.bukkit.event.entity.EntityTeleportEvent; + import org.bukkit.event.player.PlayerItemConsumeEvent; + // CraftBukkit end + +-import co.aikar.timings.MinecraftTimings; // Paper + + public abstract class LivingEntity extends Entity { + +@@ -402,7 +401,7 @@ public abstract class LivingEntity extends Entity { + boolean flag = this instanceof net.minecraft.world.entity.player.Player; + + if (!this.level.isClientSide) { +- if (this.isInWall()) { ++ if ((!gg.pufferfish.pufferfish.PufferfishConfig.enableSuffocationOptimization || (tickCount % 10 == 0 && couldPossiblyBeHurt(1.0F))) && this.isInWall()) { // Pufferfish - optimize suffocation + this.hurt(DamageSource.IN_WALL, 1.0F); + } else if (flag && !this.level.getWorldBorder().isWithinBounds(this.getBoundingBox())) { + double d0 = this.level.getWorldBorder().getDistanceToBorder(this) + this.level.getWorldBorder().getDamageSafeZone(); +@@ -1327,6 +1326,15 @@ public abstract class LivingEntity extends Entity { + return this.getHealth() <= 0.0F; + } + ++ // Pufferfish start - optimize suffocation ++ public boolean couldPossiblyBeHurt(float amount) { ++ if ((float) this.invulnerableTime > (float) this.invulnerableDuration / 2.0F && amount <= this.lastHurt) { ++ return false; ++ } ++ return true; ++ } ++ // Pufferfish end ++ + @Override + public boolean hurt(DamageSource source, float amount) { + if (this.isInvulnerableTo(source)) { +@@ -1934,6 +1942,20 @@ public abstract class LivingEntity extends Entity { + return this.lastClimbablePos; + } + ++ ++ // Pufferfish start ++ private boolean cachedOnClimable = false; ++ private BlockPos lastClimbingPosition = null; ++ ++ public boolean onClimableCached() { ++ if (!this.blockPosition().equals(this.lastClimbingPosition)) { ++ this.cachedOnClimable = this.onClimbable(); ++ this.lastClimbingPosition = this.blockPosition(); ++ } ++ return this.cachedOnClimable; ++ } ++ // Pufferfish end ++ + public boolean onClimbable() { + if (this.isSpectator()) { + return false; +@@ -3647,7 +3669,10 @@ public abstract class LivingEntity extends Entity { + Vec3 vec3d1 = new Vec3(entity.getX(), entity.getEyeY(), entity.getZ()); + + // Paper - diff on change - used in CraftLivingEntity#hasLineOfSight(Location) and CraftWorld#lineOfSightExists +- return vec3d1.distanceToSqr(vec3d) > 128D * 128D ? false : this.level.clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)).getType() == HitResult.Type.MISS; // Paper - use distanceToSqr ++ // Pufferfish start ++ //return vec3d1.distanceToSqr(vec3d) > 128D * 128D ? false : this.level.clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)).getType() == HitResult.Type.MISS; // Paper - use distanceToSqr ++ return vec3d1.distanceToSqr(vec3d) > 128D * 128D ? false : this.level.rayTraceDirect(vec3d, vec3d1, net.minecraft.world.phys.shapes.CollisionContext.of(this)) == net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ // Pufferfish end + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 49b983064ea810382b6112f5dc7f93ba4e5710bd..94b45579dc371ee980565aed2f5dee78ebd44427 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -210,14 +210,16 @@ public abstract class Mob extends LivingEntity { + return this.lookControl; + } + ++ int _pufferfish_inactiveTickDisableCounter = 0; // Pufferfish - throttle inactive goal selector ticking + // Paper start + @Override + public void inactiveTick() { + super.inactiveTick(); +- if (this.goalSelector.inactiveTick()) { ++ boolean isThrottled = gg.pufferfish.pufferfish.PufferfishConfig.throttleInactiveGoalSelectorTick && _pufferfish_inactiveTickDisableCounter++ % 20 != 0; // Pufferfish - throttle inactive goal selector ticking ++ if (this.goalSelector.inactiveTick(this.activatedPriority, true) && !isThrottled) { // Pufferfish - pass activated priroity // Pufferfish - throttle inactive goal selector ticking + this.goalSelector.tick(); + } +- if (this.targetSelector.inactiveTick()) { ++ if (this.targetSelector.inactiveTick(this.activatedPriority, true)) { // Pufferfish - pass activated priority + this.targetSelector.tick(); + } + } +@@ -878,16 +880,20 @@ public abstract class Mob extends LivingEntity { + + if (i % 2 != 0 && this.tickCount > 1) { + this.level.getProfiler().push("targetSelector"); ++ if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.targetSelector.tickRunningGoals(false); + this.level.getProfiler().pop(); + this.level.getProfiler().push("goalSelector"); ++ if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.goalSelector.tickRunningGoals(false); + this.level.getProfiler().pop(); + } else { + this.level.getProfiler().push("targetSelector"); ++ if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.targetSelector.tick(); + this.level.getProfiler().pop(); + this.level.getProfiler().push("goalSelector"); ++ if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.goalSelector.tick(); + this.level.getProfiler().pop(); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +index dd1102d5291ef6f18e82400a6d8a0a376cc071e9..e283eb57c25f7de222f9d09dca851169f5f6e488 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +@@ -23,9 +23,11 @@ public class AttributeMap { + private final Map attributes = Maps.newHashMap(); + private final Set dirtyAttributes = Sets.newHashSet(); + private final AttributeSupplier supplier; ++ private final java.util.function.Function createInstance; // Pufferfish + + public AttributeMap(AttributeSupplier defaultAttributes) { + this.supplier = defaultAttributes; ++ this.createInstance = attribute -> this.supplier.createInstance(this::onAttributeModified, attribute); // Pufferfish + } + + private void onAttributeModified(AttributeInstance instance) { +@@ -45,11 +47,10 @@ public class AttributeMap { + }).collect(Collectors.toList()); + } + ++ + @Nullable + public AttributeInstance getInstance(Attribute attribute) { +- return this.attributes.computeIfAbsent(attribute, (attributex) -> { +- return this.supplier.createInstance(this::onAttributeModified, attributex); +- }); ++ return this.attributes.computeIfAbsent(attribute, this.createInstance); // Pufferfish - cache lambda, as for some reason java allocates it anyways + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +index d4c91e0a0c64fcb7f1145de3f30134cb1f1f8ee6..fe502445a77afe7e3807afae48d7bf03f370e290 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +@@ -47,6 +47,7 @@ public class AcquirePoi { + return false; + } else { + mutableLong.setValue(time + 20L + (long)world.getRandom().nextInt(20)); ++ if (entity.getNavigation().isStuck()) mutableLong.add(200); // Pufferfish - wait an additional 10s to check again if they're stuck + PoiManager poiManager = world.getPoiManager(); + long2ObjectMap.long2ObjectEntrySet().removeIf((entry) -> { + return !entry.getValue().isStillValid(time); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java +index 646d9a121d908a2fc3e4e302484dd5cd1bfc6804..e546ecdccde352502e26a8668eaaafe048d6e282 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java +@@ -37,7 +37,11 @@ public class VillagerPanicTrigger extends Behavior { + + @Override + protected void tick(ServerLevel world, Villager entity, long time) { +- if (time % 100L == 0L) { ++ // Pufferfish start ++ if (entity.nextGolemPanic < 0) entity.nextGolemPanic = time + 100; ++ if (--entity.nextGolemPanic < time) { ++ entity.nextGolemPanic = -1; ++ // Pufferfish end + entity.spawnGolemIfNeeded(world, time, 3); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index b738ee2d3801fadfd09313f05ae24593e56b0ec6..1635818fc4b1788c0d397085239df6dd75b210ab 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -53,9 +53,12 @@ public class GoalSelector { + } + + // Paper start +- public boolean inactiveTick() { ++ public boolean inactiveTick(int tickRate, boolean inactive) { // Pufferfish start ++ if (inactive && !gg.pufferfish.pufferfish.PufferfishConfig.dearEnabled) tickRate = 4; // reset to Paper's ++ tickRate = Math.min(tickRate, this.newGoalRate); + this.curRate++; +- return this.curRate % this.newGoalRate == 0; ++ return this.curRate % tickRate == 0; ++ // Pufferfish end + } + public boolean hasTasks() { + for (WrappedGoal task : this.availableGoals) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +index 26bf383caea68834c654b25653ced9017f1b1b22..615eb55e24d365d994fbfe9d45d2be387fd5d561 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +@@ -119,6 +119,7 @@ public abstract class MoveToBlockGoal extends Goal { + for(int m = 0; m <= l; m = m > 0 ? -m : 1 - m) { + for(int n = m < l && m > -l ? l : 0; n <= l; n = n > 0 ? -n : 1 - n) { + mutableBlockPos.setWithOffset(blockPos, m, k - 1, n); ++ if (!this.mob.level.hasChunkAt(mutableBlockPos)) continue; // Pufferfish - if this block isn't loaded, continue + if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level, mutableBlockPos)) { + this.blockPos = mutableBlockPos; + setTargetPosition(mutableBlockPos.immutable()); // Paper +diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +index a7575b5ef56af6f53448d391abb4956e130148ca..e752c83df50fb9b670ecea2abc95426c2a009b6f 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java ++++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +@@ -75,9 +75,18 @@ public class TargetingConditions { + } + + if (this.range > 0.0D) { +- double d = this.testInvisible ? targetEntity.getVisibilityPercent(baseEntity) : 1.0D; +- double e = Math.max((this.useFollowRange ? this.getFollowRange(baseEntity) : this.range) * d, 2.0D); // Paper ++ // Pufferfish start - check range before getting visibility ++ // d = invisibility percent, e = follow range adjusted for invisibility, f = distance + double f = baseEntity.distanceToSqr(targetEntity.getX(), targetEntity.getY(), targetEntity.getZ()); ++ double followRangeRaw = this.useFollowRange ? this.getFollowRange(baseEntity) : this.range; ++ ++ if (f > followRangeRaw * followRangeRaw) { // the actual follow range will always be this value or smaller, so if the distance is larger then it never will return true after getting invis ++ return false; ++ } ++ ++ double d = this.testInvisible ? targetEntity.getVisibilityPercent(baseEntity) : 1.0D; ++ double e = Math.max((followRangeRaw) * d, 2.0D); // Paper ++ // Pufferfish end + if (f > e * e) { + return false; + } +diff --git a/src/main/java/net/minecraft/world/entity/ambient/Bat.java b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +index 320c558bbe80d4bbc641e895ec43cfa2b45e8d70..1572a81ce1718964d795f2a2a411402f88901c73 100644 +--- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java ++++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +@@ -256,13 +256,22 @@ public class Bat extends AmbientCreature { + } + } + ++ // Pufferfish start - only check for spooky season once an hour ++ private static boolean isSpookySeason = false; ++ private static final int ONE_HOUR = 20 * 60 * 60; ++ private static int lastSpookyCheck = -ONE_HOUR; + private static boolean isHalloween() { ++ if (net.minecraft.server.MinecraftServer.currentTick - lastSpookyCheck > ONE_HOUR) { + LocalDate localdate = LocalDate.now(); + int i = localdate.get(ChronoField.DAY_OF_MONTH); + int j = localdate.get(ChronoField.MONTH_OF_YEAR); + +- return j == 10 && i >= 20 || j == 11 && i <= 3; ++ isSpookySeason = j == 10 && i >= 20 || j == 11 && i <= 3; ++ lastSpookyCheck = net.minecraft.server.MinecraftServer.currentTick; ++ } ++ return isSpookySeason; + } ++ // Pufferfish end + + @Override + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +index c0084b1f146a4697194c421519537e612ff737c0..c66a214dfbde7fd8e7a68efaa82ac260178f297f 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java ++++ b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +@@ -228,9 +228,11 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + return 0.4F; + } + ++ private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { + this.level.getProfiler().push("allayBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); + this.level.getProfiler().pop(); + this.level.getProfiler().push("allayActivityUpdate"); +diff --git a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +index 0d7f951e3837de7553d93f3d4525276048feb405..02219f5ca614fefffa1ceb3c7036dfe1c90c8676 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java ++++ b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +@@ -285,9 +285,11 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder { + return this.getDeltaMovement().horizontalDistanceSqr() > 1.0E-6D && this.isInWaterOrBubble(); + } + ++ private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { + this.level.getProfiler().push("frogBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel)this.level, this); + this.level.getProfiler().pop(); + this.level.getProfiler().push("frogActivityUpdate"); +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +index 9058f9f2e561cda9f475f33218bf7a78297de4bc..e591b0a09f5a8475b3ec9cd28bd5d5b69809ed73 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -77,9 +77,11 @@ public class Tadpole extends AbstractFish { + return SoundEvents.TADPOLE_FLOP; + } + ++ private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { + this.level.getProfiler().push("tadpoleBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); + this.level.getProfiler().pop(); + this.level.getProfiler().push("tadpoleActivityUpdate"); +diff --git a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +index e9f7c08ae3ea9c578971b1ede88788572c20e277..0f365b9dbb160d90ddf5fcd40895305df48ce916 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +@@ -188,9 +188,11 @@ public class Goat extends Animal { + return (Brain) super.getBrain(); // CraftBukkit - decompile error + } + ++ private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { + this.level.getProfiler().push("goatBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); + this.level.getProfiler().pop(); + this.level.getProfiler().push("goatActivityUpdate"); +diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +index f0ccdfbd7d7be8c6e302609accf8fe9cac8885c4..c58496c84b2b3f86890050813041fa49711f3a01 100644 +--- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +@@ -255,10 +255,16 @@ public class ItemEntity extends Entity { + if (entityitem.isMergable()) { + // Paper Start - Fix items merging through walls + if (this.level.paperConfig().fixes.fixItemsMergingThroughWalls) { ++ // Pufferfish start - skip the allocations ++ /* + net.minecraft.world.level.ClipContext rayTrace = new net.minecraft.world.level.ClipContext(this.position(), entityitem.position(), + net.minecraft.world.level.ClipContext.Block.COLLIDER, net.minecraft.world.level.ClipContext.Fluid.NONE, this); + net.minecraft.world.phys.BlockHitResult rayTraceResult = level.clip(rayTrace); + if (rayTraceResult.getType() == net.minecraft.world.phys.HitResult.Type.BLOCK) continue; ++ */ ++ if (level.rayTraceDirect(this.position(), entityitem.position(), net.minecraft.world.phys.shapes.CollisionContext.of(this)) == ++ net.minecraft.world.phys.HitResult.Type.BLOCK) continue; ++ // Pufferfish end + } + // Paper End + this.tryToMerge(entityitem); +diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +index f4002ac7cba7d5e41b4f11b98212c625f6a92a65..ff0e09a7387e7dc9ca136d3e48e640b9e9cb4bf3 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java ++++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +@@ -322,11 +322,17 @@ public class EnderMan extends Monster implements NeutralMob { + private boolean teleport(double x, double y, double z) { + BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(x, y, z); + +- while (blockposition_mutableblockposition.getY() > this.level.getMinBuildHeight() && !this.level.getBlockState(blockposition_mutableblockposition).getMaterial().blocksMotion()) { ++ // Pufferfish start - single chunk lookup ++ net.minecraft.world.level.chunk.LevelChunk chunk = this.level.getChunkIfLoaded(blockposition_mutableblockposition); ++ if (chunk == null) { ++ return false; ++ } ++ // Pufferfish end ++ while (blockposition_mutableblockposition.getY() > this.level.getMinBuildHeight() && !chunk.getBlockState(blockposition_mutableblockposition).getMaterial().blocksMotion()) { // Pufferfish + blockposition_mutableblockposition.move(Direction.DOWN); + } + +- BlockState iblockdata = this.level.getBlockState(blockposition_mutableblockposition); ++ BlockState iblockdata = chunk.getBlockState(blockposition_mutableblockposition); // Pufferfish + boolean flag = iblockdata.getMaterial().blocksMotion(); + boolean flag1 = iblockdata.getFluidState().is(FluidTags.WATER); + +diff --git a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +index 5d3b3cb3a882eb5d716f678095a65b28d0967476..daa2224b021c966751eb39f269ffbfe6e7f3d426 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +@@ -126,9 +126,11 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + return (Brain) super.getBrain(); // Paper - decompile fix + } + ++ private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { + this.level.getProfiler().push("hoglinBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel)this.level, this); + this.level.getProfiler().pop(); + HoglinAi.updateActivity(this); +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +index afa7ecfa8453da510ec5ccecb1ceeb1d9893d259..b401fb4f276ca81b4bb18426ee56abed8a9f7a7b 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +@@ -308,9 +308,11 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + return !this.cannotHunt; + } + ++ private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { + this.level.getProfiler().push("piglinBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); + this.level.getProfiler().pop(); + PiglinAi.updateActivity(this); +diff --git a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +index 1ae7408048f951cb94d7cfbea60efc5567b1af84..904826ea563bd2eb469f403df459def62cc1b5e6 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java ++++ b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +@@ -270,11 +270,13 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + + } + ++ private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { + ServerLevel worldserver = (ServerLevel) this.level; + + worldserver.getProfiler().push("wardenBrain"); ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick(worldserver, this); + this.level.getProfiler().pop(); + super.customServerAiStep(); +diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java +index 18eac340386a396c9850f53f30d20a41c1437788..76a9da8209d557b913c49ccd281bf147b9ac4fa4 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -140,6 +140,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + return holder.is(PoiTypes.MEETING); + }); + ++ public long nextGolemPanic = -1; // Pufferfish ++ + public Villager(EntityType entityType, Level world) { + this(entityType, world, VillagerType.PLAINS); + } +@@ -243,11 +245,17 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + // Spigot End + ++ private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { mobTick(false); } + protected void mobTick(boolean inactive) { + this.level.getProfiler().push("villagerBrain"); +- if (!inactive) this.getBrain().tick((ServerLevel) this.level, this); // Paper ++ // Pufferfish start ++ if (!inactive) { ++ if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ this.getBrain().tick((ServerLevel) this.level, this); // Paper ++ } ++ // Pufferfish end + this.level.getProfiler().pop(); + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; +diff --git a/src/main/java/net/minecraft/world/entity/player/Inventory.java b/src/main/java/net/minecraft/world/entity/player/Inventory.java +index 5bc033bf59d49eda1f8f2574165bbcbeab7faa0f..004091f2026f3c58d9bce49f1b07f6441df8da8a 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Inventory.java ++++ b/src/main/java/net/minecraft/world/entity/player/Inventory.java +@@ -681,6 +681,8 @@ public class Inventory implements Container, Nameable { + } + + public boolean contains(ItemStack stack) { ++ // Pufferfish start - don't allocate iterators ++ /* + Iterator iterator = this.compartments.iterator(); + + while (iterator.hasNext()) { +@@ -695,6 +697,18 @@ public class Inventory implements Container, Nameable { + } + } + } ++ */ ++ for (int i = 0; i < this.compartments.size(); i++) { ++ List list = this.compartments.get(i); ++ for (int j = 0; j < list.size(); j++) { ++ ItemStack itemstack1 = list.get(j); ++ ++ if (!itemstack1.isEmpty() && itemstack1.sameItem(stack)) { ++ return true; ++ } ++ } ++ } ++ // Pufferfish end + + return false; + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index 893975e8587b9036f622e2088c302e33004496d2..a000834c4ea8645a2fcd697e6396f797c42c8fa3 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -43,6 +43,36 @@ public abstract class Projectile extends Entity { + super(type, world); + } + ++ // Pufferfish start ++ private static int loadedThisTick = 0; ++ private static int loadedTick; ++ ++ private int loadedLifetime = 0; ++ @Override ++ public void setPos(double x, double y, double z) { ++ int currentTick = net.minecraft.server.MinecraftServer.currentTick; ++ if (loadedTick != currentTick) { ++ loadedTick = currentTick; ++ loadedThisTick = 0; ++ } ++ int previousX = Mth.floor(this.getX()) >> 4, previousZ = Mth.floor(this.getZ()) >> 4; ++ int newX = Mth.floor(x) >> 4, newZ = Mth.floor(z) >> 4; ++ if (previousX != newX || previousZ != newZ) { ++ boolean isLoaded = ((net.minecraft.server.level.ServerChunkCache) this.level.getChunkSource()).getChunkAtIfLoadedMainThread(newX, newZ) != null; ++ if (!isLoaded) { ++ if (Projectile.loadedThisTick > gg.pufferfish.pufferfish.PufferfishConfig.maxProjectileLoadsPerTick) { ++ if (++this.loadedLifetime > gg.pufferfish.pufferfish.PufferfishConfig.maxProjectileLoadsPerProjectile) { ++ this.discard(); ++ } ++ return; ++ } ++ Projectile.loadedThisTick++; ++ } ++ } ++ super.setPos(x, y, z); ++ } ++ // Pufferfish start ++ + public void setOwner(@Nullable Entity entity) { + if (entity != null) { + this.ownerUUID = entity.getUUID(); +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +index cc74eeb45913fab03e85969957215d2811252a83..086feb19f11a6c646b5a1a06aef4df05a4beae8b 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +@@ -27,7 +27,10 @@ import org.bukkit.inventory.InventoryHolder; + + public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity { + ++ // Pufferfish start + private NonNullList itemStacks; ++ private gg.airplane.structs.ItemListWithBitset itemStacksOptimized; ++ // Pufferfish end + @Nullable + public ResourceLocation lootTable; + public long lootTableSeed; +@@ -89,12 +92,18 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme + + protected AbstractMinecartContainer(EntityType type, Level world) { + super(type, world); +- this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513 ++ // Pufferfish start ++ this.itemStacksOptimized = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); // CraftBukkit - SPIGOT-3513 ++ this.itemStacks = this.itemStacksOptimized.nonNullList; ++ // Pufferfish end + } + + protected AbstractMinecartContainer(EntityType type, double x, double y, double z, Level world) { + super(type, world, x, y, z); +- this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513 ++ // Pufferfish start ++ this.itemStacksOptimized = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); // CraftBukkit - SPIGOT-3513 ++ this.itemStacks = this.itemStacksOptimized.nonNullList; ++ // Pufferfish end + } + + @Override +@@ -156,6 +165,10 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme + protected void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.lootableData.loadNbt(nbt); // Paper ++ // Pufferfish start ++ this.itemStacksOptimized = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); ++ this.itemStacks = this.itemStacksOptimized.nonNullList; ++ // Pufferfish end + this.readChestVehicleSaveData(nbt); + } + +diff --git a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java +index e7c06d98532160499f2610f69de27e30a326b16f..830622bd92431df5bc4a57fe6785689f8585e14b 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java ++++ b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java +@@ -26,8 +26,13 @@ public class ShapelessRecipe implements CraftingRecipe { + final CraftingBookCategory category; + final ItemStack result; + final NonNullList ingredients; ++ private final boolean isBukkit; // Pufferfish + ++ // Pufferfish start + public ShapelessRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack output, NonNullList input) { ++ this(id, group, category, output, input, false); ++ } ++ public ShapelessRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack output, NonNullList input, boolean isBukkit) { this.isBukkit = isBukkit; // Pufferfish end + this.id = id; + this.group = group; + this.category = category; +@@ -81,6 +86,28 @@ public class ShapelessRecipe implements CraftingRecipe { + } + + public boolean matches(CraftingContainer inventory, Level world) { ++ // Pufferfish start ++ if (!this.isBukkit) { ++ java.util.List ingredients = com.google.common.collect.Lists.newArrayList(this.ingredients.toArray(new Ingredient[0])); ++ ++ inventory: for (int index = 0; index < inventory.getContainerSize(); index++) { ++ ItemStack itemStack = inventory.getItem(index); ++ ++ if (!itemStack.isEmpty()) { ++ for (int i = 0; i < ingredients.size(); i++) { ++ if (ingredients.get(i).test(itemStack)) { ++ ingredients.remove(i); ++ continue inventory; ++ } ++ } ++ return false; ++ } ++ } ++ ++ return ingredients.isEmpty(); ++ } ++ // Pufferfish end ++ + StackedContents autorecipestackmanager = new StackedContents(); + int i = 0; + +diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java +index d1eefa6ef3e9abfe7af4d8310aa64465fa2d5463..0f4aa330e5b179bb706a31917c671f165e22b9cd 100644 +--- a/src/main/java/net/minecraft/world/level/BlockGetter.java ++++ b/src/main/java/net/minecraft/world/level/BlockGetter.java +@@ -73,6 +73,16 @@ public interface BlockGetter extends LevelHeightAccessor { + }); + } + ++ // Pufferfish start - broken down variant of below rayTraceBlock, used by World#rayTraceDirect ++ default net.minecraft.world.phys.BlockHitResult.Type rayTraceBlockDirect(Vec3 vec3d, Vec3 vec3d1, BlockPos blockposition, BlockState iblockdata, net.minecraft.world.phys.shapes.CollisionContext voxelshapecoll) { ++ if (iblockdata.isAir()) return null; // Tuinity - optimise air cases ++ VoxelShape voxelshape = ClipContext.Block.COLLIDER.get(iblockdata, this, blockposition, voxelshapecoll); ++ net.minecraft.world.phys.BlockHitResult movingobjectpositionblock = this.clipWithInteractionOverride(vec3d, vec3d1, blockposition, voxelshape, iblockdata); ++ ++ return movingobjectpositionblock == null ? null : movingobjectpositionblock.getType(); ++ } ++ // Pufferfish end ++ + // CraftBukkit start - moved block handling into separate method for use by Block#rayTrace + default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition) { + // Paper start - Prevent raytrace from loading chunks +diff --git a/src/main/java/net/minecraft/world/level/GameRules.java b/src/main/java/net/minecraft/world/level/GameRules.java +index 663c1d8c1611af915a1bae733920dd75ad73feb1..c15e4d95baacd30f9614dc5526dc8fc12ae5bd06 100644 +--- a/src/main/java/net/minecraft/world/level/GameRules.java ++++ b/src/main/java/net/minecraft/world/level/GameRules.java +@@ -98,6 +98,7 @@ public class GameRules { + public static final GameRules.Key RULE_LAVA_SOURCE_CONVERSION = GameRules.register("lavaSourceConversion", GameRules.Category.UPDATES, GameRules.BooleanValue.create(false)); + public static final GameRules.Key RULE_GLOBAL_SOUND_EVENTS = GameRules.register("globalSoundEvents", GameRules.Category.MISC, GameRules.BooleanValue.create(true)); + private final Map, GameRules.Value> rules; ++ private final GameRules.Value[] gameruleArray; + + private static > GameRules.Key register(String name, GameRules.Category category, GameRules.Type type) { + GameRules.Key gamerules_gamerulekey = new GameRules.Key<>(name, category); +@@ -116,17 +117,33 @@ public class GameRules { + } + + public GameRules() { +- this.rules = (Map) GameRules.GAME_RULE_TYPES.entrySet().stream().collect(ImmutableMap.toImmutableMap(Entry::getKey, (entry) -> { ++ // Pufferfish start - use this to ensure gameruleArray is initialized ++ this((Map) GameRules.GAME_RULE_TYPES.entrySet().stream().collect(ImmutableMap.toImmutableMap(Entry::getKey, (entry) -> { + return ((GameRules.Type) entry.getValue()).createRule(); +- })); ++ }))); ++ // Pufferfish end + } + + private GameRules(Map, GameRules.Value> rules) { + this.rules = rules; ++ ++ // Pufferfish start ++ int arraySize = rules.keySet().stream().mapToInt(key -> key.gameRuleIndex).max().orElse(-1) + 1; ++ GameRules.Value[] values = new GameRules.Value[arraySize]; ++ ++ for (Entry, GameRules.Value> entry : rules.entrySet()) { ++ values[entry.getKey().gameRuleIndex] = entry.getValue(); ++ } ++ ++ this.gameruleArray = values; ++ // Pufferfish end + } + + public > T getRule(GameRules.Key key) { +- return (T) this.rules.get(key); // CraftBukkit - decompile error ++ // Pufferfish start ++ return key == null ? null : (T) this.gameruleArray[key.gameRuleIndex]; ++ //return (T) this.rules.get(key); // CraftBukkit - decompile error ++ // Pufferfish end + } + + public CompoundTag createTag() { +@@ -185,6 +202,10 @@ public class GameRules { + } + + public static final class Key> { ++ // Pufferfish start ++ private static int lastGameRuleIndex = 0; ++ public final int gameRuleIndex = lastGameRuleIndex++; ++ // Pufferfish end + + final String id; + private final GameRules.Category category; +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index db971ca4136c7f922d630f38aa5c78cb04adbdfa..eb0a31c885ea64da00abcd5e67083392138b1ca0 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -270,6 +270,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey getTypeKey(); + ++ protected final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(java.util.concurrent.ThreadLocalRandom.current().nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - move thread unsafe random initialization // Pufferfish - getter ++ ++ // Pufferfish start - ensure these get inlined ++ private final int minBuildHeight, minSection, height, maxBuildHeight, maxSection; ++ @Override public final int getMaxBuildHeight() { return this.maxBuildHeight; } ++ @Override public final int getMinSection() { return this.minSection; } ++ @Override public final int getMaxSection() { return this.maxSection; } ++ @Override public final int getMinBuildHeight() { return this.minBuildHeight; } ++ @Override public final int getHeight() { return this.height; } ++ // Pufferfish end ++ + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper +@@ -292,6 +303,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + }); + final DimensionType dimensionmanager = (DimensionType) holder.value(); + ++ // Pufferfish start ++ this.minBuildHeight = dimensionmanager.minY(); ++ this.minSection = SectionPos.blockToSectionCoord(this.minBuildHeight); ++ this.height = dimensionmanager.height(); ++ this.maxBuildHeight = this.minBuildHeight + this.height; ++ this.maxSection = SectionPos.blockToSectionCoord(this.maxBuildHeight - 1) + 1; ++ // Pufferfish end + this.dimension = resourcekey; + this.isClientSide = flag; + if (dimensionmanager.coordinateScale() != 1.0D) { +@@ -407,6 +425,91 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return null; + } + ++ // Pufferfish start - broken down method of raytracing for EntityLiving#hasLineOfSight, replaces IBlockAccess#rayTrace(RayTrace) ++ public net.minecraft.world.phys.BlockHitResult.Type rayTraceDirect(net.minecraft.world.phys.Vec3 vec3d, net.minecraft.world.phys.Vec3 vec3d1, net.minecraft.world.phys.shapes.CollisionContext voxelshapecoll) { ++ // most of this code comes from IBlockAccess#a(RayTrace, BiFunction, Function), but removes the needless functions ++ if (vec3d.equals(vec3d1)) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ ++ double endX = Mth.lerp(-1.0E-7D, vec3d1.x, vec3d.x); ++ double endY = Mth.lerp(-1.0E-7D, vec3d1.y, vec3d.y); ++ double endZ = Mth.lerp(-1.0E-7D, vec3d1.z, vec3d.z); ++ ++ double startX = Mth.lerp(-1.0E-7D, vec3d.x, vec3d1.x); ++ double startY = Mth.lerp(-1.0E-7D, vec3d.y, vec3d1.y); ++ double startZ = Mth.lerp(-1.0E-7D, vec3d.z, vec3d1.z); ++ ++ int currentX = Mth.floor(startX); ++ int currentY = Mth.floor(startY); ++ int currentZ = Mth.floor(startZ); ++ ++ BlockPos.MutableBlockPos currentBlock = new BlockPos.MutableBlockPos(currentX, currentY, currentZ); ++ ++ LevelChunk chunk = this.getChunkIfLoaded(currentBlock); ++ if (chunk == null) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ ++ net.minecraft.world.phys.BlockHitResult.Type initialCheck = this.rayTraceBlockDirect(vec3d, vec3d1, currentBlock, chunk.getBlockState(currentBlock), voxelshapecoll); ++ ++ if (initialCheck != null) { ++ return initialCheck; ++ } ++ ++ double diffX = endX - startX; ++ double diffY = endY - startY; ++ double diffZ = endZ - startZ; ++ ++ int xDirection = Mth.sign(diffX); ++ int yDirection = Mth.sign(diffY); ++ int zDirection = Mth.sign(diffZ); ++ ++ double normalizedX = xDirection == 0 ? Double.MAX_VALUE : (double) xDirection / diffX; ++ double normalizedY = yDirection == 0 ? Double.MAX_VALUE : (double) yDirection / diffY; ++ double normalizedZ = zDirection == 0 ? Double.MAX_VALUE : (double) zDirection / diffZ; ++ ++ double normalizedXDirection = normalizedX * (xDirection > 0 ? 1.0D - Mth.frac(startX) : Mth.frac(startX)); ++ double normalizedYDirection = normalizedY * (yDirection > 0 ? 1.0D - Mth.frac(startY) : Mth.frac(startY)); ++ double normalizedZDirection = normalizedZ * (zDirection > 0 ? 1.0D - Mth.frac(startZ) : Mth.frac(startZ)); ++ ++ net.minecraft.world.phys.BlockHitResult.Type result; ++ ++ do { ++ if (normalizedXDirection > 1.0D && normalizedYDirection > 1.0D && normalizedZDirection > 1.0D) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ ++ if (normalizedXDirection < normalizedYDirection) { ++ if (normalizedXDirection < normalizedZDirection) { ++ currentX += xDirection; ++ normalizedXDirection += normalizedX; ++ } else { ++ currentZ += zDirection; ++ normalizedZDirection += normalizedZ; ++ } ++ } else if (normalizedYDirection < normalizedZDirection) { ++ currentY += yDirection; ++ normalizedYDirection += normalizedY; ++ } else { ++ currentZ += zDirection; ++ normalizedZDirection += normalizedZ; ++ } ++ ++ currentBlock.set(currentX, currentY, currentZ); ++ if (chunk.getPos().x != currentBlock.getX() >> 4 || chunk.getPos().z != currentBlock.getZ() >> 4) { ++ chunk = this.getChunkIfLoaded(currentBlock); ++ if (chunk == null) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ } ++ result = this.rayTraceBlockDirect(vec3d, vec3d1, currentBlock, chunk.getBlockState(currentBlock), voxelshapecoll); ++ } while (result == null); ++ ++ return result; ++ } ++ // Pufferfish end ++ + public boolean isInWorldBounds(BlockPos pos) { + return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - use better/optimized check + } +@@ -919,13 +1022,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + try { + tickConsumer.accept(entity); + MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick +- } catch (Throwable throwable) { ++ } catch (Throwable throwable) { // Pufferfish - diff on change ServerLevel.tick + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent tile entity and entity crashes + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level.getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, throwable); + getCraftServer().getPluginManager().callEvent(new ServerExceptionEvent(new ServerInternalException(msg, throwable))); +- entity.discard(); ++ entity.discard(); // Pufferfish - diff on change ServerLevel.tick + // Paper end + } + } +@@ -1452,6 +1555,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public ProfilerFiller getProfiler() { ++ if (gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish + return (ProfilerFiller) this.profiler.get(); + } + +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 01b21f520ef1c834b9bafc3de85c1fa4fcf539d6..5521418fa307b3eeb4f02a10c39f05b360d1d06e 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -417,12 +417,12 @@ public final class NaturalSpawner { + } + } + +- private static BlockPos getRandomPosWithin(Level world, LevelChunk chunk) { ++ private static BlockPos getRandomPosWithin(ServerLevel world, LevelChunk chunk) { // Pufferfish - accept serverlevel + ChunkPos chunkcoordintpair = chunk.getPos(); +- int i = chunkcoordintpair.getMinBlockX() + world.random.nextInt(16); +- int j = chunkcoordintpair.getMinBlockZ() + world.random.nextInt(16); ++ int i = chunkcoordintpair.getMinBlockX() + world.getThreadUnsafeRandom().nextInt(16); // Pufferfish - use thread unsafe random ++ int j = chunkcoordintpair.getMinBlockZ() + world.getThreadUnsafeRandom().nextInt(16); // Pufferfish + int k = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, i, j) + 1; +- int l = Mth.randomBetweenInclusive(world.random, world.getMinBuildHeight(), k); ++ int l = Mth.randomBetweenInclusive(world.getThreadUnsafeRandom(), world.getMinBuildHeight(), k); // Pufferfish + + return new BlockPos(i, l, j); + } +diff --git a/src/main/java/net/minecraft/world/level/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java +index c4f1173aab1e53412a65793e06238e637910475a..a4c484cacbf6cec7b9225f3f66a05827bf8ef7a3 100644 +--- a/src/main/java/net/minecraft/world/level/biome/Biome.java ++++ b/src/main/java/net/minecraft/world/level/biome/Biome.java +@@ -66,14 +66,20 @@ public final class Biome { + private final BiomeGenerationSettings generationSettings; + private final MobSpawnSettings mobSettings; + private final BiomeSpecialEffects specialEffects; +- private final ThreadLocal temperatureCache = ThreadLocal.withInitial(() -> { ++ // Pufferfish start - use our cache ++ private final ThreadLocal temperatureCache = ThreadLocal.withInitial(() -> { + return Util.make(() -> { ++ /* + Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = new Long2FloatLinkedOpenHashMap(1024, 0.25F) { + protected void rehash(int i) { + } + }; + long2FloatLinkedOpenHashMap.defaultReturnValue(Float.NaN); + return long2FloatLinkedOpenHashMap; ++ ++ */ ++ return new gg.airplane.structs.Long2FloatAgingCache(TEMPERATURE_CACHE_SIZE); ++ // Pufferfish end + }); + }); + +@@ -114,17 +120,15 @@ public final class Biome { + @Deprecated + public float getTemperature(BlockPos blockPos) { + long l = blockPos.asLong(); +- Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = this.temperatureCache.get(); +- float f = long2FloatLinkedOpenHashMap.get(l); ++ // Pufferfish start ++ gg.airplane.structs.Long2FloatAgingCache cache = this.temperatureCache.get(); ++ float f = cache.getValue(l); + if (!Float.isNaN(f)) { + return f; + } else { + float g = this.getHeightAdjustedTemperature(blockPos); +- if (long2FloatLinkedOpenHashMap.size() == 1024) { +- long2FloatLinkedOpenHashMap.removeFirstFloat(); +- } +- +- long2FloatLinkedOpenHashMap.put(l, g); ++ cache.putValue(l, g); ++ // Pufferfish end + return g; + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java +index a71414397bd45ee7bcacfeef0041d80dfa25f114..d66806565770cb03a21794f99e5c4b0f3040b26a 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java +@@ -31,7 +31,10 @@ import org.bukkit.entity.HumanEntity; + public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity { + + private static final int EVENT_SET_OPEN_COUNT = 1; ++ // Pufferfish start + private NonNullList items; ++ private gg.airplane.structs.ItemListWithBitset optimizedItems; ++ // Pufferfish end + public final ContainerOpenersCounter openersCounter; + private final ChestLidController chestLidController; + +@@ -65,9 +68,13 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + } + // CraftBukkit end + ++ private final boolean isNative = getClass().equals(ChestBlockEntity.class); // Pufferfish + protected ChestBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); +- this.items = NonNullList.withSize(27, ItemStack.EMPTY); ++ // Pufferfish start ++ this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(27); ++ this.items = this.optimizedItems.nonNullList; ++ // Pufferfish end + this.openersCounter = new ContainerOpenersCounter() { + @Override + protected void onOpen(Level world, BlockPos pos, BlockState state) { +@@ -98,6 +105,23 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + this.chestLidController = new ChestLidController(); + } + ++ // Pufferfish start ++ @Override ++ public boolean hasEmptySlot(Direction enumdirection) { ++ return isNative ? !this.optimizedItems.hasFullStacks() : super.hasEmptySlot(enumdirection); ++ } ++ ++ @Override ++ public boolean isCompletelyFull(Direction enumdirection) { ++ return isNative ? this.optimizedItems.hasFullStacks() && super.isCompletelyFull(enumdirection) : super.isCompletelyFull(enumdirection); ++ } ++ ++ @Override ++ public boolean isCompletelyEmpty(Direction enumdirection) { ++ return isNative && this.optimizedItems.isCompletelyEmpty() || super.isCompletelyEmpty(enumdirection); ++ } ++ // Pufferfish end ++ + public ChestBlockEntity(BlockPos pos, BlockState state) { + this(BlockEntityType.CHEST, pos, state); + } +@@ -115,7 +139,10 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + @Override + public void load(CompoundTag nbt) { + super.load(nbt); +- this.items = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); ++ // Pufferfish start ++ this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); ++ this.items = this.optimizedItems.nonNullList; ++ // Pufferfish end + if (!this.tryLoadLootTable(nbt)) { + ContainerHelper.loadAllItems(nbt, this.items); + } +@@ -187,7 +214,10 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + + @Override + protected void setItems(NonNullList list) { +- this.items = list; ++ // Pufferfish start ++ this.optimizedItems = gg.airplane.structs.ItemListWithBitset.fromList(list); ++ this.items = this.optimizedItems.nonNullList; ++ // Pufferfish end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index d76603c4172aa10889949c6c2acff05fee02a13d..034a0665f56fca37a48972671cebc6ec249db120 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -45,7 +45,10 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + public static final int MOVE_ITEM_SPEED = 8; + public static final int HOPPER_CONTAINER_SIZE = 5; ++ // Pufferfish start + private NonNullList items; ++ private gg.airplane.structs.ItemListWithBitset optimizedItems; // Pufferfish ++ // Pufferfish end + private int cooldownTime; + private long tickedGameTime; + +@@ -81,14 +84,37 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + public HopperBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.HOPPER, pos, state); +- this.items = NonNullList.withSize(5, ItemStack.EMPTY); ++ // Pufferfish start ++ this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(5); ++ this.items = this.optimizedItems.nonNullList; ++ // Pufferfish end + this.cooldownTime = -1; + } + ++ // Pufferfish start ++ @Override ++ public boolean hasEmptySlot(Direction enumdirection) { ++ return !this.optimizedItems.hasFullStacks(); ++ } ++ ++ @Override ++ public boolean isCompletelyFull(Direction enumdirection) { ++ return this.optimizedItems.hasFullStacks() && super.isCompletelyFull(enumdirection); ++ } ++ ++ @Override ++ public boolean isCompletelyEmpty(Direction enumdirection) { ++ return this.optimizedItems.isCompletelyEmpty() || super.isCompletelyEmpty(enumdirection); ++ } ++ // Pufferfish end ++ + @Override + public void load(CompoundTag nbt) { + super.load(nbt); +- this.items = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); ++ // Pufferfish start ++ this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); ++ this.items = this.optimizedItems.nonNullList; ++ // Pufferfish end + if (!this.tryLoadLootTable(nbt)) { + ContainerHelper.loadAllItems(nbt, this.items); + } +@@ -160,7 +186,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + flag = HopperBlockEntity.ejectItems(world, pos, state, (Container) blockEntity, blockEntity); // CraftBukkit + } + +- if (!blockEntity.inventoryFull()) { ++ if (!blockEntity.optimizedItems.hasFullStacks() || !blockEntity.inventoryFull()) { // Pufferfish - use bitset first + flag |= booleansupplier.getAsBoolean(); + } + +@@ -199,7 +225,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + skipPushModeEventFire = skipHopperEvents; + boolean foundItem = false; + for (int i = 0; i < hopper.getContainerSize(); ++i) { +- final ItemStack item = hopper.getItem(i); ++ final ItemStack item = hopper.getItem(i); // Pufferfish + if (!item.isEmpty()) { + foundItem = true; + ItemStack origItemStack = item; +@@ -408,12 +434,18 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + private static boolean isFullContainer(Container inventory, Direction direction) { +- return allMatch(inventory, direction, STACK_SIZE_TEST); // Paper - no streams ++ // Pufferfish start - use bitsets ++ //return allMatch(inventory, direction, STACK_SIZE_TEST); // Paper - no streams ++ return inventory.isCompletelyFull(direction); ++ // Pufferfish end + } + + private static boolean isEmptyContainer(Container inv, Direction facing) { + // Paper start +- return allMatch(inv, facing, IS_EMPTY_TEST); ++ // Pufferfish start - use bitsets ++ //return allMatch(inv, facing, IS_EMPTY_TEST); ++ return inv.isCompletelyEmpty(facing); ++ // Pufferfish end + } + private static boolean allMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { + if (iinventory instanceof WorldlyContainer) { +@@ -592,7 +624,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + if (HopperBlockEntity.canPlaceItemInContainer(to, stack, slot, side)) { + boolean flag = false; +- boolean flag1 = to.isEmpty(); ++ boolean flag1 = to.isCompletelyEmpty(side); // Pufferfish + + if (itemstack1.isEmpty()) { + // Spigot start - SPIGOT-6693, InventorySubcontainer#setItem +@@ -745,7 +777,10 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Override + protected void setItems(NonNullList list) { +- this.items = list; ++ // Pufferfish start ++ this.optimizedItems = gg.airplane.structs.ItemListWithBitset.fromList(list); ++ this.items = this.optimizedItems.nonNullList; ++ // Pufferfish end + } + + public static void entityInside(Level world, BlockPos pos, BlockState state, Entity entity, HopperBlockEntity blockEntity) { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java +index d559f93a9a09bac414dd5d58afccad42c127f09b..13e749a3c40f0b2cc002f13675a9a56eedbefdac 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java +@@ -96,13 +96,8 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc + public boolean isEmpty() { + this.unpackLootTable((Player)null); + // Paper start +- for (ItemStack itemStack : this.getItems()) { +- if (!itemStack.isEmpty()) { +- return false; +- } +- } ++ return this.isCompletelyEmpty(null); // Pufferfish - use super + // Paper end +- return true; + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 28e4b302284f955a73e75d0f4276d55fb51826f5..de7a5f3812a017131fd1b32fbeff10e325b1cd2e 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -88,6 +88,18 @@ public class LevelChunk extends ChunkAccess { + private final LevelChunkTicks blockTicks; + private final LevelChunkTicks fluidTicks; + ++ // Pufferfish start - instead of using a random every time the chunk is ticked, define when lightning strikes preemptively ++ private int lightningTick; ++ // shouldDoLightning compiles down to 29 bytes, which with the default of 35 byte inlining should guarantee an inline ++ public final boolean shouldDoLightning(net.minecraft.util.RandomSource random) { ++ if (this.lightningTick-- <= 0) { ++ this.lightningTick = random.nextInt(this.level.spigotConfig.thunderChance) << 1; ++ return true; ++ } ++ return false; ++ } ++ // Pufferfish end ++ + public LevelChunk(Level world, ChunkPos pos) { + this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null); + } +@@ -118,6 +130,7 @@ public class LevelChunk extends ChunkAccess { + this.fluidTicks = fluidTickScheduler; + // CraftBukkit start + this.bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); ++ this.lightningTick = this.level.getThreadUnsafeRandom().nextInt(100000) << 1; // Pufferfish - initialize lightning tick + } + + public org.bukkit.Chunk bukkitChunk; +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index b0c9fce9d4e06cac139e341d218d0b6aac1f1943..f25467ad1c5bac7eaef4b63b2845ad04d7c76e4e 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -27,6 +27,7 @@ public class LevelChunkSection { + public final PalettedContainer states; + // CraftBukkit start - read/write + private PalettedContainer> biomes; ++ public short fluidStateCount; // Pufferfish + public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper + + public LevelChunkSection(int i, PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { +@@ -198,6 +199,7 @@ public class LevelChunkSection { + + if (!fluid.isEmpty()) { + --this.tickingFluidCount; ++ --this.fluidStateCount; // Pufferfish + } + + if (!state.isAir()) { +@@ -212,6 +214,7 @@ public class LevelChunkSection { + + if (!fluid1.isEmpty()) { + ++this.tickingFluidCount; ++ ++this.fluidStateCount; // Pufferfish + } + + this.updateKnownBlockInfo(x | (z << 4) | (y << 8), iblockdata1, state); // Paper +@@ -260,6 +263,7 @@ public class LevelChunkSection { + if (fluid.isRandomlyTicking()) { + this.tickingFluidCount = (short) (this.tickingFluidCount + 1); + } ++ this.fluidStateCount++; // Pufferfish + } + + }); +diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +index 4cdfc433df67afcd455422e9baf56f167dd712ae..57fcf3910f45ce371ac2e237b277b1034caaac4e 100644 +--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java ++++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +@@ -8,7 +8,7 @@ import javax.annotation.Nullable; + import net.minecraft.world.entity.Entity; + + public class EntityTickList { +- private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? ++ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? // Pufferfish - private->public + + private void ensureActiveIsNotIterated() { + // Paper - replace with better logic, do not delay removals +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index 0ffc131baf5c0edc4f2ca0f466fcdb20be4a47b8..3f72703d2063a082546305eeb0a1b21629ddb1b2 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -43,6 +43,8 @@ public abstract class FlowingFluid extends Fluid { + public static final BooleanProperty FALLING = BlockStateProperties.FALLING; + public static final IntegerProperty LEVEL = BlockStateProperties.LEVEL_FLOWING; + private static final int CACHE_SIZE = 200; ++ // Pufferfish start - use our own cache ++ /* + private static final ThreadLocal> OCCLUSION_CACHE = ThreadLocal.withInitial(() -> { + Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap = new Object2ByteLinkedOpenHashMap(200) { + protected void rehash(int i) {} +@@ -51,6 +53,14 @@ public abstract class FlowingFluid extends Fluid { + object2bytelinkedopenhashmap.defaultReturnValue((byte) 127); + return object2bytelinkedopenhashmap; + }); ++ */ ++ ++ private static final ThreadLocal> localFluidDirectionCache = ThreadLocal.withInitial(() -> { ++ // Pufferfish todo - mess with this number for performance ++ // with 2048 it seems very infrequent on a small world that it has to remove old entries ++ return new gg.airplane.structs.FluidDirectionCache<>(2048); ++ }); ++ // Pufferfish end + private final Map shapes = Maps.newIdentityHashMap(); + + public FlowingFluid() {} +@@ -239,6 +249,8 @@ public abstract class FlowingFluid extends Fluid { + } + + private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) { ++ // Pufferfish start - modify to use our cache ++ /* + Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; + + if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { +@@ -246,9 +258,16 @@ public abstract class FlowingFluid extends Fluid { + } else { + object2bytelinkedopenhashmap = null; + } ++ */ ++ gg.airplane.structs.FluidDirectionCache cache = null; ++ ++ if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { ++ cache = localFluidDirectionCache.get(); ++ } + + Block.BlockStatePairKey block_a; + ++ /* + if (object2bytelinkedopenhashmap != null) { + block_a = new Block.BlockStatePairKey(state, fromState, face); + byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(block_a); +@@ -259,11 +278,22 @@ public abstract class FlowingFluid extends Fluid { + } else { + block_a = null; + } ++ */ ++ if (cache != null) { ++ block_a = new Block.BlockStatePairKey(state, fromState, face); ++ Boolean flag = cache.getValue(block_a); ++ if (flag != null) { ++ return flag; ++ } ++ } else { ++ block_a = null; ++ } + + VoxelShape voxelshape = state.getCollisionShape(world, pos); + VoxelShape voxelshape1 = fromState.getCollisionShape(world, fromPos); + boolean flag = !Shapes.mergedFaceOccludes(voxelshape, voxelshape1, face); + ++ /* + if (object2bytelinkedopenhashmap != null) { + if (object2bytelinkedopenhashmap.size() == 200) { + object2bytelinkedopenhashmap.removeLastByte(); +@@ -271,6 +301,11 @@ public abstract class FlowingFluid extends Fluid { + + object2bytelinkedopenhashmap.putAndMoveToFirst(block_a, (byte) (flag ? 1 : 0)); + } ++ */ ++ if (cache != null) { ++ cache.putValue(block_a, flag); ++ } ++ // Pufferfish end + + return flag; + } +diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootContext.java b/src/main/java/net/minecraft/world/level/storage/loot/LootContext.java +index 35f9b11a3a61976c952a2c1c64bb2a932538f54f..9e9ac64764cf0a84e25e75d8d6f516cde6047284 100644 +--- a/src/main/java/net/minecraft/world/level/storage/loot/LootContext.java ++++ b/src/main/java/net/minecraft/world/level/storage/loot/LootContext.java +@@ -41,8 +41,10 @@ public class LootContext { + this.level = world; + this.lootTables = tableGetter; + this.conditions = conditionGetter; +- this.params = ImmutableMap.copyOf(parameters); +- this.dynamicDrops = ImmutableMap.copyOf(drops); ++ // Pufferfish start - use unmodifiable maps instead of immutable ones to skip the copy ++ this.params = java.util.Collections.unmodifiableMap(parameters); ++ this.dynamicDrops = java.util.Collections.unmodifiableMap(drops); ++ // Pufferfish end + } + + public boolean hasParam(LootContextParam parameter) { +diff --git a/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java b/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java +index ebe65474a4a05ff1637d7f37ebcfe690af59def5..42142c512b12e5b269c19f1e821c50e7496a5f25 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java +@@ -19,47 +19,66 @@ public class EntityCollisionContext implements CollisionContext { + return defaultValue; + } + }; +- private final boolean descending; +- private final double entityBottom; +- private final ItemStack heldItem; +- private final Predicate canStandOnFluid; ++ // Pufferfish start - remove these and pray no plugin uses them ++ // private final boolean descending; ++ // private final double entityBottom; ++ // private final ItemStack heldItem; ++ // private final Predicate canStandOnFluid; ++ // Pufferfish end + @Nullable + private final Entity entity; + + protected EntityCollisionContext(boolean descending, double minY, ItemStack heldItem, Predicate walkOnFluidPredicate, @Nullable Entity entity) { +- this.descending = descending; +- this.entityBottom = minY; +- this.heldItem = heldItem; +- this.canStandOnFluid = walkOnFluidPredicate; ++ // Pufferfish start - remove these ++ // this.descending = descending; ++ // this.entityBottom = minY; ++ // this.heldItem = heldItem; ++ // this.canStandOnFluid = walkOnFluidPredicate; ++ // Pufferfish end + this.entity = entity; + } + + /** @deprecated */ + @Deprecated + protected EntityCollisionContext(Entity entity) { +- this(entity.isDescending(), entity.getY(), entity instanceof LivingEntity ? ((LivingEntity)entity).getMainHandItem() : ItemStack.EMPTY, entity instanceof LivingEntity ? ((LivingEntity)entity)::canStandOnFluid : (fluidState) -> { +- return false; +- }, entity); ++ // Pufferfish start - remove this ++ // this(entity.isDescending(), entity.getY(), entity instanceof LivingEntity ? ((LivingEntity)entity).getMainHandItem() : ItemStack.EMPTY, entity instanceof LivingEntity ? ((LivingEntity)entity)::canStandOnFluid : (fluidState) -> { ++ // return false; ++ // }, entity); ++ // Pufferfish end ++ this.entity = entity; + } + + @Override + public boolean isHoldingItem(Item item) { +- return this.heldItem.is(item); ++ // Pufferfish start ++ Entity entity = this.entity; ++ if (entity instanceof LivingEntity livingEntity) { ++ return livingEntity.getMainHandItem().is(item); ++ } ++ return ItemStack.EMPTY.is(item); ++ // Pufferfish end + } + + @Override + public boolean canStandOnFluid(FluidState stateAbove, FluidState state) { +- return this.canStandOnFluid.test(state) && !stateAbove.getType().isSame(state.getType()); ++ // Pufferfish start ++ Entity entity = this.entity; ++ if (entity instanceof LivingEntity livingEntity) { ++ return livingEntity.canStandOnFluid(state) && !stateAbove.getType().isSame(state.getType()); ++ } ++ return false; ++ // Pufferfish end + } + + @Override + public boolean isDescending() { +- return this.descending; ++ return this.entity != null && this.entity.isDescending(); // Pufferfish + } + + @Override + public boolean isAbove(VoxelShape shape, BlockPos pos, boolean defaultValue) { +- return this.entityBottom > (double)pos.getY() + shape.max(Direction.Axis.Y) - (double)1.0E-5F; ++ return (this.entity == null ? -Double.MAX_VALUE : entity.getY()) > (double)pos.getY() + shape.max(Direction.Axis.Y) - (double)1.0E-5F; // Pufferfish + } + + @Nullable +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index bbb8335dae0a3e2761e6bbb8dc723bcf28cd82ba..7ddf52de4b095f63c75b696008fcdde6345fc3c8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -261,7 +261,7 @@ import javax.annotation.Nullable; // Paper + import javax.annotation.Nonnull; // Paper + + public final class CraftServer implements Server { +- private final String serverName = "Paper"; // Paper ++ private final String serverName = "Pufferfish"; // Paper // Pufferfish + private final String serverVersion; + private final String bukkitVersion = Versioning.getBukkitVersion(); + private final Logger logger = Logger.getLogger("Minecraft"); +@@ -1048,6 +1048,11 @@ public final class CraftServer implements Server { + plugin.getDescription().getName(), + "This plugin is not properly shutting down its async tasks when it is being shut down. This task may throw errors during the final shutdown logs and might not complete before process dies." + )); ++ getLogger().log(Level.SEVERE, String.format("%s Stacktrace", worker.getThread().getName())); ++ StackTraceElement[] stackTrace = worker.getThread().getStackTrace(); ++ for (StackTraceElement element : stackTrace) { ++ getLogger().log(Level.SEVERE, " " + element.toString()); ++ } + } + } + // Paper end +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java +index f7ea77dd82d978ad307f99c743efacfb34478b3d..009ab06182359862b8f543030ec4fe4e2572c93c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java +@@ -44,6 +44,6 @@ public class CraftShapelessRecipe extends ShapelessRecipe implements CraftRecipe + data.set(i, toNMS(ingred.get(i), true)); + } + +- MinecraftServer.getServer().getRecipeManager().addRecipe(new net.minecraft.world.item.crafting.ShapelessRecipe(CraftNamespacedKey.toMinecraft(this.getKey()), this.getGroup(), CraftRecipe.getCategory(this.getCategory()), CraftItemStack.asNMSCopy(this.getResult()), data)); ++ MinecraftServer.getServer().getRecipeManager().addRecipe(new net.minecraft.world.item.crafting.ShapelessRecipe(CraftNamespacedKey.toMinecraft(this.getKey()), this.getGroup(), CraftRecipe.getCategory(this.getCategory()), CraftItemStack.asNMSCopy(this.getResult()), data, true)); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 1244c1ca5e0907298b1ca6de538db82b2209ec4d..6aa2121e286dd6d43201a38722ea0cdd205baaa7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -468,7 +468,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + + @Override + public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { +- return new com.destroystokyo.paper.PaperVersionFetcher(); ++ return new gg.pufferfish.pufferfish.PufferfishVersionFetcher(); // Pufferfish + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index 774556a62eb240da42e84db4502e2ed43495be17..80553face9c70c2a3d897681e7761df85b22d464 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +@@ -11,7 +11,7 @@ public final class Versioning { + public static String getBukkitVersion() { + String result = "Unknown-Version"; + +- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/gg.pufferfish.pufferfish/pufferfish-api/pom.properties"); // Pufferfish + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index e881584d38dc354204479863f004e974a0ac6c07..63d3fcc45be732a4cd2dc8b5347d860fd6577bdd 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -38,6 +38,10 @@ import co.aikar.timings.MinecraftTimings; + import net.minecraft.world.entity.schedule.Activity; + import net.minecraft.world.level.Level; + import net.minecraft.world.phys.AABB; ++// Pufferfish start ++import net.minecraft.world.phys.Vec3; ++import java.util.List; ++// Pufferfish end + + public class ActivationRange + { +@@ -216,6 +220,25 @@ public class ActivationRange + for (int i = 0; i < entities.size(); i++) { + Entity entity = entities.get(i); + ActivationRange.activateEntity(entity); ++ ++ // Pufferfish start ++ if (gg.pufferfish.pufferfish.PufferfishConfig.dearEnabled && entity.getType().dabEnabled) { ++ if (!entity.activatedPriorityReset) { ++ entity.activatedPriorityReset = true; ++ entity.activatedPriority = gg.pufferfish.pufferfish.PufferfishConfig.maximumActivationPrio; ++ } ++ Vec3 playerVec = player.position(); ++ Vec3 entityVec = entity.position(); ++ double diffX = playerVec.x - entityVec.x, diffY = playerVec.y - entityVec.y, diffZ = playerVec.z - entityVec.z; ++ int squaredDistance = (int) (diffX * diffX + diffY * diffY + diffZ * diffZ); ++ entity.activatedPriority = squaredDistance > gg.pufferfish.pufferfish.PufferfishConfig.startDistanceSquared ? ++ Math.max(1, Math.min(squaredDistance >> gg.pufferfish.pufferfish.PufferfishConfig.activationDistanceMod, entity.activatedPriority)) : ++ 1; ++ } else { ++ entity.activatedPriority = 1; ++ } ++ // Pufferfish end ++ + } + // Paper end + } +@@ -232,12 +255,12 @@ public class ActivationRange + if ( MinecraftServer.currentTick > entity.activatedTick ) + { + if ( entity.defaultActivationState ) +- { ++ { // Pufferfish - diff on change + entity.activatedTick = MinecraftServer.currentTick; + return; + } + if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) +- { ++ { // Pufferfish - diff on change + entity.activatedTick = MinecraftServer.currentTick; + } + } +@@ -291,7 +314,7 @@ public class ActivationRange + if ( entity instanceof LivingEntity ) + { + LivingEntity living = (LivingEntity) entity; +- if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 ) // Paper ++ if ( living.onClimableCached() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 ) // Paper // Pufferfish - use cached + { + return 1; // Paper + } diff --git a/patches/server/0002-Purpur-Server-Changes.patch b/patches/server/0002-Purpur-Server-Changes.patch new file mode 100644 index 0000000..7f22979 --- /dev/null +++ b/patches/server/0002-Purpur-Server-Changes.patch @@ -0,0 +1,27878 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Mon, 6 Mar 2023 09:30:48 +0000 +Subject: [PATCH] Purpur Server Changes + +Original: PurpurMC +Copyright (C) 2023 PurpurMC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +diff --git a/build.gradle.kts b/build.gradle.kts +index af8a561e4d59d98ebb6d7a8fbcb072e361b10058..b6fde7181a58037a2e2a6fd23daac9766127fc0e 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -7,9 +7,9 @@ plugins { + } + + dependencies { +- implementation(project(":pufferfish-api")) // Pufferfish // Paper ++ implementation(project(":purpur-api")) // Purpur + // Pufferfish start +- implementation("io.papermc.paper:paper-mojangapi:1.19.2-R0.1-SNAPSHOT") { ++ implementation("io.papermc.paper:paper-mojangapi:1.19.3-R0.1-SNAPSHOT") { + exclude("io.papermc.paper", "paper-api") + } + // Pufferfish end +@@ -42,6 +42,9 @@ dependencies { + runtimeOnly("mysql:mysql-connector-java:8.0.29") + runtimeOnly("com.lmax:disruptor:3.4.4") // Paper + ++ implementation("cat.inspiracio:rhino-js-engine:1.7.7.1") // Purpur ++ implementation("dev.omega24:upnp4j:1.0") // Purpur ++ + runtimeOnly("org.apache.maven:maven-resolver-provider:3.8.5") + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3") + runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.3") +@@ -81,7 +84,7 @@ tasks.jar { + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", + "Implementation-Title" to "CraftBukkit", +- "Implementation-Version" to "git-Pufferfish-$implementationVersion", // Pufferfish ++ "Implementation-Version" to "git-Purpur-$implementationVersion", // Purpur + "Implementation-Vendor" to date, // Paper + "Specification-Title" to "Bukkit", + "Specification-Version" to project.version, +@@ -153,7 +156,7 @@ fun TaskContainer.registerRunTask( + name: String, + block: JavaExec.() -> Unit + ): TaskProvider = register(name) { +- group = "paper" ++ group = "paperweight" // Purpur + mainClass.set("org.bukkit.craftbukkit.Main") + standardInput = System.`in` + workingDir = rootProject.layout.projectDirectory +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 061716934ba0a1f01e4d85d664034f72b3c7a765..9713263c3bd34ab8a3bfc0a8797ba0b1b88ed733 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Pufferfish", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish ++ Metrics metrics = new Metrics("Purpur", serverUUID, logFailedRequests, Bukkit.getLogger()); // Purpur + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -602,8 +602,8 @@ public class Metrics { + })); + + metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); +- metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); +- metrics.addCustomChart(new Metrics.SimplePie("pufferfish_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); ++ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() ? "bungee" : "offline"))); // Purpur ++ metrics.addCustomChart(new Metrics.SimplePie("purpur_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +index bf42969859545a8a520923ef1836ffa4a5cc24a0..fba5dbdb7bcbb55400ef18342c9b54612972a718 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -19,8 +19,10 @@ import java.util.stream.StreamSupport; + + public class PaperVersionFetcher implements VersionFetcher { + private static final java.util.regex.Pattern VER_PATTERN = java.util.regex.Pattern.compile("^([0-9\\.]*)\\-.*R"); // R is an anchor, will always give '-R' at end +- private static final String GITHUB_BRANCH_NAME = "master"; +- private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads"; ++ // Purpur start ++ private static final String DOWNLOAD_PAGE = "https://purpurmc.org/downloads"; ++ private static int distance = -2; public int distance() { return distance; } ++ // Purpur end + private static @Nullable String mcVer; + + @Override +@@ -31,11 +33,11 @@ public class PaperVersionFetcher implements VersionFetcher { + @Nonnull + @Override + public Component getVersionMessage(@Nonnull String serverVersion) { +- String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]"); +- final Component updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]); ++ String[] parts = serverVersion.substring("git-Purpur-".length()).split("[-\\s]"); // Purpur ++ final Component updateMessage = getUpdateStatusMessage("PurpurMC/Purpur", "ver/" + getMinecraftVersion(), parts[0]); // Purpur + final Component history = getHistory(); + +- return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage; ++ return history != null ? Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()), history, updateMessage) : updateMessage; // Purpur + } + + private static @Nullable String getMinecraftVersion() { +@@ -45,7 +47,7 @@ public class PaperVersionFetcher implements VersionFetcher { + String result = matcher.group(); + mcVer = result.substring(0, result.length() - 2); // strip 'R' anchor and trailing '-' + } else { +- org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to PaperMC!"); ++ org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Purpur!"); // Purpur + org.bukkit.Bukkit.getLogger().warning("Pattern: " + VER_PATTERN.toString()); + org.bukkit.Bukkit.getLogger().warning("Version: " + org.bukkit.Bukkit.getBukkitVersion()); + } +@@ -55,7 +57,7 @@ public class PaperVersionFetcher implements VersionFetcher { + } + + private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) { +- int distance; ++ //int distance; // Purpur - use field + try { + int jenkinsBuild = Integer.parseInt(versionInfo); + distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion()); +@@ -66,13 +68,13 @@ public class PaperVersionFetcher implements VersionFetcher { + + switch (distance) { + case -1: +- return Component.text("Error obtaining version information", NamedTextColor.YELLOW); ++ return Component.text("* Error obtaining version information", NamedTextColor.RED); // Purpur + case 0: +- return Component.text("You are running the latest version", NamedTextColor.GREEN); ++ return Component.text("* You are running the latest version", NamedTextColor.GREEN); // Purpur + case -2: +- return Component.text("Unknown version", NamedTextColor.YELLOW); ++ return Component.text("* Unknown version", NamedTextColor.RED); // Purpur + default: +- return Component.text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) ++ return Component.text("* You are " + distance + " version(s) behind", NamedTextColor.YELLOW) // Purpur + .append(Component.newline()) + .append(Component.text("Download the new version at: ") + .append(Component.text(DOWNLOAD_PAGE, NamedTextColor.GOLD) +@@ -85,15 +87,11 @@ public class PaperVersionFetcher implements VersionFetcher { + if (siteApiVersion == null) { return -1; } + try { + try (BufferedReader reader = Resources.asCharSource( +- new URL("https://api.papermc.io/v2/projects/paper/versions/" + siteApiVersion), ++ new URL("https://api.purpurmc.org/v2/purpur/" + siteApiVersion), // Purpur + Charsets.UTF_8 + ).openBufferedStream()) { + JsonObject json = new Gson().fromJson(reader, JsonObject.class); +- JsonArray builds = json.getAsJsonArray("builds"); +- int latest = StreamSupport.stream(builds.spliterator(), false) +- .mapToInt(e -> e.getAsInt()) +- .max() +- .getAsInt(); ++ int latest = json.getAsJsonObject("builds").getAsJsonPrimitive("latest").getAsInt(); // Purpur + return latest - jenkinsBuild; + } catch (JsonSyntaxException ex) { + ex.printStackTrace(); +@@ -144,6 +142,6 @@ public class PaperVersionFetcher implements VersionFetcher { + return null; + } + +- return Component.text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); ++ return org.bukkit.ChatColor.parseMM("Previous: %s", oldVersion); // Purpur + } + } +diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +index c5d5648f4ca603ef2b1df723b58f9caf4dd3c722..3cb56595822799926a8141e60a42f5d1edfc6de5 100644 +--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java ++++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +@@ -17,7 +17,7 @@ public final class PaperConsole extends SimpleTerminalConsole { + @Override + protected LineReader buildReader(LineReaderBuilder builder) { + builder +- .appName("Paper") ++ .appName("Purpur") // Purpur + .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) + .completer(new ConsoleCommandCompleter(this.server)) + .option(LineReader.Option.COMPLETE_IN_WORD, true); +diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java +index ed74f2b90afaa43ae66fbd4797d23cfac9ea9e88..9b4ebe6a9311aa57609b00b6d40722b1fb39aec5 100644 +--- a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java ++++ b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java +@@ -136,6 +136,10 @@ public class MobGoalHelper { + static { + // TODO these kinda should be checked on each release, in case obfuscation changes + deobfuscationMap.put("abstract_skeleton_1", "abstract_skeleton_melee"); ++ // Purpur start ++ deobfuscationMap.put("zombie_1", "zombie_attack_villager"); ++ deobfuscationMap.put("drowned_1", "drowned_attack_villager"); ++ // Purpur end + + ignored.add("goal_selector_1"); + ignored.add("goal_selector_2"); +diff --git a/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java b/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java +index fa56cd09102a89692b42f1d14257990508c5c720..f9251183df72ddc56662fd3f02acf21641a2200c 100644 +--- a/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java ++++ b/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java +@@ -58,7 +58,7 @@ public class RAMDetails extends JList { + GraphData data = RAMGraph.DATA.peekLast(); + Vector vector = new Vector<>(); + +- double[] tps = new double[] {server.tps1.getAverage(), server.tps5.getAverage(), server.tps15.getAverage()}; ++ double[] tps = new double[] {server.tps5s.getAverage(), server.tps1.getAverage(), server.tps5.getAverage(), server.tps15.getAverage()}; // Purpur + String[] tpsAvg = new String[tps.length]; + + for ( int g = 0; g < tps.length; g++) { +@@ -67,7 +67,7 @@ public class RAMDetails extends JList { + vector.add("Memory use: " + (data.getUsedMem() / 1024L / 1024L) + " mb (" + (data.getFree() * 100L / data.getMax()) + "% free)"); + vector.add("Heap: " + (data.getTotal() / 1024L / 1024L) + " / " + (data.getMax() / 1024L / 1024L) + " mb"); + vector.add("Avg tick: " + DECIMAL_FORMAT.format(getAverage(server.tickTimes)) + " ms"); +- vector.add("TPS from last 1m, 5m, 15m: " + String.join(", ", tpsAvg)); ++ vector.add("TPS from last 5s, 1m, 5m, 15m: " + String.join(", ", tpsAvg)); // Purpur + + setListData(vector); + } +diff --git a/src/main/java/com/mojang/brigadier/tree/CommandNode.java b/src/main/java/com/mojang/brigadier/tree/CommandNode.java +index 39844531b03eb8a6c70700b4ecbf0ff1a557424d..632ae75cb3bbc7a3955872d14ad0fbc2459f32e8 100644 +--- a/src/main/java/com/mojang/brigadier/tree/CommandNode.java ++++ b/src/main/java/com/mojang/brigadier/tree/CommandNode.java +@@ -35,6 +35,7 @@ public abstract class CommandNode implements Comparable> { + private final boolean forks; + private Command command; + public LiteralCommandNode clientNode = null; // Paper ++ private String permission = null; public String getPermission() { return permission; } public void setPermission(String permission) { this.permission = permission; } // Purpur + // CraftBukkit start + public void removeCommand(String name) { + this.children.remove(name); +diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java +index 6e441a1a28ba72a8b1cc09fe5fca71b3c70627d4..47e77541e558e18758ae0fcc2aa4e47261e928b6 100644 +--- a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java ++++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java +@@ -28,6 +28,7 @@ public class PufferfishConfig { + + private static final YamlFile config = new YamlFile(); + private static int updates = 0; ++ public static File pufferfishFile; // Purpur + + private static ConfigurationSection convertToBukkit(org.simpleyaml.configuration.ConfigurationSection section) { + ConfigurationSection newSection = new MemoryConfiguration(); +@@ -50,7 +51,7 @@ public class PufferfishConfig { + } + + public static void load() throws IOException { +- File configFile = new File("pufferfish.yml"); ++ File configFile = pufferfishFile; // Purpur + + if (configFile.exists()) { + try { +@@ -224,7 +225,7 @@ public class PufferfishConfig { + public static int activationDistanceMod; + + private static void dynamicActivationOfBrains() throws IOException { +- dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", true); ++ dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", false); // Purpur + startDistance = getInt("dab.start-distance", "activation-range.start-distance", 12, + "This value determines how far away an entity has to be", + "from the player to start being effected by DEAR."); +@@ -268,7 +269,7 @@ public class PufferfishConfig { + + public static boolean throttleInactiveGoalSelectorTick; + private static void inactiveGoalSelectorThrottle() { +- getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", true, ++ getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", false, // Purpur + "Throttles the AI goal selector in entity inactive ticks.", + "This can improve performance by a few percent, but has minor gameplay implications."); + } +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index e5d9c6f2cbe11c2ded6d8ad111fa6a8b2086dfba..830d863cd9665d58875bfa5ca2bcd22f89ab2d49 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -915,9 +915,9 @@ public final class ChunkHolderManager { + } + + public boolean processTicketUpdates() { +- co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager ++ //co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager // Purpur + return this.processTicketUpdates(true, true, null); +- } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager ++ //} finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager // Purpur + } + + private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +index 8013dd333e27aa5fd0beb431fa32491eec9f5246..e42eb93fd9f6f51ff5bb4b14a2304d4ffcdd8441 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +@@ -1750,7 +1750,7 @@ public final class NewChunkHolder { + boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty()); + boolean canSaveEntities = entities != null; + +- try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper // Purpur + if (canSaveChunk) { + canSaveChunk = this.saveChunk(chunk, unloading); + } +@@ -1764,7 +1764,7 @@ public final class NewChunkHolder { + this.lastEntityUnload = null; + } + } +- } ++ //} // Purpur + + return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; + } +diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +index f0fce4113fb07c64adbec029d177c236cbdcbae8..e94224ed280247ee69dfdff8dc960f2b8729be33 100644 +--- a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +@@ -78,10 +78,10 @@ public class PaperPluginsCommand extends BukkitCommand { + this.setAliases(Arrays.asList("pl")); + } + +- private static List formatProviders(TreeMap> plugins) { ++ private static List formatProviders(TreeMap> plugins, @NotNull CommandSender sender) { // Purpur + List components = new ArrayList<>(plugins.size()); + for (PluginProvider entry : plugins.values()) { +- components.add(formatProvider(entry)); ++ components.add(formatProvider(entry, sender)); // Purpur + } + + boolean isFirst = true; +@@ -109,7 +109,7 @@ public class PaperPluginsCommand extends BukkitCommand { + return formattedSublists; + } + +- private static Component formatProvider(PluginProvider provider) { ++ private static Component formatProvider(PluginProvider provider, @NotNull CommandSender sender) { // Purpur + TextComponent.Builder builder = Component.text(); + if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) { + builder.append(LEGACY_PLUGIN_STAR); +@@ -117,12 +117,64 @@ public class PaperPluginsCommand extends BukkitCommand { + + String name = provider.getMeta().getName(); + Component pluginName = Component.text(name, fromStatus(provider)) +- .clickEvent(ClickEvent.runCommand("/version " + name)); ++ // Purpur start ++ .clickEvent(ClickEvent.suggestCommand("/version " + name)); ++ ++ if (sender instanceof org.bukkit.entity.Player && sender.hasPermission("bukkit.command.version")) { ++ // Event components ++ String description = provider.getMeta().getDescription(); ++ TextComponent.Builder hover = Component.text(); ++ hover.append(Component.text("Version: ", NamedTextColor.WHITE)).append(Component.text(provider.getMeta().getVersion(), NamedTextColor.GREEN)); ++ ++ if (description != null) { ++ hover.append(Component.newline()) ++ .append(Component.text("Description: ", NamedTextColor.WHITE)) ++ .append(Component.text(description, NamedTextColor.GREEN)); ++ } ++ ++ if (provider.getMeta().getWebsite() != null) { ++ hover.append(Component.newline()) ++ .append(Component.text("Website: ", NamedTextColor.WHITE)) ++ .append(Component.text(provider.getMeta().getWebsite(), NamedTextColor.GREEN)); ++ } ++ ++ if (!provider.getMeta().getAuthors().isEmpty()) { ++ hover.append(Component.newline()); ++ if (provider.getMeta().getAuthors().size() == 1) { ++ hover.append(Component.text("Author: ")); ++ } else { ++ hover.append(Component.text("Authors: ")); ++ } ++ ++ hover.append(getAuthors(provider.getMeta())); ++ } ++ ++ pluginName.hoverEvent(hover.build()); ++ } + + builder.append(pluginName); ++ // Purpur end ++ ++ return builder.build(); ++ } ++ ++ // Purpur start ++ @NotNull ++ private static TextComponent getAuthors(@NotNull final PluginMeta pluginMeta) { ++ TextComponent.Builder builder = Component.text(); ++ List authors = pluginMeta.getAuthors(); ++ ++ for (int i = 0; i < authors.size(); i++) { ++ if (i > 0) { ++ builder.append(Component.text(i < authors.size() - 1 ? ", " : " and ", NamedTextColor.WHITE)); ++ } ++ ++ builder.append(Component.text(authors.get(i), NamedTextColor.GREEN)); ++ } + + return builder.build(); + } ++ // Purpur end + + private static Component asPlainComponents(String strings) { + net.kyori.adventure.text.TextComponent.Builder builder = Component.text(); +@@ -182,24 +234,24 @@ public class PaperPluginsCommand extends BukkitCommand { + } + } + +- Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); ++ //Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); + //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs + +- sender.sendMessage(infoMessage); ++ //sender.sendMessage(infoMessage); // Purpur + + if (!paperPlugins.isEmpty()) { +- sender.sendMessage(PAPER_HEADER); ++ sender.sendMessage(PAPER_HEADER.append(Component.text(" (%s):".formatted(paperPlugins.size())))); // Purpur + } + +- for (Component component : formatProviders(paperPlugins)) { ++ for (Component component : formatProviders(paperPlugins, sender)) { // Purpur + sender.sendMessage(component); + } + + if (!spigotPlugins.isEmpty()) { +- sender.sendMessage(BUKKIT_HEADER); ++ sender.sendMessage(BUKKIT_HEADER.append(Component.text(" (%s):".formatted(spigotPlugins.size())))); // Purpur + } + +- for (Component component : formatProviders(spigotPlugins)) { ++ for (Component component : formatProviders(spigotPlugins, sender)) { // Purpur + sender.sendMessage(component); + } + +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 01bdf134fc21220ab7ecca51f2dcd51c0b466bba..6bf14183a3fcd2b3d166752ce33240d2ff1ffa7c 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -67,14 +67,14 @@ public class GlobalConfiguration extends ConfigurationPart { + + @Override + public void postProcess() { +- // Pufferfish start ++ /*// Pufferfish start // Purpur + if (enabled && !reallyEnabled) { + Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] To improve performance, timings have been disabled by default"); + Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] You can still use timings by using /timings on, but they will not start on server startup unless you set timings.really-enabled to true in paper.yml"); + Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] If you would like to disable this message, either set timings.really-enabled to true or timings.enabled to false."); + } + enabled = reallyEnabled; +- // Pufferfish end ++ */// Pufferfish end // Purpur + MinecraftTimings.processConfig(this); + } + } +diff --git a/src/main/java/io/papermc/paper/console/HexFormattingConverter.java b/src/main/java/io/papermc/paper/console/HexFormattingConverter.java +index b9922b07cb105618390187d98acdf89e728e1f5a..6a1eda942aa33fc0802066416f8bc64f5f15d011 100644 +--- a/src/main/java/io/papermc/paper/console/HexFormattingConverter.java ++++ b/src/main/java/io/papermc/paper/console/HexFormattingConverter.java +@@ -38,6 +38,7 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + private static final String ANSI_RESET = "\u001B[m"; + + private static final char COLOR_CHAR = 0x7f; ++ private static final char LEGACY_CHAR = 0xa7; // Purpur + public static final LegacyComponentSerializer SERIALIZER = LegacyComponentSerializer.builder() + .hexColors() + .flattener(PaperAdventure.FLATTENER) +@@ -49,6 +50,8 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + private static final String RESET_RGB_ANSI = ANSI_RESET + RGB_ANSI; + private static final Pattern NAMED_PATTERN = Pattern.compile(COLOR_CHAR + "[0-9a-fk-orA-FK-OR]"); + private static final Pattern RGB_PATTERN = Pattern.compile(COLOR_CHAR + "#([0-9a-fA-F]){6}"); ++ private static final Pattern LEGACY_RGB_PATTERN = Pattern.compile(LEGACY_CHAR + "x((" + LEGACY_CHAR + "[0-9a-fA-F]){6})"); // Purpur ++ private static final Pattern LEGACY_PATTERN = Pattern.compile(LEGACY_CHAR + "([0-9a-fk-orxA-FK-ORX])"); // Purpur + + private static final String[] RGB_ANSI_CODES = new String[]{ + formatHexAnsi(NamedTextColor.BLACK), // Black ยง0 +@@ -134,7 +137,21 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + } + + private static String convertRGBColors(final String input) { +- return RGB_PATTERN.matcher(input).replaceAll(result -> { ++ // Purpur start - lets just shove this back in place ++ Matcher matcher = LEGACY_RGB_PATTERN.matcher(input); ++ StringBuilder buffer = new StringBuilder(); ++ while (matcher.find()) { ++ String s = matcher.group().replace(String.valueOf(LEGACY_CHAR), "").replace('x', '#'); ++ int hex = Integer.decode(s); ++ int red = (hex >> 16) & 0xFF; ++ int green = (hex >> 8) & 0xFF; ++ int blue = hex & 0xFF; ++ String replacement = String.format(RGB_ANSI, red, green, blue); ++ matcher.appendReplacement(buffer, replacement); ++ } ++ matcher.appendTail(buffer); ++ return RGB_PATTERN.matcher(buffer.toString()).replaceAll(result -> { ++ // Purpur end + final int hex = Integer.decode(result.group().substring(1)); + return formatHexAnsi(hex); + }); +@@ -152,10 +169,11 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + } + + private static String stripRGBColors(final String input) { +- return RGB_PATTERN.matcher(input).replaceAll(""); ++ return LEGACY_RGB_PATTERN.matcher(RGB_PATTERN.matcher(input).replaceAll("")).replaceAll(""); // Purpur + } + + static void format(String content, StringBuilder result, int start, boolean ansi) { ++ content = LEGACY_PATTERN.matcher(content).replaceAll(COLOR_CHAR + "$1"); // Purpur + int next = content.indexOf(COLOR_CHAR); + int last = content.length() - 1; + if (next == -1 || next == last) { +diff --git a/src/main/java/io/papermc/paper/logging/SysoutCatcher.java b/src/main/java/io/papermc/paper/logging/SysoutCatcher.java +index 76d0d00cd6742991e3f3ec827a75ee87d856b6c9..38480793e300f9d8f3404617a9a85bae2f313df2 100644 +--- a/src/main/java/io/papermc/paper/logging/SysoutCatcher.java ++++ b/src/main/java/io/papermc/paper/logging/SysoutCatcher.java +@@ -54,9 +54,9 @@ public final class SysoutCatcher { + final JavaPlugin plugin = JavaPlugin.getProvidingPlugin(clazz); + + // Instead of just printing the message, send it to the plugin's logger +- plugin.getLogger().log(this.level, this.prefix + line); ++ plugin.getLogger().log(this.level, /*this.prefix +*/ line); // Purpur - prefix not needed + +- if (SysoutCatcher.SUPPRESS_NAGS) { ++ if (true || SysoutCatcher.SUPPRESS_NAGS) { // Purpur - nagging is annoying + return; + } + if (SysoutCatcher.NAG_INTERVAL > 0 || SysoutCatcher.NAG_TIMEOUT > 0) { +diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +index f7e43c693140b7a820b2432db312df8f7b099d4d..7ca119409eaab2052920e8d425bfde87a8ffc205 100644 +--- a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java ++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +@@ -102,6 +102,7 @@ public class PluginInitializerManager { + java.util.List files = (java.util.List) optionSet.valuesOf("add-plugin"); + // Register plugins from the flag + io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files); ++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.SparkProviderSource.INSTANCE, new File("cache", "spark.jar").toPath()); // Purpur + } + + // This will be the end of me... +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..74e3334ec92e3864b84e299b33ca995224eb7c3f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java +@@ -0,0 +1,82 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import java.io.BufferedReader; ++import java.io.File; ++import java.io.InputStreamReader; ++import java.math.BigInteger; ++import java.net.URL; ++import java.net.URLConnection; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.security.MessageDigest; ++import java.util.stream.Collectors; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.slf4j.Logger; ++ ++public class SparkProviderSource extends FileProviderSource { ++ public static final SparkProviderSource INSTANCE = new SparkProviderSource(); ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public SparkProviderSource() { ++ super("File '%s' specified by Purpur"::formatted); ++ } ++ ++ @Override ++ public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception { ++ if (!Boolean.getBoolean("Purpur.IReallyDontWantSpark")) { ++ try { ++ File file = context.toFile(); ++ file.getParentFile().mkdirs(); ++ ++ boolean shouldDownload = true; ++ if (file.exists()) { ++ String fileSha1 = String.format("%040x", new BigInteger(1, MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(file.toPath())))); ++ String sparkSha1; ++ URLConnection urlConnection = new URL("https://sparkapi.lucko.me/download/bukkit/sha1").openConnection(); ++ urlConnection.setReadTimeout(5000); ++ urlConnection.setConnectTimeout(5000); ++ try (BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()))) { ++ sparkSha1 = reader.lines().collect(Collectors.joining("")); ++ } ++ ++ if (fileSha1.equals(sparkSha1)) { ++ shouldDownload = false; ++ } ++ } ++ ++ if (shouldDownload) { ++ URLConnection urlConnection = new URL("https://sparkapi.lucko.me/download/bukkit").openConnection(); ++ urlConnection.setReadTimeout(5000); ++ urlConnection.setConnectTimeout(5000); ++ Files.copy(urlConnection.getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); ++ } ++ ++ if (hasSpark()) { ++ LOGGER.info("Purpur: Using user-provided spark plugin instead of our own."); ++ } else { ++ super.registerProviders(entrypointHandler, context); ++ } ++ ++ } catch (Exception e) { ++ LOGGER.error("Purpur: Failed to download and install spark plugin"); ++ e.printStackTrace(); ++ } ++ } ++ } ++ ++ private static boolean hasSpark() { ++ for (PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { ++ if (provider.getMeta().getName().equalsIgnoreCase("spark")) { ++ return true; ++ } ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index abe37c7c3c6f5ab73afd738ec78f06d7e4d2ed96..b5b6657e52e4f7a630229bd3ba433438af293e22 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -123,6 +123,10 @@ public class CrashReport { + StringBuilder stringbuilder = new StringBuilder(); + + stringbuilder.append("---- Minecraft Crash Report ----\n"); ++ // Purpur start ++ stringbuilder.append("// "); ++ stringbuilder.append("// DO NOT REPORT THIS TO PAPER! REPORT TO PURPUR INSTEAD!"); ++ // Purpur end + stringbuilder.append("// "); + stringbuilder.append(CrashReport.getErrorComment()); + stringbuilder.append("\n\n"); +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index 7b6b51392b123d34382233adcf4c3d4867bdaa32..941f3a0d50329658a9380500ef039d7f10a284e2 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -212,6 +212,21 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy + } + // CraftBukkit end + ++ // Purpur start ++ public boolean testPermission(int i, String bukkitPermission) { ++ if (hasPermission(i, bukkitPermission)) { ++ return true; ++ } ++ String permissionMessage = getLevel().getServer().server.getPermissionMessage(); ++ if (!permissionMessage.isBlank()) { ++ for (String line : permissionMessage.replace("", bukkitPermission).split("\n")) { ++ sendFailure(Component.literal(line)); ++ } ++ } ++ return false; ++ } ++ // Purpur end ++ + public Vec3 getPosition() { + return this.worldPosition; + } +@@ -317,6 +332,30 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy + } + } + ++ // Purpur start ++ public void sendSuccess(@Nullable String message) { ++ sendSuccess(message, false); ++ } ++ ++ public void sendSuccess(@Nullable String message, boolean broadcastToOps) { ++ if (message == null) { ++ return; ++ } ++ sendSuccess(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message), broadcastToOps); ++ } ++ ++ public void sendSuccess(@Nullable net.kyori.adventure.text.Component message) { ++ sendSuccess(message, false); ++ } ++ ++ public void sendSuccess(@Nullable net.kyori.adventure.text.Component message, boolean broadcastToOps) { ++ if (message == null) { ++ return; ++ } ++ sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), broadcastToOps); ++ } ++ // Purpur end ++ + public void sendSuccess(Component message, boolean broadcastToOps) { + if (this.source.acceptsSuccess() && !this.silent) { + this.source.sendSystemMessage(message); +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index e92864ecf32dd984f6f87f7b05341e43af3a2977..139a57b38eac74887c950041e890e1613d8d3073 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -145,7 +145,7 @@ public class Commands { + CloneCommands.register(this.dispatcher, commandRegistryAccess); + DataCommands.register(this.dispatcher); + DataPackCommand.register(this.dispatcher); +- DebugCommand.register(this.dispatcher); ++ //DebugCommand.register(this.dispatcher); // Purpur + DefaultGameModeCommands.register(this.dispatcher); + DifficultyCommand.register(this.dispatcher); + EffectCommands.register(this.dispatcher, commandRegistryAccess); +@@ -216,6 +216,14 @@ public class Commands { + SetPlayerIdleTimeoutCommand.register(this.dispatcher); + StopCommand.register(this.dispatcher); + WhitelistCommand.register(this.dispatcher); ++ org.purpurmc.purpur.command.CreditsCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.DemoCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.PingCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.UptimeCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.TPSBarCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.CompassCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.RamBarCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.RamCommand.register(this.dispatcher); // Purpur + } + + if (environment.includeIntegrated) { +@@ -303,9 +311,9 @@ public class Commands { + public int performCommand(ParseResults parseresults, String s, String label) { // CraftBukkit + CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource(); + +- commandlistenerwrapper.getServer().getProfiler().push(() -> { ++ /*commandlistenerwrapper.getServer().getProfiler().push(() -> { // Purpur + return "/" + s; +- }); ++ });*/ // Purpur + + byte b0; + +@@ -388,7 +396,7 @@ public class Commands { + b0 = 0; + } + } finally { +- commandlistenerwrapper.getServer().getProfiler().pop(); ++ //commandlistenerwrapper.getServer().getProfiler().pop(); // Purpur + } + + return b0; +@@ -448,6 +456,7 @@ public class Commands { + private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { + // Paper end - Async command map building + new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper ++ if (PlayerCommandSendEvent.getHandlerList().getRegisteredListeners().length > 0) { // Purpur - skip all this crap if there's nothing listening + PlayerCommandSendEvent event = new PlayerCommandSendEvent(player.getBukkitEntity(), new LinkedHashSet<>(bukkit)); + event.getPlayer().getServer().getPluginManager().callEvent(event); + +@@ -458,6 +467,7 @@ public class Commands { + } + } + // CraftBukkit end ++ } // Purpur - skip event + player.connection.send(new ClientboundCommandsPacket(rootcommandnode)); + } + +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +index f25b9330e068c7d9e12cb57a7761cfef9ebaf7bc..7e66aaa960ce7b6dda7c064d4c6856cc4b368b58 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +@@ -200,10 +200,10 @@ public class EntitySelector { + + if (this.playerName != null) { + entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); +- return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); ++ return entityplayer == null || !canSee(source, entityplayer) ? Collections.emptyList() : Lists.newArrayList(entityplayer); // Purpur + } else if (this.entityUUID != null) { + entityplayer = source.getServer().getPlayerList().getPlayer(this.entityUUID); +- return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); ++ return entityplayer == null || !canSee(source, entityplayer) ? Collections.emptyList() : Lists.newArrayList(entityplayer); // Purpur + } else { + Vec3 vec3d = (Vec3) this.position.apply(source.getPosition()); + Predicate predicate = this.getPredicate(vec3d); +@@ -213,7 +213,7 @@ public class EntitySelector { + ServerPlayer entityplayer1 = (ServerPlayer) source.getEntity(); + + if (predicate.test(entityplayer1)) { +- return Lists.newArrayList(new ServerPlayer[]{entityplayer1}); ++ return !canSee(source, entityplayer1) ? Collections.emptyList() : Lists.newArrayList(entityplayer1); // Purpur + } + } + +@@ -224,6 +224,7 @@ public class EntitySelector { + + if (this.isWorldLimited()) { + object = source.getLevel().getPlayers(predicate, i); ++ ((List) object).removeIf(entityplayer3 -> !canSee(source, (ServerPlayer) entityplayer3)); // Purpur + } else { + object = Lists.newArrayList(); + Iterator iterator = source.getServer().getPlayerList().getPlayers().iterator(); +@@ -231,7 +232,7 @@ public class EntitySelector { + while (iterator.hasNext()) { + ServerPlayer entityplayer2 = (ServerPlayer) iterator.next(); + +- if (predicate.test(entityplayer2)) { ++ if (predicate.test(entityplayer2) && canSee(source, entityplayer2)) { // Purpur + ((List) object).add(entityplayer2); + if (((List) object).size() >= i) { + return (List) object; +@@ -276,4 +277,10 @@ public class EntitySelector { + public static Component joinNames(List entities) { + return ComponentUtils.formatList(entities, Entity::getDisplayName); + } ++ ++ // Purpur start ++ private boolean canSee(CommandSourceStack sender, ServerPlayer target) { ++ return !org.purpurmc.purpur.PurpurConfig.hideHiddenPlayersFromEntitySelector || !(sender.getEntity() instanceof ServerPlayer player) || player.getBukkitEntity().canSee(target.getBukkitEntity()); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index b1d12c78edf21cc29a9f9ca54e7957ddc8875ffb..a3e398d3bcc88f9c0feaa6ca8dc646f3c522c0a9 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -41,6 +41,12 @@ public class BlockPos extends Vec3i { + private static final int X_OFFSET = 38; + // Paper end + ++ // Purpur start ++ public BlockPos(net.minecraft.world.entity.Entity entity) { ++ super(entity.getX(), entity.getY(), entity.getZ()); ++ } ++ // Purpur end ++ + public BlockPos(int x, int y, int z) { + super(x, y, z); + } +diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java +index c1172ba542bc07e0c780a50d5b4ce26ac04c1720..a4dc96b1a3bf309584657e3a1e7dfaea967f9425 100644 +--- a/src/main/java/net/minecraft/core/Direction.java ++++ b/src/main/java/net/minecraft/core/Direction.java +@@ -247,6 +247,12 @@ public enum Direction implements StringRepresentable { + case EAST: + var10000 = SOUTH; + break; ++ // Purpur start ++ case UP: ++ return UP; ++ case DOWN: ++ return DOWN; ++ // Purpur end + default: + throw new IllegalStateException("Unable to get Y-rotated facing of " + this); + } +@@ -359,6 +365,12 @@ public enum Direction implements StringRepresentable { + case EAST: + var10000 = NORTH; + break; ++ // Purpur start ++ case UP: ++ return UP; ++ case DOWN: ++ return DOWN; ++ // Purpur end + default: + throw new IllegalStateException("Unable to get CCW facing of " + this); + } +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 58fa7b99dc7a9745afe6faf31c1804e95ed27dbe..ed137bb15f76bac5a73f3dec215078c21e4953b9 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -51,6 +51,7 @@ import net.minecraft.world.item.SpawnEggItem; + import net.minecraft.world.item.alchemy.PotionUtils; + import net.minecraft.world.item.alchemy.Potions; + import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.AnvilBlock; + import net.minecraft.world.level.block.BaseFireBlock; + import net.minecraft.world.level.block.BeehiveBlock; + import net.minecraft.world.level.block.Block; +@@ -1161,6 +1162,23 @@ public interface DispenseItemBehavior { + } + } + }); ++ // Purpur start ++ DispenserBlock.registerBehavior(Items.ANVIL, (new OptionalDispenseItemBehavior() { ++ @Override ++ public ItemStack execute(BlockSource dispenser, ItemStack stack) { ++ Level level = dispenser.getLevel(); ++ if (!level.purpurConfig.dispenserPlaceAnvils) return super.execute(dispenser, stack); ++ Direction facing = dispenser.getBlockState().getValue(DispenserBlock.FACING); ++ BlockPos pos = dispenser.getPos().relative(facing); ++ BlockState state = level.getBlockState(pos); ++ if (state.isAir()) { ++ level.setBlockAndUpdate(pos, Blocks.ANVIL.defaultBlockState().setValue(AnvilBlock.FACING, facing.getAxis() == Direction.Axis.Y ? Direction.NORTH : facing.getClockWise())); ++ stack.shrink(1); ++ } ++ return stack; ++ } ++ })); ++ // Purpur end + } + + static void setEntityPokingOutOfBlock(BlockSource pointer, Entity entity, Direction direction) { +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index d1127d93a85a837933d0d73c24cacac4adc3a5b9..d9a6d273108165f59b995b1fd7748cb5c12b8b1f 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -107,7 +107,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + continue; + } + // CraftBukkit end +- ishearable.shear(SoundSource.BLOCKS); ++ ishearable.shear(SoundSource.BLOCKS, net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MOB_LOOTING, CraftItemStack.asNMSCopy(craftItem))); // Purpur + worldserver.gameEvent((Entity) null, GameEvent.SHEAR, blockposition); + return true; + } +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 38c09c65dfa4a7a0c80d36f726c1fd028cbe05f8..52c7f83f525d150ce30e33f220d879d1d125508f 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -562,11 +562,20 @@ public class Connection extends SimpleChannelInboundHandler> { + private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper + private static int joinAttemptsThisTick; // Paper + private static int currTick; // Paper ++ private static int tickSecond; // Purpur + public void tick() { + this.flushQueue(); + // Paper start + if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { + Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.maxJoinsPerSecond) { ++ if (++Connection.tickSecond > 20) { ++ Connection.tickSecond = 0; ++ Connection.joinAttemptsThisTick = 0; ++ } ++ } else ++ // Purpur end + Connection.joinAttemptsThisTick = 0; + } + // Paper end +diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java +index 32ee4ed11aefd82dca2e3e78b3108f041fdc3695..314318a21b6fa9e827945d8996c6ed0f9679a4eb 100644 +--- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java ++++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java +@@ -89,6 +89,8 @@ public class FriendlyByteBuf extends ByteBuf { + private static final int MAX_PUBLIC_KEY_HEADER_SIZE = 256; + private static final int MAX_PUBLIC_KEY_LENGTH = 512; + ++ public static boolean hasItemSerializeEvent = false; // Purpur ++ + public FriendlyByteBuf(ByteBuf parent) { + this.source = parent; + } +@@ -632,6 +634,13 @@ public class FriendlyByteBuf extends ByteBuf { + this.writeBoolean(false); + } else { + this.writeBoolean(true); ++ // Purpur start ++ if (hasItemSerializeEvent) { ++ var event = new org.purpurmc.purpur.event.packet.NetworkItemSerializeEvent(stack.asBukkitCopy()); ++ event.callEvent(); ++ stack = ItemStack.fromBukkitCopy(event.getItemStack()); ++ } ++ // Purpur end + Item item = stack.getItem(); + + this.writeId(BuiltInRegistries.ITEM, item); +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index 27d4aa45e585842c04491839826d405d6f447f0e..b693a26ac6a5729bf91aca9ca0ae332b904d436f 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -47,7 +47,7 @@ public class PacketUtils { + if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590 + if (listener.getConnection().isConnected()) { + co.aikar.timings.Timing timing = co.aikar.timings.MinecraftTimings.getPacketTiming(packet); // Paper - timings +- try (co.aikar.timings.Timing ignored = timing.startTiming()) { // Paper - timings ++ try { // Paper - timings // Purpur + packet.handle(listener); + } catch (Exception exception) { + net.minecraft.network.Connection networkmanager = listener.getConnection(); +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java +index 53b75f5737a910ffc5448cd9a85eae57f9c1488f..ea95873dd034779e56a8b924cd27f9375be05daf 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java +@@ -9,6 +9,7 @@ public class ClientboundPlayerCombatKillPacket implements Packet { + private final long gameTime; +- private final long dayTime; ++ private long dayTime; public void setDayTime(long dayTime) { this.dayTime = dayTime; } // Purpur + + public ClientboundSetTimePacket(long time, long timeOfDay, boolean doDaylightCycle) { + this.gameTime = time; +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 33a5e900c2cab99c311fa5f5b71a609cf8f802cb..1772800c123353207e3563a7e2c2b70431aec097 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -246,7 +246,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +@@ -301,10 +302,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ //this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; // Purpur ++ //this.profiler = this.metricsRecorder.getProfiler(); // Purpur ++ /*this.onMetricsRecordingStopped = (methodprofilerresults) -> { // Purpur + this.stopRecordingMetrics(); +- }; +- this.onMetricsRecordingFinished = (path) -> { +- }; ++ };*/ // Purpur ++ //this.onMetricsRecordingFinished = (path) -> { // Purpur ++ //}; // Purpur + this.status = new ServerStatus(); + this.random = RandomSource.create(); + this.port = -1; +@@ -924,7 +927,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + return !this.canOversleep(); + }); +- isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); ++ //isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Purpur + // Paper end + new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper + +@@ -1424,7 +1449,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.tickCount % autosavePeriod == 0; + try { + this.isSaving = true; +@@ -1439,20 +1464,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Purpur + Iterator iterator = this.getAllLevels().iterator(); // Paper - move down + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ worldserver.hasRidableMoveEvent = org.purpurmc.purpur.event.entity.RidableMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Purpur + net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper + +- this.profiler.push(() -> { ++ /*this.profiler.push(() -> { // Purpur + return worldserver + " " + worldserver.dimension().location(); +- }); ++ });*/ // Purpur + /* Drop global time updates + if (this.tickCount % 20 == 0) { +- this.profiler.push("timeSync"); ++ //this.profiler.push("timeSync"); // Purpur + this.playerList.broadcastAll(new PacketPlayOutUpdateTime(worldserver.getGameTime(), worldserver.getDayTime(), worldserver.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)), worldserver.dimension()); +- this.profiler.pop(); ++ //this.profiler.pop(); // Purpur + } + // CraftBukkit end */ + +- this.profiler.push("tick"); ++ //this.profiler.push("tick"); // Purpur + + try { +- worldserver.timings.doTick.startTiming(); // Spigot ++ //worldserver.timings.doTick.startTiming(); // Spigot // Purpur + worldserver.tick(shouldKeepTicking); + // Paper start + for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { + regionManager.recalculateRegions(); + } + // Paper end +- worldserver.timings.doTick.stopTiming(); // Spigot ++ //worldserver.timings.doTick.stopTiming(); // Spigot // Purpur + } catch (Throwable throwable) { + // Spigot Start + CrashReport crashreport; +@@ -1556,33 +1583,33 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return org.purpurmc.purpur.PurpurConfig.serverModName; // Purpur - Purpur > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! + } + + public SystemReport fillSystemReport(SystemReport details) { +@@ -1849,17 +1876,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + this.executeBlocking(() -> { + this.saveDebugReport(path.resolve("server")); +@@ -2490,40 +2512,40 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop resultConsumer, Consumer dumpConsumer) { +- this.onMetricsRecordingStopped = (methodprofilerresults) -> { ++ /*this.onMetricsRecordingStopped = (methodprofilerresults) -> { // Purpur + this.stopRecordingMetrics(); + resultConsumer.accept(methodprofilerresults); + }; + this.onMetricsRecordingFinished = dumpConsumer; +- this.willStartRecordingMetrics = true; ++ this.willStartRecordingMetrics = true;*/ // Purpur + } + + public void stopRecordingMetrics() { +- this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; ++ //this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; // Purpur + } + + public void finishRecordingMetrics() { +- this.metricsRecorder.end(); ++ //this.metricsRecorder.end(); // Purpur + } + + public void cancelRecordingMetrics() { +- this.metricsRecorder.cancel(); +- this.profiler = this.metricsRecorder.getProfiler(); ++ //this.metricsRecorder.cancel(); // Purpur ++ //this.profiler = this.metricsRecorder.getProfiler(); // Purpur + } + + public Path getWorldPath(LevelResource worldSavePath) { +@@ -2572,15 +2594,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + final io.papermc.paper.adventure.ChatDecorationProcessor processor = new io.papermc.paper.adventure.ChatDecorationProcessor(this, sender, commandSourceStack, message); +@@ -2744,7 +2775,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop functions, ResourceLocation label) { +- ProfilerFiller gameprofilerfiller = this.server.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.server.getProfiler(); // Purpur + + Objects.requireNonNull(label); +- gameprofilerfiller.push(label::toString); ++ //gameprofilerfiller.push(label::toString); // Purpur + Iterator iterator = functions.iterator(); + + while (iterator.hasNext()) { +@@ -69,7 +69,7 @@ public class ServerFunctionManager { + this.execute(customfunction, this.getGameLoopSender()); + } + +- this.server.getProfiler().pop(); ++ //this.server.getProfiler().pop(); // Purpur + } + + public int execute(CommandFunction function, CommandSourceStack source) { +@@ -88,7 +88,7 @@ public class ServerFunctionManager { + } else { + int i; + +- try (co.aikar.timings.Timing timing = function.getTiming().startTiming()) { // Paper ++ try /*(co.aikar.timings.Timing timing = function.getTiming().startTiming())*/ { // Paper // Purpur + this.context = new ServerFunctionManager.ExecutionContext(tracer); + i = this.context.runTopCommand(function, source); + } finally { +@@ -177,10 +177,10 @@ public class ServerFunctionManager { + + try { + ServerFunctionManager.QueuedCommand customfunctiondata_b = (ServerFunctionManager.QueuedCommand) this.commandQueue.removeFirst(); +- ProfilerFiller gameprofilerfiller = ServerFunctionManager.this.server.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = ServerFunctionManager.this.server.getProfiler(); // Purpur + + Objects.requireNonNull(customfunctiondata_b); +- gameprofilerfiller.push(customfunctiondata_b::toString); ++ //gameprofilerfiller.push(customfunctiondata_b::toString); // Purpur + this.depth = customfunctiondata_b.depth; + customfunctiondata_b.execute(ServerFunctionManager.this, this.commandQueue, i, this.tracer); + if (!this.nestedCalls.isEmpty()) { +@@ -192,7 +192,7 @@ public class ServerFunctionManager { + this.nestedCalls.clear(); + } + } finally { +- ServerFunctionManager.this.server.getProfiler().pop(); ++ //ServerFunctionManager.this.server.getProfiler().pop(); // Purpur + } + + ++j; +diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +index e639c0ec642910e66b1d68ae0b9208ef58d91fce..24c4ad919eeb9c5e15572ee32b0895c993ac6735 100644 +--- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java ++++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +@@ -48,7 +48,7 @@ public class EnchantCommand { + + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { + Enchantment enchantment2 = enchantment.value(); +- if (level > enchantment2.getMaxLevel()) { ++ if (!org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchantCommand && level > enchantment2.getMaxLevel()) { // Purpur + throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel()); + } else { + int i = 0; +@@ -58,7 +58,7 @@ public class EnchantCommand { + LivingEntity livingEntity = (LivingEntity)entity; + ItemStack itemStack = livingEntity.getMainHandItem(); + if (!itemStack.isEmpty()) { +- if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) { ++ if ((enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchantCommand && !itemStack.hasEnchantment(enchantment2))) { // Purpur + itemStack.enchant(enchantment2, level); + ++i; + } else if (targets.size() == 1) { +diff --git a/src/main/java/net/minecraft/server/commands/FillCommand.java b/src/main/java/net/minecraft/server/commands/FillCommand.java +index 99fbb24dabe867ed4956a2996543107f58a57193..5c81c64540579fbacc335a3fadf4bf59f853dc39 100644 +--- a/src/main/java/net/minecraft/server/commands/FillCommand.java ++++ b/src/main/java/net/minecraft/server/commands/FillCommand.java +@@ -59,8 +59,10 @@ public class FillCommand { + + private static int fillBlocks(CommandSourceStack source, BoundingBox range, BlockInput block, FillCommand.Mode mode, @Nullable Predicate filter) throws CommandSyntaxException { + int i = range.getXSpan() * range.getYSpan() * range.getZSpan(); +- if (i > 32768) { +- throw ERROR_AREA_TOO_LARGE.create(32768, i); ++ // Purpur start ++ if (i > org.purpurmc.purpur.PurpurConfig.commandFillMaxArea) { ++ throw ERROR_AREA_TOO_LARGE.create(org.purpurmc.purpur.PurpurConfig.commandFillMaxArea, i); ++ // Purpur end + } else { + List list = Lists.newArrayList(); + ServerLevel serverLevel = source.getLevel(); +diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +index 27c0aaf123c3e945eb24e8a3892bd8ac42115733..85e1c1d6eb4472baa958b4f482791e8479dfcbf0 100644 +--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +@@ -41,6 +41,18 @@ public class GameModeCommand { + } + + private static int setMode(CommandContext context, Collection targets, GameType gameMode) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.commandGamemodeRequiresPermission) { ++ String gamemode = gameMode.getName(); ++ CommandSourceStack sender = context.getSource(); ++ if (!sender.testPermission(2, "minecraft.command.gamemode." + gamemode)) { ++ return 0; ++ } ++ if (sender.getEntity() instanceof ServerPlayer player && (targets.size() > 1 || !targets.contains(player)) && !sender.testPermission(2, "minecraft.command.gamemode." + gamemode + ".other")) { ++ return 0; ++ } ++ } ++ // Purpur end + int i = 0; + + for(ServerPlayer serverPlayer : targets) { +diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java +index 06e3a868e922f1b7a586d0ca28f64a67ae463b68..32beb045f990d4da6112da4fea295333cb69e2ea 100644 +--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java +@@ -58,6 +58,7 @@ public class GiveCommand { + boolean flag = entityplayer.getInventory().add(itemstack); + ItemEntity entityitem; + ++ if (org.purpurmc.purpur.PurpurConfig.disableGiveCommandDrops) continue; // Purpur - add config option for toggling give command dropping + if (flag && itemstack.isEmpty()) { + itemstack.setCount(1); + entityitem = entityplayer.drop(itemstack, false, false, false); // SPIGOT-2942: Add boolean to call event +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index ad4fdbdcf09f30d10e61ccf47f8fb9ce6bd92e73..fd1b0564d2d2b45128e6f2556fb93ee56bd683b5 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -218,9 +218,19 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + thread.start(); // Paper - start console thread after MinecraftServer.console & PaperConfig are initialized + io.papermc.paper.command.PaperCommands.registerCommands(this); + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); ++ // Purpur start ++ try { ++ org.purpurmc.purpur.PurpurConfig.init((java.io.File) options.valueOf("purpur-settings")); ++ } catch (Exception e) { ++ DedicatedServer.LOGGER.error("Unable to load server configuration", e); ++ return false; ++ } ++ org.purpurmc.purpur.PurpurConfig.registerCommands(); ++ // Purpur end + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now + io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider + // Paper end ++ gg.pufferfish.pufferfish.PufferfishConfig.pufferfishFile = (java.io.File) options.valueOf("pufferfish-settings"); // Purpur + gg.pufferfish.pufferfish.PufferfishConfig.load(); // Pufferfish + gg.pufferfish.pufferfish.PufferfishCommand.init(); // Pufferfish + +@@ -269,6 +279,30 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + DedicatedServer.LOGGER.warn("Perhaps a server is already running on that port?"); + return false; + } ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useUPnP) { ++ LOGGER.info("[UPnP] Attempting to start UPnP port forwarding service..."); ++ if (dev.omega24.upnp4j.UPnP4J.isUPnPAvailable()) { ++ if (dev.omega24.upnp4j.UPnP4J.isOpen(this.getPort(), dev.omega24.upnp4j.util.Protocol.TCP)) { ++ this.upnp = false; ++ LOGGER.info("[UPnP] Port {} is already open", this.getPort()); ++ } else if (dev.omega24.upnp4j.UPnP4J.open(this.getPort(), dev.omega24.upnp4j.util.Protocol.TCP)) { ++ this.upnp = true; ++ LOGGER.info("[UPnP] Successfully opened port {}", this.getPort()); ++ } else { ++ this.upnp = false; ++ LOGGER.info("[UPnP] Failed to open port {}", this.getPort()); ++ } ++ ++ if (upnp) { ++ LOGGER.info("[UPnP] {}:{}", dev.omega24.upnp4j.UPnP4J.getExternalIP(), this.getPort()); ++ } ++ } else { ++ this.upnp = false; ++ LOGGER.error("[UPnP] Service is unavailable"); ++ } ++ } ++ // Purpur end + + // CraftBukkit start + // this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); // Spigot - moved up +@@ -342,6 +376,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + if (gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning) mobSpawnExecutor.start(); // Pufferfish ++ ++ org.purpurmc.purpur.task.BossBarTask.startAll(); // Purpur ++ org.purpurmc.purpur.task.BeehiveTask.instance().register(); // Purpur ++ + return true; + } + } +@@ -488,7 +526,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + public void handleConsoleInputs() { +- MinecraftTimings.serverCommandTimer.startTiming(); // Spigot ++ //MinecraftTimings.serverCommandTimer.startTiming(); // Spigot // Purpur + // Paper start - use proper queue + ConsoleInput servercommand; + while ((servercommand = this.serverCommandQueue.poll()) != null) { +@@ -505,7 +543,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + // CraftBukkit end + } + +- MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot ++ //MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +index c7e4330c93baff1f3027d7c75cf857b673d38970..5134fed0cd0eedbe0c2177bce91b978b20061517 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +@@ -58,6 +58,7 @@ public class DedicatedServerProperties extends Settings public + // this function is so hot that removing the map lookup call can have an order of magnitude impact on its performance + // tested and confirmed via System.nanoTime() + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange; +@@ -1250,24 +1250,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - optimised tracker + private final void processTrackQueue() { +- this.level.timings.tracker1.startTiming(); ++ //this.level.timings.tracker1.startTiming(); // Purpur + try { + for (TrackedEntity tracker : this.entityMap.values()) { + // update tracker entry + tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); + } + } finally { +- this.level.timings.tracker1.stopTiming(); ++ //this.level.timings.tracker1.stopTiming(); // Purpur + } + + +- this.level.timings.tracker2.startTiming(); ++ //this.level.timings.tracker2.startTiming(); // Purpur + try { + for (TrackedEntity tracker : this.entityMap.values()) { + tracker.serverEntity.sendChanges(); + } + } finally { +- this.level.timings.tracker2.stopTiming(); ++ //this.level.timings.tracker2.stopTiming(); // Purpur + } + } + // Paper end - optimised tracker +@@ -1282,7 +1282,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + List list = Lists.newArrayList(); + List list1 = this.level.players(); + ObjectIterator objectiterator = this.entityMap.values().iterator(); +- level.timings.tracker1.startTiming(); // Paper ++ //level.timings.tracker1.startTiming(); // Paper // Purpur + + ChunkMap.TrackedEntity playerchunkmap_entitytracker; + +@@ -1307,17 +1307,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + playerchunkmap_entitytracker.serverEntity.sendChanges(); + } + } +- level.timings.tracker1.stopTiming(); // Paper ++ //level.timings.tracker1.stopTiming(); // Paper // Purpur + + if (!list.isEmpty()) { + objectiterator = this.entityMap.values().iterator(); + +- level.timings.tracker2.startTiming(); // Paper ++ //level.timings.tracker2.startTiming(); // Paper // Purpur + while (objectiterator.hasNext()) { + playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); + playerchunkmap_entitytracker.updatePlayers(list); + } +- level.timings.tracker2.stopTiming(); // Paper ++ //level.timings.tracker2.stopTiming(); // Paper // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index c6f5d6756fa0e068a462d9c0ded12e0771abba37..0ae45cf5a084fd412305e8b2f5dabe608b4eb1c1 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -431,16 +431,16 @@ public class ServerChunkCache extends ChunkSource { + return ifLoaded; + } + // Paper end +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- gameprofilerfiller.incrementCounter("getChunk"); ++ //gameprofilerfiller.incrementCounter("getChunk"); // Purpur + long k = ChunkPos.asLong(x, z); + + ChunkAccess ichunkaccess; + + // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system + +- gameprofilerfiller.incrementCounter("getChunkCacheMiss"); ++ //gameprofilerfiller.incrementCounter("getChunkCacheMiss"); // Purpur + CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create, true); // Paper + ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor; + +@@ -450,10 +450,10 @@ public class ServerChunkCache extends ChunkSource { + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system + // Paper end + com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x1, z1); // Paper - sync load info +- this.level.timings.syncChunkLoad.startTiming(); // Paper ++ //this.level.timings.syncChunkLoad.startTiming(); // Paper // Purpur + chunkproviderserver_b.managedBlock(completablefuture::isDone); + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - async chunk debug // Paper - rewrite chunk system +- this.level.timings.syncChunkLoad.stopTiming(); // Paper ++ //this.level.timings.syncChunkLoad.stopTiming(); // Paper // Purpur + } // Paper + ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> { + return ichunkaccess1; +@@ -601,17 +601,17 @@ public class ServerChunkCache extends ChunkSource { + + public void save(boolean flush) { + this.runDistanceManagerUpdates(); +- try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ //try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings // Purpur + this.chunkMap.saveAllChunks(flush); +- } // Paper - Timings ++ //} // Paper - Timings // Purpur + } + + // Paper start - duplicate save, but call incremental + public void saveIncrementally() { + this.runDistanceManagerUpdates(); +- try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ //try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings // Purpur + this.chunkMap.saveIncrementally(); +- } // Paper - Timings ++ //} // Paper - Timings // Purpur + } + // Paper end + +@@ -628,36 +628,36 @@ public class ServerChunkCache extends ChunkSource { + // CraftBukkit start - modelled on below + public void purgeUnload() { + if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system +- this.level.getProfiler().push("purge"); ++ //this.level.getProfiler().push("purge"); // Purpur + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +- this.level.getProfiler().popPush("unload"); ++ //this.level.getProfiler().popPush("unload"); // Purpur + this.chunkMap.tick(() -> true); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.clearCache(); + } + // CraftBukkit end + + @Override + public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) { +- this.level.getProfiler().push("purge"); +- this.level.timings.doChunkMap.startTiming(); // Spigot ++ //this.level.getProfiler().push("purge"); // Purpur ++ //this.level.timings.doChunkMap.startTiming(); // Spigot // Purpur + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +- this.level.timings.doChunkMap.stopTiming(); // Spigot +- this.level.getProfiler().popPush("chunks"); ++ //this.level.timings.doChunkMap.stopTiming(); // Spigot // Purpur ++ //this.level.getProfiler().popPush("chunks"); // Purpur + if (tickChunks) { +- this.level.timings.chunks.startTiming(); // Paper - timings ++ //this.level.timings.chunks.startTiming(); // Paper - timings // Purpur + this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes + this.tickChunks(); +- this.level.timings.chunks.stopTiming(); // Paper - timings ++ //this.level.timings.chunks.stopTiming(); // Paper - timings // Purpur + } + +- this.level.timings.doChunkUnload.startTiming(); // Spigot +- this.level.getProfiler().popPush("unload"); ++ //this.level.timings.doChunkUnload.startTiming(); // Spigot // Purpur ++ //this.level.getProfiler().popPush("unload"); // Purpur + this.chunkMap.tick(shouldKeepTicking); +- this.level.timings.doChunkUnload.stopTiming(); // Spigot +- this.level.getProfiler().pop(); ++ //this.level.timings.doChunkUnload.stopTiming(); // Spigot // Purpur ++ //this.level.getProfiler().pop(); // Purpur + this.clearCache(); + } + +@@ -703,15 +703,15 @@ public class ServerChunkCache extends ChunkSource { + } + // Paper end - optimize isOutisdeRange + LevelData worlddata = this.level.getLevelData(); +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- gameprofilerfiller.push("pollingChunks"); ++ //gameprofilerfiller.push("pollingChunks"); // Purpur + this.level.resetIceAndSnowTick(); // Pufferfish - reset ice & snow tick random + int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit + +- gameprofilerfiller.push("naturalSpawnCount"); +- this.level.timings.countNaturalMobs.startTiming(); // Paper - timings ++ //gameprofilerfiller.push("naturalSpawnCount"); // Purpur ++ //this.level.timings.countNaturalMobs.startTiming(); // Paper - timings // Purpur + int l = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - per player mob spawning + NaturalSpawner.SpawnState spawnercreature_d; // moved down +@@ -732,16 +732,16 @@ public class ServerChunkCache extends ChunkSource { + // Pufferfish end + } + // Paper end +- this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings ++ //this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings // Purpur + + //this.lastSpawnState = spawnercreature_d; // Pufferfish - this is managed asynchronously +- gameprofilerfiller.popPush("filteringLoadedChunks"); ++ //gameprofilerfiller.popPush("filteringLoadedChunks"); // Purpur + // Paper - moved down +- this.level.timings.chunkTicks.startTiming(); // Paper ++ //this.level.timings.chunkTicks.startTiming(); // Paper // Purpur + + // Paper - moved down + +- gameprofilerfiller.popPush("spawnAndTick"); ++ //gameprofilerfiller.popPush("spawnAndTick"); // Purpur + boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + + // Paper - only shuffle if per-player mob spawning is disabled +@@ -791,17 +791,17 @@ public class ServerChunkCache extends ChunkSource { + } + } + // Paper end - optimise chunk tick iteration +- this.level.timings.chunkTicks.stopTiming(); // Paper +- gameprofilerfiller.popPush("customSpawners"); ++ //this.level.timings.chunkTicks.stopTiming(); // Paper // Purpur ++ //gameprofilerfiller.popPush("customSpawners"); // Purpur + if (flag2) { +- try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings ++ //try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings // Purpur + this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); +- } // Paper - timings ++ //} // Paper - timings // Purpur + } +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + // Paper start - use set of chunks requiring updates, rather than iterating every single one loaded +- gameprofilerfiller.popPush("broadcast"); +- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing ++ //gameprofilerfiller.popPush("broadcast"); // Purpur ++ //this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing // Purpur + if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { + ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); + this.chunkMap.needsChangeBroadcasting.clear(); +@@ -813,8 +813,8 @@ public class ServerChunkCache extends ChunkSource { + } + } + } +- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing +- gameprofilerfiller.pop(); ++ //this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing // Purpur ++ //gameprofilerfiller.pop(); // Purpur + // Paper end - use set of chunks requiring updates, rather than iterating every single one loaded + // Paper start - controlled flush for entity tracker packets + List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); +@@ -1029,7 +1029,7 @@ public class ServerChunkCache extends ChunkSource { + + @Override + protected void doRunTask(Runnable task) { +- ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); ++ //ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); // Purpur + super.doRunTask(task); + } + +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 50cf4d200bc2892f2140c9929193b4b20ad2bd17..0f9a3a6c05fee59c29764f0c0d7a6cb8a2a861b1 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -69,7 +69,7 @@ public class ServerEntity { + @Nullable + private List> trackedDataValues; + // CraftBukkit start +- final Set trackedPlayers; // Paper - private -> package ++ public final Set trackedPlayers; // Paper - private -> package // Purpur - package -> public + + public ServerEntity(ServerLevel worldserver, Entity entity, int i, boolean flag, Consumer> consumer, Set trackedPlayers) { + this.trackedPlayers = trackedPlayers; +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 619ee9d8b99970fb6fce19438f29e09858412ac4..a5655ebb233f1e1e1dd7f79fdd948020478928fc 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -214,6 +214,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + private final StructureManager structureManager; + private final StructureCheck structureCheck; + private final boolean tickTime; ++ private double preciseTime; // Purpur ++ private boolean forceTime; // Purpur + public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick + + // CraftBukkit start +@@ -222,6 +224,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public boolean hasPhysicsEvent = true; // Paper + public boolean hasEntityMoveEvent = false; // Paper + private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) ++ public boolean hasRidableMoveEvent = false; // Purpur + public static Throwable getAddToWorldStackTrace(Entity entity) { + final Throwable thr = new Throwable(entity + " Added to world at " + new java.util.Date()); + io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(thr); +@@ -542,7 +545,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.dragonParts = new Int2ObjectOpenHashMap(); + this.tickTime = flag1; + this.server = minecraftserver; +- this.customSpawners = list; ++ // Purpur start - enable/disable MobSpawners per world ++ this.customSpawners = Lists.newArrayList(); ++ if (purpurConfig.phantomSpawning) { ++ customSpawners.add(new net.minecraft.world.level.levelgen.PhantomSpawner()); ++ } ++ if (purpurConfig.patrolSpawning) { ++ customSpawners.add(new net.minecraft.world.level.levelgen.PatrolSpawner()); ++ } ++ if (purpurConfig.catSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.npc.CatSpawner()); ++ } ++ if (purpurConfig.villageSiegeSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.ai.village.VillageSiege()); ++ } ++ if (purpurConfig.villagerTraderSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.npc.WanderingTraderSpawner(iworlddataserver)); ++ } ++ // Purpur end + this.serverLevelData = iworlddataserver; + ChunkGenerator chunkgenerator = worlddimension.generator(); + // CraftBukkit start +@@ -605,6 +625,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system + this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system ++ this.preciseTime = this.serverLevelData.getDayTime(); // Purpur + } + + public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) { +@@ -633,17 +654,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + // Paper end - optimise checkDespawn +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + + this.handlingTick = true; +- gameprofilerfiller.push("world border"); ++ //gameprofilerfiller.push("world border"); // Purpur + this.getWorldBorder().tick(); +- gameprofilerfiller.popPush("weather"); ++ //gameprofilerfiller.popPush("weather"); // Purpur + this.advanceWeatherCycle(); + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + long j; + +- if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { ++ if (this.purpurConfig.playersSkipNight && this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { + // CraftBukkit start + j = this.levelData.getDayTime() + 24000L; + TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); +@@ -665,32 +686,32 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.updateSkyBrightness(); + this.tickTime(); +- gameprofilerfiller.popPush("tickPending"); +- timings.scheduledBlocks.startTiming(); // Paper ++ //gameprofilerfiller.popPush("tickPending"); // Purpur ++ //timings.scheduledBlocks.startTiming(); // Paper // Purpur + if (!this.isDebug()) { + j = this.getGameTime(); +- gameprofilerfiller.push("blockTicks"); ++ //gameprofilerfiller.push("blockTicks"); // Purpur + this.blockTicks.tick(j, 65536, this::tickBlock); +- gameprofilerfiller.popPush("fluidTicks"); ++ //gameprofilerfiller.popPush("fluidTicks"); // Purpur + this.fluidTicks.tick(j, 65536, this::tickFluid); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } +- timings.scheduledBlocks.stopTiming(); // Paper ++ //timings.scheduledBlocks.stopTiming(); // Paper // Purpur + +- gameprofilerfiller.popPush("raid"); +- this.timings.raids.startTiming(); // Paper - timings ++ //gameprofilerfiller.popPush("raid"); // Purpur ++ //this.timings.raids.startTiming(); // Paper - timings // Purpur + this.raids.tick(); +- this.timings.raids.stopTiming(); // Paper - timings +- gameprofilerfiller.popPush("chunkSource"); +- this.timings.chunkProviderTick.startTiming(); // Paper - timings ++ //this.timings.raids.stopTiming(); // Paper - timings // Purpur ++ //gameprofilerfiller.popPush("chunkSource"); // Purpur ++ //this.timings.chunkProviderTick.startTiming(); // Paper - timings // Purpur + this.getChunkSource().tick(shouldKeepTicking, true); +- this.timings.chunkProviderTick.stopTiming(); // Paper - timings +- gameprofilerfiller.popPush("blockEvents"); +- timings.doSounds.startTiming(); // Spigot ++ //this.timings.chunkProviderTick.stopTiming(); // Paper - timings // Purpur ++ //gameprofilerfiller.popPush("blockEvents"); // Purpur ++ //timings.doSounds.startTiming(); // Spigot // Purpur + this.runBlockEvents(); +- timings.doSounds.stopTiming(); // Spigot ++ //timings.doSounds.stopTiming(); // Spigot // Purpur + this.handlingTick = false; +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players + + if (flag) { +@@ -698,25 +719,25 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + if (flag || this.emptyTime++ < 300) { +- gameprofilerfiller.push("entities"); +- timings.tickEntities.startTiming(); // Spigot ++ //gameprofilerfiller.push("entities"); // Purpur ++ //timings.tickEntities.startTiming(); // Spigot // Purpur + if (this.dragonFight != null) { +- gameprofilerfiller.push("dragonFight"); ++ //gameprofilerfiller.push("dragonFight"); // Purpur + this.dragonFight.tick(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + org.spigotmc.ActivationRange.activateEntities(this); // Spigot +- timings.entityTick.startTiming(); // Spigot ++ //timings.entityTick.startTiming(); // Spigot // Purpur + this.entityTickList.forEach((entity) -> { + entity.activatedPriorityReset = false; // Pufferfish - DAB + if (!entity.isRemoved()) { + if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed + entity.discard(); + } else { +- gameprofilerfiller.push("checkDespawn"); ++ //gameprofilerfiller.push("checkDespawn"); // Purpur + entity.checkDespawn(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list + Entity entity1 = entity.getVehicle(); + +@@ -728,7 +749,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + entity.stopRiding(); + } + +- gameprofilerfiller.push("tick"); ++ //gameprofilerfiller.push("tick"); // Purpur + // Pufferfish start - copied from this.guardEntityTick + try { + this.tickNonPassenger(entity); // Pufferfish - changed +@@ -743,20 +764,19 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper end + } + // Pufferfish end +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + } + } + }); +- timings.entityTick.stopTiming(); // Spigot +- timings.tickEntities.stopTiming(); // Spigot +- gameprofilerfiller.pop(); ++ //timings.entityTick.stopTiming(); // Spigot // Purpur ++ //timings.tickEntities.stopTiming(); // Spigot // Purpur ++ //gameprofilerfiller.pop(); // Purpur + this.tickBlockEntities(); + } + +- gameprofilerfiller.push("entityManagement"); ++ //gameprofilerfiller.push("entityManagement"); // Purpur + //this.entityManager.tick(); // Paper - rewrite chunk system +- gameprofilerfiller.pop(); + } + + @Override +@@ -774,6 +794,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setGameTime(i); + this.serverLevelData.getScheduledEvents().tick(this.server, i); + if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ // Purpur start ++ int incrementTicks = isDay() ? this.purpurConfig.daytimeTicks : this.purpurConfig.nighttimeTicks; ++ if (incrementTicks != 12000) { ++ this.preciseTime += 12000 / (double) incrementTicks; ++ this.setDayTime(this.preciseTime); ++ } else ++ // Purpur end + this.setDayTime(this.levelData.getDayTime() + 1L); + } + +@@ -782,7 +809,21 @@ public class ServerLevel extends Level implements WorldGenLevel { + + public void setDayTime(long timeOfDay) { + this.serverLevelData.setDayTime(timeOfDay); ++ // Purpur start ++ this.preciseTime = timeOfDay; ++ this.forceTime = false; + } ++ public void setDayTime(double i) { ++ this.serverLevelData.setDayTime((long) i); ++ this.forceTime = true; ++ // Purpur end ++ } ++ ++ // Purpur start ++ public boolean isForceTime() { ++ return this.forceTime; ++ } ++ // Purpur end + + public void tickCustomSpawners(boolean spawnMonsters, boolean spawnAnimals) { + Iterator iterator = this.customSpawners.iterator(); +@@ -807,7 +848,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // Paper start - optimise random block ticking + private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); +- // private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(); // Pufferfish - moved to super ++ private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - moved to super // Purpur - dont break ABI + // Paper end + + private int currentIceAndSnowTick = 0; protected void resetIceAndSnowTick() { this.currentIceAndSnowTick = this.randomTickRandom.nextInt(16); } // Pufferfish +@@ -817,9 +858,9 @@ public class ServerLevel extends Level implements WorldGenLevel { + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); + int k = chunkcoordintpair.getMinBlockZ(); +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push("thunder"); ++ //gameprofilerfiller.push("thunder"); // Purpur + final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change + + if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && /*this.random.nextInt(this.spigotConfig.thunderChance) == 0 &&*/ chunk.shouldDoLightning(this.random)) { // Spigot // Paper - disable thunder // Pufferfish - replace random with shouldDoLightning +@@ -829,10 +870,18 @@ public class ServerLevel extends Level implements WorldGenLevel { + boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01D) && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper + + if (flag1) { +- SkeletonHorse entityhorseskeleton = (SkeletonHorse) EntityType.SKELETON_HORSE.create(this); ++ // Purpur start ++ net.minecraft.world.entity.animal.horse.AbstractHorse entityhorseskeleton; ++ if (purpurConfig.zombieHorseSpawnChance > 0D && random.nextDouble() <= purpurConfig.zombieHorseSpawnChance) { ++ entityhorseskeleton = EntityType.ZOMBIE_HORSE.create(this); ++ } else { ++ entityhorseskeleton = EntityType.SKELETON_HORSE.create(this); ++ if (entityhorseskeleton != null) ((SkeletonHorse) entityhorseskeleton).setTrap(true); ++ } ++ // Purpur end + + if (entityhorseskeleton != null) { +- entityhorseskeleton.setTrap(true); ++ //entityhorseskeleton.setTrap(true); // Purpur - moved up + entityhorseskeleton.setAge(0); + entityhorseskeleton.setPos((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()); + this.addFreshEntity(entityhorseskeleton, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit +@@ -849,7 +898,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- gameprofilerfiller.popPush("iceandsnow"); ++ //gameprofilerfiller.popPush("iceandsnow"); // Purpur + int l; + + if (!this.paperConfig().environment.disableIceAndSnow && (this.currentIceAndSnowTick++ & 15) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking // Pufferfish - optimize further random ticking +@@ -901,8 +950,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + // Paper start - optimise random block ticking +- gameprofilerfiller.popPush("randomTick"); +- timings.chunkTicksBlocks.startTiming(); // Paper ++ //gameprofilerfiller.popPush("randomTick"); // Purpur ++ //timings.chunkTicksBlocks.startTiming(); // Paper // Purpur + if (randomTickSpeed > 0) { + LevelChunkSection[] sections = chunk.getSections(); + int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); +@@ -936,8 +985,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + // Paper end - optimise random block ticking +- timings.chunkTicksBlocks.stopTiming(); // Paper +- gameprofilerfiller.pop(); ++ //timings.chunkTicksBlocks.stopTiming(); // Paper // Purpur ++ //gameprofilerfiller.pop(); // Purpur + } + + public Optional findLightningRod(BlockPos pos) { +@@ -945,7 +994,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + return holder.is(PoiTypes.LIGHTNING_ROD); + }, (blockposition1) -> { + return blockposition1.getY() == this.getHeight(Heightmap.Types.WORLD_SURFACE, blockposition1.getX(), blockposition1.getZ()) - 1; +- }, pos, 128, PoiManager.Occupancy.ANY); ++ }, pos, org.purpurmc.purpur.PurpurConfig.lightningRodRange, PoiManager.Occupancy.ANY); + + return optional.map((blockposition1) -> { + return blockposition1.above(1); +@@ -994,11 +1043,27 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (this.canSleepThroughNights()) { + if (!this.getServer().isSingleplayer() || this.getServer().isPublished()) { + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); +- MutableComponent ichatmutablecomponent; ++ Component ichatmutablecomponent; + + if (this.sleepStatus.areEnoughSleeping(i)) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.sleepSkippingNight.isBlank()) { ++ return; ++ } ++ if (!org.purpurmc.purpur.PurpurConfig.sleepSkippingNight.equalsIgnoreCase("default")) { ++ ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepSkippingNight)); ++ } else + ichatmutablecomponent = Component.translatable("sleep.skipping_night"); + } else { ++ if (org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent.isBlank()) { ++ return; ++ } ++ if (!org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent.equalsIgnoreCase("default")) { ++ ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent, ++ net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed("count", Integer.toString(this.sleepStatus.amountSleeping())), ++ net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed("total", Integer.toString(this.sleepStatus.sleepersNeeded(i))))); ++ } else ++ // Purpur end + ichatmutablecomponent = Component.translatable("sleep.players_sleeping", this.sleepStatus.amountSleeping(), this.sleepStatus.sleepersNeeded(i)); + } + +@@ -1137,6 +1202,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + private void resetWeatherCycle() { + // CraftBukkit start ++ if (this.purpurConfig.rainStopsAfterSleep) // Purpur + this.serverLevelData.setRaining(false, org.bukkit.event.weather.WeatherChangeEvent.Cause.SLEEP); // Paper - when passing the night + // If we stop due to everyone sleeping we should reset the weather duration to some other random value. + // Not that everyone ever manages to get the whole server to sleep at the same time.... +@@ -1144,6 +1210,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setRainTime(0); + } + // CraftBukkit end ++ if (this.purpurConfig.thunderStopsAfterSleep) // Purpur + this.serverLevelData.setThundering(false, org.bukkit.event.weather.ThunderChangeEvent.Cause.SLEEP); // Paper - when passing the night + // CraftBukkit start + // If we stop due to everyone sleeping we should reset the weather duration to some other random value. +@@ -1211,24 +1278,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Spigot end + // Paper start- timings + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); +- timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper +- try { ++ //timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper // Purpur ++ //try { // Purpur + // Paper end - timings + entity.setOldPosAndRot(); +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + + ++entity.tickCount; +- this.getProfiler().push(() -> { ++ /*this.getProfiler().push(() -> { // Purpur + return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); +- }); +- gameprofilerfiller.incrementCounter("tickNonPassenger"); ++ });*/ // Purpur ++ //gameprofilerfiller.incrementCounter("tickNonPassenger"); // Purpur + if (isActive) { // Paper - EAR 2 + TimingHistory.activatedEntityTicks++; + entity.tick(); + entity.postTick(); // CraftBukkit + } else { entity.inactiveTick(); } // Paper - EAR 2 +- this.getProfiler().pop(); +- } finally { timer.stopTiming(); } // Paper - timings ++ //this.getProfiler().pop(); // Purpur ++ //} finally { timer.stopTiming(); } // Paper - timings // Purpur + Iterator iterator = entity.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1251,17 +1318,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (passenger instanceof Player || this.entityTickList.contains(passenger)) { + // Paper - EAR 2 + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); +- co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper +- try { ++ //co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper // Purpur ++ //try { // Purpur + // Paper end + passenger.setOldPosAndRot(); + ++passenger.tickCount; +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push(() -> { ++ /*gameprofilerfiller.push(() -> { // Purpur + return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString(); +- }); +- gameprofilerfiller.incrementCounter("tickPassenger"); ++ });*/ // Purpur ++ //gameprofilerfiller.incrementCounter("tickPassenger"); // Purpur + // Paper start - EAR 2 + if (isActive) { + passenger.rideTick(); +@@ -1273,7 +1340,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + vehicle.positionRider(passenger); + } + // Paper end - EAR 2 +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + Iterator iterator = passenger.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1282,7 +1349,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.tickPassenger(passenger, entity2); + } + +- } finally { timer.stopTiming(); }// Paper - EAR2 timings ++ //} finally { timer.stopTiming(); }// Paper - EAR2 timings // Purpur + } + } else { + passenger.stopRiding(); +@@ -1302,14 +1369,14 @@ public class ServerLevel extends Level implements WorldGenLevel { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); + } + +- try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { ++ //try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { // Purpur + if (doFull) { + this.saveLevelData(); + } + +- this.timings.worldSaveChunks.startTiming(); // Paper ++ //this.timings.worldSaveChunks.startTiming(); // Paper // Purpur + if (!this.noSave()) chunkproviderserver.saveIncrementally(); +- this.timings.worldSaveChunks.stopTiming(); // Paper ++ //this.timings.worldSaveChunks.stopTiming(); // Paper // Purpur + + // Copied from save() + // CraftBukkit start - moved from MinecraftServer.saveChunks +@@ -1321,7 +1388,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); + } + // CraftBukkit end +- } ++ //} // Purpur + } + // Paper end + +@@ -1335,7 +1402,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + if (!savingDisabled) { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); // CraftBukkit +- try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper // Purpur // Purpur + if (progressListener != null) { + progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); + } +@@ -1345,11 +1412,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + progressListener.progressStage(Component.translatable("menu.savingChunks")); + } + +- timings.worldSaveChunks.startTiming(); // Paper ++ //timings.worldSaveChunks.startTiming(); // Paper // Purpur + if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system + if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system +- timings.worldSaveChunks.stopTiming(); // Paper +- }// Paper ++ //timings.worldSaveChunks.stopTiming(); // Paper // Purpur ++ //}// Paper // Purpur + // Paper - rewrite chunk system - entity saving moved into ChunkHolder + + } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system +@@ -2615,7 +2682,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Spigot Start + if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message + // Paper start +- if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { ++ if (!entity.level.purpurConfig.playerVoidTrading && entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { // Purpur + merchant.getTrader().closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); + } + // Paper end +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 289429eb464548acc80262a49444f49f8f57fc0c..b91bf50a1f450a78c8d16cf5b8772687120e9119 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -268,6 +268,11 @@ public class ServerPlayer extends Player { + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper + public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event ++ public boolean purpurClient = false; // Purpur ++ public boolean acceptingResourcePack = false; // Purpur ++ private boolean ramBar = false; // Purpur ++ private boolean tpsBar = false; // Purpur ++ private boolean compassBar = false; // Purpur + + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); +@@ -367,6 +372,7 @@ public class ServerPlayer extends Player { + this.bukkitPickUpLoot = true; + this.maxHealthCache = this.getMaxHealth(); + this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper ++ this.spawnInvulnerableTime = world.purpurConfig.playerSpawnInvulnerableTicks; // Purpur + } + + // Yes, this doesn't match Vanilla, but it's the best we can do for now. +@@ -506,6 +512,9 @@ public class ServerPlayer extends Player { + } + } + ++ if (nbt.contains("Purpur.RamBar")) { this.ramBar = nbt.getBoolean("Purpur.RamBar"); } // Purpur ++ if (nbt.contains("Purpur.TPSBar")) { this.tpsBar = nbt.getBoolean("Purpur.TPSBar"); } // Purpur ++ if (nbt.contains("Purpur.CompassBar")) { this.compassBar = nbt.getBoolean("Purpur.CompassBar"); } // Purpur + } + + @Override +@@ -572,6 +581,9 @@ public class ServerPlayer extends Player { + } + this.getBukkitEntity().setExtraData(nbt); // CraftBukkit + ++ nbt.putBoolean("Purpur.RamBar", this.ramBar); // Purpur ++ nbt.putBoolean("Purpur.TPSBar", this.tpsBar); // Purpur ++ nbt.putBoolean("Purpur.CompassBar", this.compassBar); // Purpur + } + + // CraftBukkit start - World fallback code, either respawn location or global spawn +@@ -700,6 +712,15 @@ public class ServerPlayer extends Player { + this.trackStartFallingPosition(); + this.trackEnteredOrExitedLavaOnVehicle(); + this.advancements.flushDirty(this); ++ ++ // Purpur start ++ if (this.level.purpurConfig.useNightVisionWhenRiding && this.getVehicle() != null && this.getVehicle().getRider() == this && this.level.getGameTime() % 100 == 0) { // 5 seconds ++ MobEffectInstance nightVision = this.getEffect(MobEffects.NIGHT_VISION); ++ if (nightVision == null || nightVision.getDuration() <= 300) { // 15 seconds ++ this.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 400, 0)); // 20 seconds ++ } ++ } ++ // Purpur end + } + + public void doTick() { +@@ -938,6 +959,7 @@ public class ServerPlayer extends Player { + })); + Team scoreboardteambase = this.getTeam(); + ++ if (org.purpurmc.purpur.PurpurConfig.deathMessageOnlyBroadcastToAffectedPlayer) this.sendSystemMessage(ichatbasecomponent); else // Purpur + if (scoreboardteambase != null && scoreboardteambase.getDeathMessageVisibility() != Team.Visibility.ALWAYS) { + if (scoreboardteambase.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OTHER_TEAMS) { + this.server.getPlayerList().broadcastSystemToTeam(this, ichatbasecomponent); +@@ -1039,14 +1061,30 @@ public class ServerPlayer extends Player { + + } + ++ // Purpur start ++ public boolean isSpawnInvulnerable() { ++ return spawnInvulnerableTime > 0 || frozen; ++ } ++ // Purpur end ++ + @Override + public boolean hurt(DamageSource source, float amount) { + if (this.isInvulnerableTo(source)) { + return false; + } else { ++ // Purpur start ++ if (source == DamageSource.FALL) { ++ if (getRootVehicle() instanceof net.minecraft.world.entity.vehicle.AbstractMinecart && level.purpurConfig.minecartControllable && !level.purpurConfig.minecartControllableFallDamage) { ++ return false; ++ } ++ if (getRootVehicle() instanceof net.minecraft.world.entity.vehicle.Boat && !level.purpurConfig.boatsDoFallDamage) { ++ return false; ++ } ++ } ++ // Purpur end + boolean flag = this.server.isDedicatedServer() && this.isPvpAllowed() && "fall".equals(source.msgId); + +- if (!flag && this.spawnInvulnerableTime > 0 && source != DamageSource.OUT_OF_WORLD) { ++ if (!flag && isSpawnInvulnerable() && source != DamageSource.OUT_OF_WORLD) { // Purpur + return false; + } else { + if (source instanceof EntityDamageSource) { +@@ -1149,7 +1187,7 @@ public class ServerPlayer extends Player { + PortalInfo shapedetectorshape = this.findDimensionEntryPoint(worldserver); + + if (shapedetectorshape != null) { +- worldserver1.getProfiler().push("moving"); ++ //worldserver1.getProfiler().push("moving"); // Purpur + worldserver = shapedetectorshape.world; // CraftBukkit + if (worldserver == null) { } else // CraftBukkit - empty to fall through to null to event + if (resourcekey == LevelStem.OVERWORLD && worldserver.getTypeKey() == LevelStem.NETHER) { // CraftBukkit +@@ -1172,8 +1210,8 @@ public class ServerPlayer extends Player { + worldserver = ((CraftWorld) exit.getWorld()).getHandle(); + // CraftBukkit end + +- worldserver1.getProfiler().pop(); +- worldserver1.getProfiler().push("placing"); ++ //worldserver1.getProfiler().pop(); // Purpur ++ //worldserver1.getProfiler().push("placing"); // Purpur + if (true) { // CraftBukkit + this.isChangingDimension = true; // CraftBukkit - Set teleport invulnerability only if player changing worlds + +@@ -1184,6 +1222,7 @@ public class ServerPlayer extends Player { + playerlist.sendPlayerPermissionLevel(this); + worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); + this.unsetRemoved(); ++ this.portalPos = io.papermc.paper.util.MCUtil.toBlockPosition(exit); // Purpur + + // CraftBukkit end + this.setLevel(worldserver); +@@ -1191,7 +1230,7 @@ public class ServerPlayer extends Player { + this.connection.teleport(exit); // CraftBukkit - use internal teleport without event + this.connection.resetPosition(); + worldserver.addDuringPortalTeleport(this); +- worldserver1.getProfiler().pop(); ++ //worldserver1.getProfiler().pop(); // Purpur + this.triggerDimensionChangeTriggers(worldserver1); + this.connection.send(new ClientboundPlayerAbilitiesPacket(this.getAbilities())); + playerlist.sendLevelInfo(this, worldserver); +@@ -1220,6 +1259,7 @@ public class ServerPlayer extends Player { + } + // Paper end + ++ this.spawnInvulnerableTime = worldserver.purpurConfig.playerSpawnInvulnerableTicks; // Purpur + return this; + } + } +@@ -1341,7 +1381,7 @@ public class ServerPlayer extends Player { + return entitymonster.isPreventingPlayerRest(this); + }); + +- if (!list.isEmpty()) { ++ if (!this.level.purpurConfig.playerSleepNearMonsters && !list.isEmpty()) { // Purpur + return Either.left(Player.BedSleepingProblem.NOT_SAFE); + } + } +@@ -1512,6 +1552,7 @@ public class ServerPlayer extends Player { + + @Override + public void openTextEdit(SignBlockEntity sign) { ++ if (level.purpurConfig.signAllowColors) this.connection.send(sign.getTranslatedUpdatePacket(textFilteringEnabled)); // Purpur + sign.setAllowedPlayerEditor(this.getUUID()); + this.connection.send(new ClientboundBlockUpdatePacket(this.level, sign.getBlockPos())); + this.connection.send(new ClientboundOpenSignEditorPacket(sign.getBlockPos())); +@@ -1738,6 +1779,26 @@ public class ServerPlayer extends Player { + this.lastSentExp = -1; // CraftBukkit - Added to reset + } + ++ // Purpur start ++ public void sendActionBarMessage(@Nullable String message) { ++ if (message != null && !message.isEmpty()) { ++ sendActionBarMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message)); ++ } ++ } ++ ++ public void sendActionBarMessage(@Nullable net.kyori.adventure.text.Component message) { ++ if (message != null) { ++ sendActionBarMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ } ++ ++ public void sendActionBarMessage(@Nullable Component message) { ++ if (message != null) { ++ displayClientMessage(message, true); ++ } ++ } ++ // Purpur end ++ + @Override + public void displayClientMessage(Component message, boolean overlay) { + this.sendSystemMessage(message, overlay); +@@ -2007,6 +2068,7 @@ public class ServerPlayer extends Player { + } + + public void sendTexturePack(String url, String hash, boolean required, @Nullable Component resourcePackPrompt) { ++ this.acceptingResourcePack = true; // Purpur + this.connection.send(new ClientboundResourcePackPacket(url, hash, required, resourcePackPrompt)); + } + +@@ -2021,8 +2083,63 @@ public class ServerPlayer extends Player { + + public void resetLastActionTime() { + this.lastActionTime = Util.getMillis(); ++ this.setAfk(false); // Purpur + } + ++ // Purpur Start ++ private boolean isAfk = false; ++ ++ @Override ++ public void setAfk(boolean afk) { ++ if (this.isAfk == afk) { ++ return; ++ } ++ ++ String msg = afk ? org.purpurmc.purpur.PurpurConfig.afkBroadcastAway : org.purpurmc.purpur.PurpurConfig.afkBroadcastBack; ++ ++ org.purpurmc.purpur.event.PlayerAFKEvent event = new org.purpurmc.purpur.event.PlayerAFKEvent(this.getBukkitEntity(), afk, this.level.purpurConfig.idleTimeoutKick, msg, !Bukkit.isPrimaryThread()); ++ if (!event.callEvent() || event.shouldKick()) { ++ return; ++ } ++ ++ this.isAfk = afk; ++ ++ if (!afk) { ++ resetLastActionTime(); ++ } ++ ++ msg = event.getBroadcastMsg(); ++ if (msg != null && !msg.isEmpty()) { ++ server.getPlayerList().broadcastMiniMessage(String.format(msg, this.getGameProfile().getName()), false); ++ } ++ ++ if (this.level.purpurConfig.idleTimeoutUpdateTabList) { ++ String scoreboardName = getScoreboardName(); ++ String playerListName = getBukkitEntity().getPlayerListName(); ++ String[] split = playerListName.split(scoreboardName); ++ String prefix = (split.length > 0 ? split[0] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix, ""); ++ String suffix = (split.length > 1 ? split[1] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, ""); ++ if (afk) { ++ getBukkitEntity().setPlayerListName(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix + prefix + scoreboardName + suffix + org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, true); ++ } else { ++ getBukkitEntity().setPlayerListName(prefix + scoreboardName + suffix); ++ } ++ } ++ ++ ((ServerLevel) this.level).updateSleepingPlayerList(); ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return this.isAfk; ++ } ++ ++ @Override ++ public boolean canBeCollidedWith() { ++ return !this.isAfk() && super.canBeCollidedWith(); ++ } ++ // Purpur End ++ + public ServerStatsCounter getStats() { + return this.stats; + } +@@ -2498,8 +2615,16 @@ public class ServerPlayer extends Player { + + @Override + public boolean isImmobile() { +- return super.isImmobile() || (this.connection != null && this.connection.isDisconnected()); // Paper ++ return super.isImmobile() || frozen || (this.connection != null && this.connection.isDisconnected()); // Paper // Purpur ++ } ++ ++ // Purpur start ++ private boolean frozen = false; ++ ++ public void setFrozen(boolean frozen) { ++ this.frozen = frozen; + } ++ // Purpur end + + @Override + public Scoreboard getScoreboard() { +@@ -2548,4 +2673,50 @@ public class ServerPlayer extends Player { + return (CraftPlayer) super.getBukkitEntity(); + } + // CraftBukkit end ++ ++ // Purpur start ++ public void teleport(Location to) { ++ this.ejectPassengers(); ++ this.stopRiding(true); ++ ++ if (this.isSleeping()) { ++ this.stopSleepInBed(true, false); ++ } ++ ++ if (this.containerMenu != this.inventoryMenu) { ++ this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.TELEPORT); ++ } ++ ++ ServerLevel toLevel = ((CraftWorld) to.getWorld()).getHandle(); ++ if (this.level == toLevel) { ++ this.connection.internalTeleport(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch(), java.util.EnumSet.noneOf(net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket.RelativeArgument.class), true); ++ } else { ++ this.server.getPlayerList().respawn(this, toLevel, true, to, !toLevel.paperConfig().environment.disableTeleportationSuffocationCheck); ++ } ++ } ++ ++ public boolean ramBar() { ++ return this.ramBar; ++ } ++ ++ public void ramBar(boolean ramBar) { ++ this.ramBar = ramBar; ++ } ++ ++ public boolean tpsBar() { ++ return this.tpsBar; ++ } ++ ++ public void tpsBar(boolean tpsBar) { ++ this.tpsBar = tpsBar; ++ } ++ ++ public boolean compassBar() { ++ return this.compassBar; ++ } ++ ++ public void compassBar(boolean compassBar) { ++ this.compassBar = compassBar; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index 58b093bb1de78ee3b3b2ea364aa50474883f443a..744c936c3aa9bd7bcf43ac3d78a08ece9cde87c3 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -389,6 +389,7 @@ public class ServerPlayerGameMode { + } else {capturedBlockEntity = true;} // Paper end + return false; + } ++ if (this.player.level.purpurConfig.slabHalfBreak && this.player.isShiftKeyDown() && iblockdata.getBlock() instanceof net.minecraft.world.level.block.SlabBlock && ((net.minecraft.world.level.block.SlabBlock) iblockdata.getBlock()).halfBreak(iblockdata, pos, this.player)) return true; // Purpur + } + // CraftBukkit end + +@@ -419,7 +420,7 @@ public class ServerPlayerGameMode { + + ItemStack mainHandStack = null; // Paper + boolean isCorrectTool = false; // Paper +- if (this.isCreative()) { ++ if (this.isCreative() || (this.level.purpurConfig.shulkerBoxAllowOversizedStacks && block instanceof net.minecraft.world.level.block.ShulkerBoxBlock)) { // Purpur + // return true; // CraftBukkit + } else { + ItemStack itemstack = this.player.getMainHandItem(); +@@ -481,7 +482,7 @@ public class ServerPlayerGameMode { + player.setItemInHand(hand, itemstack1); + } + +- if (this.isCreative()) { ++ if (this.isCreative() && itemstack1 != ItemStack.EMPTY) { // Purpur + itemstack1.setCount(i); + if (itemstack1.isDamageableItem() && itemstack1.getDamageValue() != j) { + itemstack1.setDamageValue(j); +@@ -508,6 +509,7 @@ public class ServerPlayerGameMode { + public InteractionHand interactHand; + public ItemStack interactItemStack; + public InteractionResult useItemOn(ServerPlayer player, Level world, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) { ++ if (shiftClickMended(stack)) return InteractionResult.SUCCESS; // Purpur + BlockPos blockposition = hitResult.getBlockPos(); + BlockState iblockdata = world.getBlockState(blockposition); + InteractionResult enuminteractionresult = InteractionResult.PASS; +@@ -568,7 +570,7 @@ public class ServerPlayerGameMode { + boolean flag1 = player.isSecondaryUseActive() && flag; + ItemStack itemstack1 = stack.copy(); + +- if (!flag1) { ++ if (!flag1 || (player.level.purpurConfig.composterBulkProcess && iblockdata.is(Blocks.COMPOSTER))) { // Purpur + enuminteractionresult = iblockdata.use(world, player, hand, hitResult); + + if (enuminteractionresult.consumesAction()) { +@@ -604,4 +606,18 @@ public class ServerPlayerGameMode { + public void setLevel(ServerLevel world) { + this.level = world; + } ++ ++ // Purpur start ++ public boolean shiftClickMended(ItemStack itemstack) { ++ if (this.player.level.purpurConfig.shiftRightClickRepairsMendingPoints > 0 && this.player.isShiftKeyDown() && this.player.getBukkitEntity().hasPermission("purpur.mending_shift_click")) { ++ int points = Math.min(this.player.totalExperience, this.player.level.purpurConfig.shiftRightClickRepairsMendingPoints); ++ if (points > 0 && itemstack.isDamaged() && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MENDING, itemstack) > 0) { ++ this.player.giveExperiencePoints(-points); ++ this.player.level.addFreshEntity(new net.minecraft.world.entity.ExperienceOrb(this.player.level, this.player.getX(), this.player.getY(), this.player.getZ(), points, org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN, this.player, this.player)); ++ return true; ++ } ++ } ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 877498729c66de9aa6a27c9148f7494d7895615c..acd7468ee3c86d3456e96e4ec3d7e6a4c612e89d 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -297,6 +297,7 @@ public class WorldGenRegion implements WorldGenLevel { + return true; + } else { + // Paper start ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressSetBlockFarChunk) // Purpur + if (!hasSetFarWarned) { + Util.logAndPauseIfInIde("Detected setBlock in a far chunk [" + i + ", " + j + "], pos: " + pos + ", status: " + this.generatingStatus + (this.currentlyGenerating == null ? "" : ", currently generating: " + (String) this.currentlyGenerating.get())); + hasSetFarWarned = true; +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index c6a1bde35274bdd0e008a6ca5af006a5d7bcd8bb..69463c7ca9995c02968bc2688adbe7131d91aa8b 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -259,6 +259,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private long keepAliveTime = Util.getMillis(); + private boolean keepAlivePending; + private long keepAliveChallenge; ++ private it.unimi.dsi.fastutil.longs.LongList keepAlives = new it.unimi.dsi.fastutil.longs.LongArrayList(); // Purpur + // CraftBukkit start - multithreaded fields + private final AtomicInteger chatSpamTickCount = new AtomicInteger(); + private final java.util.concurrent.atomic.AtomicInteger tabSpamLimiter = new java.util.concurrent.atomic.AtomicInteger(); // Paper - configurable tab spam limits +@@ -335,6 +336,20 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private boolean justTeleported = false; + private boolean hasMoved; // Spigot + ++ // Purpur start ++ private final com.google.common.cache.LoadingCache kickPermissionCache = com.google.common.cache.CacheBuilder.newBuilder() ++ .maximumSize(1000) ++ .expireAfterWrite(1, java.util.concurrent.TimeUnit.MINUTES) ++ .build( ++ new com.google.common.cache.CacheLoader<>() { ++ @Override ++ public Boolean load(CraftPlayer player) { ++ return player.hasPermission("purpur.bypassIdleKick"); ++ } ++ } ++ ); ++ // Purpur end ++ + public CraftPlayer getCraftPlayer() { + return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity(); + } +@@ -390,12 +405,27 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.aboveGroundVehicleTickCount = 0; + } + +- this.server.getProfiler().push("keepAlive"); ++ //this.server.getProfiler().push("keepAlive"); // Purpur + // Paper Start - give clients a longer time to respond to pings as per pre 1.12.2 timings + // This should effectively place the keepalive handling back to "as it was" before 1.12.2 + long currentTime = Util.getMillis(); + long elapsedTime = currentTime - this.keepAliveTime; + ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useAlternateKeepAlive) { ++ if (elapsedTime >= 1000L) { // 1 second ++ if (!processedDisconnect && keepAlives.size() * 1000L >= KEEPALIVE_LIMIT) { ++ LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); ++ disconnect(Component.translatable("disconnect.timeout"), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); ++ } else { ++ keepAliveTime = currentTime; // hijack this field for 1 second intervals ++ keepAlives.add(currentTime); // currentTime is ID ++ send(new ClientboundKeepAlivePacket(currentTime)); ++ } ++ } ++ } else ++ // Purpur end ++ + if (this.keepAlivePending) { + if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected + ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info +@@ -411,7 +441,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + // Paper end + +- this.server.getProfiler().pop(); ++ //this.server.getProfiler().pop(); // Purpur + // CraftBukkit start + for (int spam; (spam = this.chatSpamTickCount.get()) > 0 && !this.chatSpamTickCount.compareAndSet(spam, spam - 1); ) ; + if (tabSpamLimiter.get() > 0) tabSpamLimiter.getAndDecrement(); // Paper - split to seperate variable +@@ -428,6 +458,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + + if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) (this.server.getPlayerIdleTimeout() * 1000 * 60) && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits. ++ // Purpur start ++ this.player.setAfk(true); ++ if (!this.player.level.purpurConfig.idleTimeoutKick || kickPermissionCache.getUnchecked(this.player.getBukkitEntity())) { ++ return; ++ } ++ // Purpur end + this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 + this.disconnect(Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause + } +@@ -716,7 +752,6 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + to.setY(packet.getY()); + to.setZ(packet.getZ()); + +- + // If the packet contains look information then we update the To location with the correct Yaw & Pitch. + to.setYaw(packet.getYRot()); + to.setPitch(packet.getXRot()); +@@ -732,6 +767,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur ++ + // Skip the first time we do this + if (true) { // Spigot - don't skip any move events + Location oldTo = to.clone(); +@@ -808,6 +845,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + if (packet.getId() == this.awaitingTeleport) { + if (this.awaitingPositionFromClient == null) { + this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause ++ ServerGamePacketListenerImpl.LOGGER.warn("Disconnected on accept teleport packet. Was not expecting position data from client at this time"); // Purpur + return; + } + +@@ -1212,10 +1250,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + int maxBookPageSize = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax; + double multiplier = Math.max(0.3D, Math.min(1D, io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.totalMultiplier)); + long byteAllowed = maxBookPageSize; ++ ItemStack itemstack = this.player.getInventory().getItem(packet.getSlot()); // Purpur + for (String testString : pageList) { + int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + if (byteLength > 256 * 4) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); ++ org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent event = new org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent(player.getBukkitEntity(), itemstack.asBukkitCopy()); if (event.shouldKickPlayer()) // Purpur + server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + return; + } +@@ -1239,6 +1279,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + if (byteTotal > byteAllowed) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); ++ org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent event = new org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent(player.getBukkitEntity(), itemstack.asBukkitCopy()); if (event.shouldKickPlayer()) // Purpur + server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + return; + } +@@ -1292,13 +1333,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + itemstack1.setTag(nbttagcompound.copy()); + } + ++ // Purpur start ++ boolean hasPerm = getCraftPlayer().hasPermission("purpur.book.color.edit") || getCraftPlayer().hasPermission("purpur.book.color.sign"); + itemstack1.addTagElement("author", StringTag.valueOf(this.player.getName().getString())); + if (this.player.isTextFilteringEnabled()) { +- itemstack1.addTagElement("title", StringTag.valueOf(title.filteredOrEmpty())); ++ itemstack1.addTagElement("title", StringTag.valueOf(color(title.filteredOrEmpty(), hasPerm))); + } else { +- itemstack1.addTagElement("filtered_title", StringTag.valueOf(title.filteredOrEmpty())); +- itemstack1.addTagElement("title", StringTag.valueOf(title.raw())); ++ itemstack1.addTagElement("filtered_title", StringTag.valueOf(color(title.filteredOrEmpty(), hasPerm))); ++ itemstack1.addTagElement("title", StringTag.valueOf(color(title.raw(), hasPerm))); + } ++ // Purpur end + + this.updateBookPages(pages, (s) -> { + return Component.Serializer.toJson(Component.literal(s)); +@@ -1310,10 +1354,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private void updateBookPages(List list, UnaryOperator unaryoperator, ItemStack itemstack, int slot, ItemStack handItem) { // CraftBukkit + ListTag nbttaglist = new ListTag(); + ++ // Purpur start ++ boolean hasPerm = getCraftPlayer().hasPermission("purpur.book.color.edit"); + if (this.player.isTextFilteringEnabled()) { +- Stream stream = list.stream().map((filteredtext) -> { // CraftBukkit - decompile error +- return StringTag.valueOf((String) unaryoperator.apply(filteredtext.filteredOrEmpty())); ++ Stream stream = list.stream().map(s -> color(s.filteredOrEmpty(), hasPerm, false)).map((s) -> { // CraftBukkit - decompile error ++ return StringTag.valueOf((String) unaryoperator.apply(s)); + }); ++ // Purpur end + + Objects.requireNonNull(nbttaglist); + stream.forEach(nbttaglist::add); +@@ -1323,11 +1370,11 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + for (int j = list.size(); i < j; ++i) { + FilteredText filteredtext = (FilteredText) list.get(i); +- String s = filteredtext.raw(); ++ String s = color(filteredtext.raw(), hasPerm, false); // Purpur + + nbttaglist.add(StringTag.valueOf((String) unaryoperator.apply(s))); + if (filteredtext.isFiltered()) { +- nbttagcompound.putString(String.valueOf(i), (String) unaryoperator.apply(filteredtext.filteredOrEmpty())); ++ nbttagcompound.putString(String.valueOf(i), (String) unaryoperator.apply((String) color(filteredtext.filteredOrEmpty(), hasPerm, false))); // Purpur + } + } + +@@ -1340,6 +1387,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.player.getInventory().setItem(slot, CraftEventFactory.handleEditBookEvent(player, slot, handItem, itemstack)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent) + } + ++ // Purpur start ++ private String color(String str, boolean hasPerm) { ++ return color(str, hasPerm, true); ++ } ++ ++ private String color(String str, boolean hasPerm, boolean parseHex) { ++ return hasPerm ? org.bukkit.ChatColor.color(str, parseHex) : str; ++ } ++ // Purpur end ++ + @Override + public void handleEntityTagQuery(ServerboundEntityTagQuery packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); +@@ -1369,8 +1426,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleMovePlayer(ServerboundMovePlayerPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); +- if (ServerGamePacketListenerImpl.containsInvalidValues(packet.getX(0.0D), packet.getY(0.0D), packet.getZ(0.0D), packet.getYRot(0.0F), packet.getXRot(0.0F))) { ++ // Purpur start ++ boolean invalidX = Double.isNaN(packet.getX(0.0D)); ++ boolean invalidY = Double.isNaN(packet.getY(0.0D)); ++ boolean invalidZ = Double.isNaN(packet.getZ(0.0D)); ++ boolean invalidYaw = !Floats.isFinite(packet.getYRot(0.0F)); ++ boolean invalidPitch = !Floats.isFinite(packet.getXRot(0.0F)); ++ if (invalidX || invalidY || invalidZ || invalidYaw || invalidPitch) { + this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause ++ ServerGamePacketListenerImpl.LOGGER.warn(String.format("Disconnected on move player packet. Invalid data: x=%b, y=%b, z=%b, yaw=%b, pitch=%b", invalidX, invalidY, invalidZ, invalidYaw, invalidPitch)); ++ // Purpur end + } else { + ServerLevel worldserver = this.player.getLevel(); + +@@ -1536,7 +1601,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot + flag2 = true; // Paper - diff on change, this should be moved wrongly +- ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); ++ ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur + } + + this.player.absMoveTo(d0, d1, d2, f, f1); +@@ -1587,6 +1652,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur ++ + // Skip the first time we do this + if (from.getX() != Double.MAX_VALUE) { + Location oldTo = to.clone(); +@@ -1626,6 +1693,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.player.resetFallDistance(); + } + ++ // Purpur Start ++ if (this.player.level.purpurConfig.dontRunWithScissors && this.player.isSprinting() && (isScissor(this.player.getItemInHand(InteractionHand.MAIN_HAND)) || isScissor(this.player.getItemInHand(InteractionHand.OFF_HAND))) && (int) (Math.random() * 10) == 0) { ++ this.player.hurt(net.minecraft.world.damagesource.DamageSource.SCISSORS, (float) this.player.level.purpurConfig.scissorsRunningDamage); ++ if (!org.purpurmc.purpur.PurpurConfig.dontRunWithScissors.isBlank()) this.player.sendActionBarMessage(org.purpurmc.purpur.PurpurConfig.dontRunWithScissors); ++ } ++ // Purpur End ++ + this.player.checkMovementStatistics(this.player.getX() - d3, this.player.getY() - d4, this.player.getZ() - d5); + this.lastGoodX = this.player.getX(); + this.lastGoodY = this.player.getY(); +@@ -1659,6 +1733,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + // Paper end - optimise out extra getCubes + ++ // Purpur start ++ public boolean isScissor(ItemStack stack) { ++ return stack.is(Items.SHEARS) && (stack.getTag() == null || stack.getTag().getInt("CustomModelData") == 0); ++ } ++ // Purpur end ++ + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) { + Iterable iterable = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D)); + VoxelShape voxelshape = Shapes.create(box.deflate(9.999999747378752E-6D)); +@@ -2015,6 +2095,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + boolean cancelled; + if (movingobjectposition == null || movingobjectposition.getType() != HitResult.Type.BLOCK) { ++ if (this.player.gameMode.shiftClickMended(itemstack)) return; // Purpur + org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemstack, enumhand); + cancelled = event.useItemInHand() == Event.Result.DENY; + } else { +@@ -2068,12 +2149,21 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleResourcePackResponse(ServerboundResourcePackPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); ++ // Purpur start ++ if (player.level.purpurConfig.playerInvulnerableWhileAcceptingResourcePack && !this.player.acceptingResourcePack) { ++ ServerGamePacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack packet exploitation attempt", this.player.getName()); ++ this.disconnect(Component.translatable("multiplayer.texturePrompt.failure.line1"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // "Server resource pack couldn't be applied" ++ return; ++ } ++ // Purpur end + if (packet.getAction() == ServerboundResourcePackPacket.Action.DECLINED && this.server.isResourcePackRequired()) { + ServerGamePacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack rejection", this.player.getGameProfile().getName()); // Paper - Don't print component in resource pack rejection message + this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // Paper - add cause + } + // Paper start + PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action.ordinal()]; ++ if (player.level.purpurConfig.playerInvulnerableWhileAcceptingResourcePack) player.setFrozen(packStatus == PlayerResourcePackStatusEvent.Status.ACCEPTED); // Purpur ++ this.player.acceptingResourcePack = packStatus == PlayerResourcePackStatusEvent.Status.ACCEPTED; // Purpur + player.getBukkitEntity().setResourcePackStatus(packStatus); + this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packStatus)); // CraftBukkit + // Paper end +@@ -2359,7 +2449,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + do { + instant1 = (Instant) this.lastChatTimeStamp.get(); + if (timestamp.isBefore(instant1)) { +- return false; ++ return !org.purpurmc.purpur.PurpurConfig.kickForOutOfOrderChat; // Purpur + } + } while (!this.lastChatTimeStamp.compareAndSet(instant1, timestamp)); + +@@ -2496,7 +2586,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + } + // Paper End +- co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper // Purpur + if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot + this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s); + +@@ -2506,7 +2596,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.cserver.getPluginManager().callEvent(event); + + if (event.isCancelled()) { +- co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper // Purpur + return; + } + +@@ -2519,7 +2609,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + return; + } finally { +- co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper // Purpur + } + } + // CraftBukkit end +@@ -2765,6 +2855,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + + if (entity.distanceToSqr(this.player.getEyePosition()) < ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) { ++ if (entity instanceof Mob mob) mob.ticksSinceLastInteraction = 0; // Purpur + packet.dispatch(new ServerboundInteractPacket.Handler() { + private void performInteraction(InteractionHand enumhand, ServerGamePacketListenerImpl.EntityInteraction playerconnection_a, PlayerInteractEntityEvent event) { // CraftBukkit + ItemStack itemstack = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand); +@@ -2778,6 +2869,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event); + ++ player.processClick(enumhand); // Purpur ++ + // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a + if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { + entity.getEntityData().resendPossiblyDesyncedEntity(player); // Paper - The entire mob gets deleted, so resend it. +@@ -3435,11 +3528,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + for (int i = 0; i < signText.size(); ++i) { + FilteredText filteredtext = (FilteredText) signText.get(i); + +- if (this.player.isTextFilteringEnabled()) { +- lines.add(net.kyori.adventure.text.Component.text(SharedConstants.filterText(filteredtext.filteredOrEmpty()))); // Paper - adventure ++ // Purpur start ++ String line = SharedConstants.filterText(this.player.isTextFilteringEnabled() ? filteredtext.filteredOrEmpty() : filteredtext.raw()); ++ if (worldserver.purpurConfig.signAllowColors) { ++ if (player.hasPermission("purpur.sign.color")) line = line.replaceAll("(?i)&([0-9a-fr])", "\u00a7$1"); ++ if (player.hasPermission("purpur.sign.style")) line = line.replaceAll("(?i)&([l-or])", "\u00a7$1"); ++ if (player.hasPermission("purpur.sign.magic")) line = line.replaceAll("(?i)&([kr])", "\u00a7$1"); ++ lines.add(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(line)); + } else { +- lines.add(net.kyori.adventure.text.Component.text(SharedConstants.filterText(filteredtext.raw()))); // Paper - adventure ++ lines.add(net.kyori.adventure.text.Component.text(line)); + } ++ // Purpur end + } + SignChangeEvent event = new SignChangeEvent((org.bukkit.craftbukkit.block.CraftBlock) player.getWorld().getBlockAt(x, y, z), this.player.getBukkitEntity(), lines); + this.cserver.getPluginManager().callEvent(event); +@@ -3461,6 +3560,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + @Override + public void handleKeepAlive(ServerboundKeepAlivePacket packet) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useAlternateKeepAlive) { ++ long id = packet.getId(); ++ if (keepAlives.size() > 0 && keepAlives.contains(id)) { ++ int ping = (int) (Util.getMillis() - id); ++ player.latency = (player.latency * 3 + ping) / 4; ++ keepAlives.clear(); // we got a valid response, lets roll with it and forget the rest ++ } ++ } else ++ // Purpur end + //PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // CraftBukkit // Paper - This shouldn't be on the main thread + if (this.keepAlivePending && packet.getId() == this.keepAliveChallenge) { + int i = (int) (Util.getMillis() - this.keepAliveTime); +@@ -3511,6 +3620,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private static final ResourceLocation CUSTOM_UNREGISTER = new ResourceLocation("unregister"); + + private static final ResourceLocation MINECRAFT_BRAND = new ResourceLocation("brand"); // Paper - Brand support ++ private static final ResourceLocation PURPUR_CLIENT = new ResourceLocation("purpur", "client"); // Purpur + + @Override + public void handleCustomPayload(ServerboundCustomPayloadPacket packet) { +@@ -3535,6 +3645,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t unregister custom payload", ex); + this.disconnect("Invalid payload UNREGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause + } ++ // Purpur start ++ } else if (packet.identifier.equals(PURPUR_CLIENT)) { ++ try { ++ player.purpurClient = true; ++ } catch (Exception ignore) { ++ } ++ // Purpur end + } else { + try { + byte[] data = new byte[packet.data.readableBytes()]; +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index a25306fe8a35bb70a490e6a0c01d0340bbc0d781..cd718c7a9f95d003014ea28642b375297872e5f9 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -220,6 +220,8 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + return false; + } + ++ if (true) return org.purpurmc.purpur.PurpurConfig.usernameValidCharactersPattern.matcher(in).matches(); // Purpur ++ + for (int i = 0, len = in.length(); i < len; ++i) { + char c = in.charAt(i); + +@@ -339,7 +341,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + ServerLoginPacketListenerImpl.this.gameProfile = gameprofile; + ServerLoginPacketListenerImpl.this.state = ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT; + } else { +- ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.unverified_username")); ++ ServerLoginPacketListenerImpl.this.disconnect(org.purpurmc.purpur.PurpurConfig.unverifiedUsername.equals("default") ? Component.translatable("multiplayer.disconnect.unverified_username") : io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.unverifiedUsername))); // Purpur + ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", gameprofile.getName()); + } + } catch (AuthenticationUnavailableException authenticationunavailableexception) { +diff --git a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +index 0725c39d9cbec3282f93975a0ae76f060f70d86d..b1afe7d9fff390cc9668ce9bbb408d64147553e6 100644 +--- a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +@@ -152,6 +152,7 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene + this.connection.send(new ClientboundStatusResponsePacket(ping)); + // CraftBukkit end + */ ++ if (this.server.getStatus().getVersion() == null) return; // Purpur - do not respond to pings before we know the protocol version + com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(this.server, this.connection); + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java b/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java +index 9ddbfcf80d9a381dace78a62880f85a4d767e0eb..7383c7d3820dce06108eaafd236a7c6c06a10a42 100644 +--- a/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java ++++ b/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java +@@ -9,11 +9,11 @@ public interface ResourceManagerReloadListener extends PreparableReloadListener + @Override + default CompletableFuture reload(PreparableReloadListener.PreparationBarrier synchronizer, ResourceManager manager, ProfilerFiller prepareProfiler, ProfilerFiller applyProfiler, Executor prepareExecutor, Executor applyExecutor) { + return synchronizer.wait(Unit.INSTANCE).thenRunAsync(() -> { +- applyProfiler.startTick(); +- applyProfiler.push("listener"); ++ //applyProfiler.startTick(); // Purpur ++ //applyProfiler.push("listener"); // Purpur + this.onResourceManagerReload(manager); +- applyProfiler.pop(); +- applyProfiler.endTick(); ++ //applyProfiler.pop(); // Purpur ++ //applyProfiler.endTick(); // Purpur + }, applyExecutor); + } + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 835e439a1af327c67558653ef79ef7e59692a976..82093c413722e280b9fcb46fb0ba03ca629e54fa 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -455,6 +455,7 @@ public abstract class PlayerList { + scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); + } + // Paper end ++ org.purpurmc.purpur.task.BossBarTask.addToAll(player); // Purpur + // CraftBukkit - Moved from above, added world + PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ()); + } +@@ -564,6 +565,8 @@ public abstract class PlayerList { + } + public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) { + // Paper end ++ org.purpurmc.purpur.task.BossBarTask.removeFromAll(entityplayer.getBukkitEntity()); // Purpur ++ + ServerLevel worldserver = entityplayer.getLevel(); + + entityplayer.awardStat(Stats.LEAVE_GAME); +@@ -717,7 +720,7 @@ public abstract class PlayerList { + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + } else { + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { ++ if (this.players.size() >= this.maxPlayers && !(player.hasPermission("purpur.joinfullserver") || this.canBypassPlayerLimit(gameprofile))) { // Purpur + event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +@@ -955,6 +958,8 @@ public abstract class PlayerList { + } + // Paper end + ++ entityplayer1.spawnInvulnerableTime = entityplayer1.level.purpurConfig.playerSpawnInvulnerableTicks; // Purpur ++ + // CraftBukkit end + return entityplayer1; + } +@@ -1015,6 +1020,20 @@ public abstract class PlayerList { + } + // CraftBukkit end + ++ // Purpur Start ++ public void broadcastMiniMessage(@Nullable String message, boolean overlay) { ++ if (message != null && !message.isEmpty()) { ++ this.broadcastMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message), overlay); ++ } ++ } ++ ++ public void broadcastMessage(@Nullable net.kyori.adventure.text.Component message, boolean overlay) { ++ if (message != null) { ++ this.broadcastSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), overlay); ++ } ++ } ++ // Purpur end ++ + public void broadcastAll(Packet packet, ResourceKey dimension) { + Iterator iterator = this.players.iterator(); + +@@ -1118,6 +1137,7 @@ public abstract class PlayerList { + } else { + b0 = (byte) (24 + permissionLevel); + } ++ if (b0 < 28 && player.getBukkitEntity().hasPermission("purpur.debug.f3n")) b0 = 28; // Purpur + + player.connection.send(new ClientboundEntityEventPacket(player, b0)); + } +@@ -1126,6 +1146,27 @@ public abstract class PlayerList { + player.getBukkitEntity().recalculatePermissions(); // CraftBukkit + this.server.getCommands().sendCommands(player); + } // Paper ++ ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.enderChestSixRows && org.purpurmc.purpur.PurpurConfig.enderChestPermissionRows) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity bukkit = player.getBukkitEntity(); ++ if (bukkit.hasPermission("purpur.enderchest.rows.six")) { ++ player.sixRowEnderchestSlotCount = 54; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.five")) { ++ player.sixRowEnderchestSlotCount = 45; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.four")) { ++ player.sixRowEnderchestSlotCount = 36; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.three")) { ++ player.sixRowEnderchestSlotCount = 27; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.two")) { ++ player.sixRowEnderchestSlotCount = 18; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.one")) { ++ player.sixRowEnderchestSlotCount = 9; ++ } ++ } else { ++ player.sixRowEnderchestSlotCount = -1; ++ } ++ //Purpur end + } + + public boolean isWhiteListed(GameProfile profile) { +@@ -1187,7 +1228,7 @@ public abstract class PlayerList { + + public void saveAll(int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main +- MinecraftTimings.savePlayers.startTiming(); // Paper ++ //MinecraftTimings.savePlayers.startTiming(); // Paper // Purpur + int numSaved = 0; + long now = MinecraftServer.currentTick; + for (int i = 0; i < this.players.size(); ++i) { +@@ -1198,7 +1239,7 @@ public abstract class PlayerList { + } + // Paper end + } +- MinecraftTimings.savePlayers.stopTiming(); // Paper ++ //MinecraftTimings.savePlayers.stopTiming(); // Paper // Purpur + return null; }); // Paper - ensure main + } + +diff --git a/src/main/java/net/minecraft/server/players/SleepStatus.java b/src/main/java/net/minecraft/server/players/SleepStatus.java +index 823efad652d8ff9e96b99375b102fef6f017716e..60f89d7c77a5e792e21e93e35ed1670bd565799a 100644 +--- a/src/main/java/net/minecraft/server/players/SleepStatus.java ++++ b/src/main/java/net/minecraft/server/players/SleepStatus.java +@@ -19,7 +19,7 @@ public class SleepStatus { + + public boolean areEnoughDeepSleeping(int percentage, List players) { + // CraftBukkit start +- int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping; }).count(); ++ int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping || (eh.level.purpurConfig.idleTimeoutCountAsSleeping && eh.isAfk()); }).count(); // Purpur + boolean anyDeepSleep = players.stream().anyMatch(Player::isSleepingLongEnough); + + return anyDeepSleep && j >= this.sleepersNeeded(percentage); +@@ -52,7 +52,7 @@ public class SleepStatus { + + if (!entityplayer.isSpectator()) { + ++this.activePlayers; +- if (entityplayer.isSleeping() || entityplayer.fauxSleeping) { // CraftBukkit ++ if ((entityplayer.isSleeping() || entityplayer.fauxSleeping) || (entityplayer.level.purpurConfig.idleTimeoutCountAsSleeping && entityplayer.isAfk())) { // CraftBukkit // Purpur + ++this.sleepingPlayers; + } + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/stats/ServerRecipeBook.java b/src/main/java/net/minecraft/stats/ServerRecipeBook.java +index d13ed3069e944d138442ea440ac3eaf8d44c18d3..29ac7f202aa23f7e6fcdc9829af3d59875c92d4e 100644 +--- a/src/main/java/net/minecraft/stats/ServerRecipeBook.java ++++ b/src/main/java/net/minecraft/stats/ServerRecipeBook.java +@@ -122,6 +122,7 @@ public class ServerRecipeBook extends RecipeBook { + Optional> optional = recipeManager.byKey(minecraftkey); + + if (!optional.isPresent()) { ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressUnrecognizedRecipeErrors) // Purpur + ServerRecipeBook.LOGGER.error("Tried to load unrecognized recipe: {} removed now.", minecraftkey); + } else { + handler.accept((Recipe) optional.get()); +diff --git a/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java b/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java +index 6d96da16f25e2359e053c45270310886e168f828..de024b88e7328c25748f59288fb7ff575fce1fdc 100644 +--- a/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java ++++ b/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java +@@ -55,7 +55,7 @@ public class ActiveProfiler implements ProfileCollector { + this.started = true; + this.path = ""; + this.paths.clear(); +- this.push("root"); ++ //this.push("root"); // Purpur + } + } + +@@ -64,7 +64,7 @@ public class ActiveProfiler implements ProfileCollector { + if (!this.started) { + LOGGER.error("Profiler tick already ended - missing startTick()?"); + } else { +- this.pop(); ++ //this.pop(); // Purpur + this.started = false; + if (!this.path.isEmpty()) { + LOGGER.error("Profiler tick ended before path was fully popped (remainder: '{}'). Mismatched push/pop?", LogUtils.defer(() -> { +@@ -93,7 +93,7 @@ public class ActiveProfiler implements ProfileCollector { + + @Override + public void push(Supplier locationGetter) { +- this.push(locationGetter.get()); ++ //this.push(locationGetter.get()); // Purpur + } + + @Override +@@ -132,14 +132,14 @@ public class ActiveProfiler implements ProfileCollector { + + @Override + public void popPush(String location) { +- this.pop(); +- this.push(location); ++ //this.pop(); // Purpur ++ //this.push(location); // Purpur + } + + @Override + public void popPush(Supplier locationGetter) { +- this.pop(); +- this.push(locationGetter); ++ //this.pop(); // Purpur ++ //this.push(locationGetter); // Purpur + } + + private ActiveProfiler.PathEntry getCurrentEntry() { +diff --git a/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java b/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java +index 5725c6593480fada65facc29664a00a8cc073512..ccb1f998ae3122d1856d77149ff7e7dffeedc71a 100644 +--- a/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java ++++ b/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java +@@ -6,32 +6,44 @@ import net.minecraft.util.profiling.metrics.MetricCategory; + public interface ProfilerFiller { + String ROOT = "root"; + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void startTick(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void endTick(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void push(String location); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void push(Supplier locationGetter); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void pop(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void popPush(String location); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void popPush(Supplier locationGetter); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void markForCharting(MetricCategory type); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + default void incrementCounter(String marker) { +- this.incrementCounter(marker, 1); ++ //this.incrementCounter(marker, 1); // Purpur + } + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void incrementCounter(String marker, int i); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + default void incrementCounter(Supplier markerGetter) { +- this.incrementCounter(markerGetter, 1); ++ //this.incrementCounter(markerGetter, 1); // Purpur + } + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void incrementCounter(Supplier markerGetter, int i); + + static ProfilerFiller tee(final ProfilerFiller a, final ProfilerFiller b) { +@@ -41,62 +53,62 @@ public interface ProfilerFiller { + return b == InactiveProfiler.INSTANCE ? a : new ProfilerFiller() { + @Override + public void startTick() { +- a.startTick(); +- b.startTick(); ++ //a.startTick(); // Purpur ++ //b.startTick(); // Purpur + } + + @Override + public void endTick() { +- a.endTick(); +- b.endTick(); ++ //a.endTick(); // Purpur ++ //b.endTick(); // Purpur + } + + @Override + public void push(String location) { +- a.push(location); +- b.push(location); ++ //a.push(location); // Purpur ++ //b.push(location); // Purpur + } + + @Override + public void push(Supplier locationGetter) { +- a.push(locationGetter); +- b.push(locationGetter); ++ //a.push(locationGetter); // Purpur ++ //b.push(locationGetter); // Purpur + } + + @Override + public void markForCharting(MetricCategory type) { +- a.markForCharting(type); +- b.markForCharting(type); ++ //a.markForCharting(type); // Purpur ++ //b.markForCharting(type); // Purpur + } + + @Override + public void pop() { +- a.pop(); +- b.pop(); ++ //a.pop(); // Purpur ++ //b.pop(); // Purpur + } + + @Override + public void popPush(String location) { +- a.popPush(location); +- b.popPush(location); ++ //a.popPush(location); // Purpur ++ //b.popPush(location); // Purpur + } + + @Override + public void popPush(Supplier locationGetter) { +- a.popPush(locationGetter); +- b.popPush(locationGetter); ++ //a.popPush(locationGetter); // Purpur ++ //b.popPush(locationGetter); // Purpur + } + + @Override + public void incrementCounter(String marker, int i) { +- a.incrementCounter(marker, i); +- b.incrementCounter(marker, i); ++ //a.incrementCounter(marker, i); // Purpur ++ //b.incrementCounter(marker, i); // Purpur + } + + @Override + public void incrementCounter(Supplier markerGetter, int i) { +- a.incrementCounter(markerGetter, i); +- b.incrementCounter(markerGetter, i); ++ //a.incrementCounter(markerGetter, i); // Purpur ++ //b.incrementCounter(markerGetter, i); // Purpur + } + }; + } +diff --git a/src/main/java/net/minecraft/world/damagesource/CombatRules.java b/src/main/java/net/minecraft/world/damagesource/CombatRules.java +index ccbfcef3e83b1bef364447657bfd08a92d615cf6..aa2331c6df4e79d4bb0add071a0b11d2a3a08b88 100644 +--- a/src/main/java/net/minecraft/world/damagesource/CombatRules.java ++++ b/src/main/java/net/minecraft/world/damagesource/CombatRules.java +@@ -11,12 +11,12 @@ public class CombatRules { + + public static float getDamageAfterAbsorb(float damage, float armor, float armorToughness) { + float f = 2.0F + armorToughness / 4.0F; +- float g = Mth.clamp(armor - damage / f, armor * 0.2F, 20.0F); ++ float g = Mth.clamp(armor - damage / f, armor * 0.2F, org.purpurmc.purpur.PurpurConfig.limitArmor ? 20F : Float.MAX_VALUE); // Purpur + return damage * (1.0F - g / 25.0F); + } + + public static float getDamageAfterMagicAbsorb(float damageDealt, float protection) { +- float f = Mth.clamp(protection, 0.0F, 20.0F); ++ float f = Mth.clamp(protection, 0.0F, org.purpurmc.purpur.PurpurConfig.limitArmor ? 20F : Float.MAX_VALUE); // Purpur + return damageDealt * (1.0F - f / 25.0F); + } + } +diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +index 2848cb7c76e94d8349f042dc92daf01322a6ce5a..2e1c34d37b4371fcd7f5616ad5cd5876014ebc9d 100644 +--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java ++++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +@@ -35,6 +35,20 @@ public class DamageSource { + public static final DamageSource SWEET_BERRY_BUSH = new DamageSource("sweetBerryBush"); + public static final DamageSource FREEZE = (new DamageSource("freeze")).bypassArmor(); + public static final DamageSource STALAGMITE = (new DamageSource("stalagmite")).bypassArmor().setIsFall(); ++ // Purpur start ++ public static final DamageSource SCISSORS = (new DamageSource("scissors") { ++ @Override ++ public Component getLocalizedDeathMessage(LivingEntity entity) { ++ return getLocalizedDeathMessage(org.purpurmc.purpur.PurpurConfig.deathMsgRunWithScissors, entity); ++ } ++ }).bypassArmor(); ++ public static final DamageSource STONECUTTER = (new DamageSource("stonecutter") { ++ @Override ++ public Component getLocalizedDeathMessage(LivingEntity entity) { ++ return getLocalizedDeathMessage(org.purpurmc.purpur.PurpurConfig.deathMsgStonecutter, entity); ++ } ++ }).bypassArmor(); ++ // Purpur end + private boolean damageHelmet; + private boolean bypassArmor; + private boolean bypassInvul; +@@ -265,6 +279,15 @@ public class DamageSource { + return entityliving1 != null ? Component.translatable(s1, entity.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, entity.getDisplayName()); + } + ++ // Purpur start ++ public Component getLocalizedDeathMessage(String str, LivingEntity entity) { ++ net.kyori.adventure.text.Component name = io.papermc.paper.adventure.PaperAdventure.asAdventure(entity.getDisplayName()); ++ net.kyori.adventure.text.minimessage.tag.resolver.TagResolver template = net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("player", name); ++ net.kyori.adventure.text.Component component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(str, template); ++ return io.papermc.paper.adventure.PaperAdventure.asVanilla(component); ++ } ++ // Purpur end ++ + public boolean isFire() { + return this.isFireSource; + } +diff --git a/src/main/java/net/minecraft/world/effect/MobEffect.java b/src/main/java/net/minecraft/world/effect/MobEffect.java +index e708b2c987fac150c22b3367cec2e3e2bcb9914c..434f229c9e67c4ca459ab612345a1754d8be9780 100644 +--- a/src/main/java/net/minecraft/world/effect/MobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/MobEffect.java +@@ -61,16 +61,16 @@ public class MobEffect { + public void applyEffectTick(LivingEntity entity, int amplifier) { + if (this == MobEffects.REGENERATION) { + if (entity.getHealth() < entity.getMaxHealth()) { +- entity.heal(1.0F, RegainReason.MAGIC_REGEN); // CraftBukkit ++ entity.heal(entity.level.purpurConfig.entityHealthRegenAmount, RegainReason.MAGIC_REGEN); // CraftBukkit // Purpur + } + } else if (this == MobEffects.POISON) { +- if (entity.getHealth() > 1.0F) { +- entity.hurt(CraftEventFactory.POISON, 1.0F); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON ++ if (entity.getHealth() > entity.level.purpurConfig.entityMinimalHealthPoison) { // Purpur ++ entity.hurt(CraftEventFactory.POISON, entity.level.purpurConfig.entityPoisonDegenerationAmount); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON // Purpur + } + } else if (this == MobEffects.WITHER) { +- entity.hurt(DamageSource.WITHER, 1.0F); ++ entity.hurt(DamageSource.WITHER, entity.level.purpurConfig.entityWitherDegenerationAmount); // Purpur + } else if (this == MobEffects.HUNGER && entity instanceof Player) { +- ((Player) entity).causeFoodExhaustion(0.005F * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent ++ ((Player) entity).causeFoodExhaustion(entity.level.purpurConfig.humanHungerExhaustionAmount * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent // Purpur + } else if (this == MobEffects.SATURATION && entity instanceof Player) { + if (!entity.level.isClientSide) { + // CraftBukkit start +@@ -80,7 +80,7 @@ public class MobEffect { + org.bukkit.event.entity.FoodLevelChangeEvent event = CraftEventFactory.callFoodLevelChangeEvent(entityhuman, amplifier + 1 + oldFoodLevel); + + if (!event.isCancelled()) { +- entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, 1.0F); ++ entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, entity.level.purpurConfig.humanSaturationRegenAmount); // Purpur + } + + ((ServerPlayer) entityhuman).connection.send(new ClientboundSetHealthPacket(((ServerPlayer) entityhuman).getBukkitEntity().getScaledHealth(), entityhuman.getFoodData().foodLevel, entityhuman.getFoodData().saturationLevel)); +diff --git a/src/main/java/net/minecraft/world/effect/MobEffectInstance.java b/src/main/java/net/minecraft/world/effect/MobEffectInstance.java +index 038ba61e4845a4a71bb78ba388ed249d19529b78..6d5080ba244daf3b93d61d28ee0b88eb56bac723 100644 +--- a/src/main/java/net/minecraft/world/effect/MobEffectInstance.java ++++ b/src/main/java/net/minecraft/world/effect/MobEffectInstance.java +@@ -13,6 +13,7 @@ import net.minecraft.util.ExtraCodecs; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.LivingEntity; + import org.slf4j.Logger; ++import org.bukkit.NamespacedKey; + + public class MobEffectInstance implements Comparable { + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -23,6 +24,7 @@ public class MobEffectInstance implements Comparable { + private boolean visible; + private boolean showIcon; + @Nullable ++ private NamespacedKey key; // Purpur - add key + private MobEffectInstance hiddenEffect; + private final Optional factorData; + +@@ -42,17 +44,36 @@ public class MobEffectInstance implements Comparable { + this(type, duration, amplifier, ambient, visible, visible); + } + ++ // Purpur start ++ public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean visible, @Nullable NamespacedKey key) { ++ this(type, duration, amplifier, ambient, visible, visible, key); ++ } ++ // Purpur end ++ + public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon) { +- this(type, duration, amplifier, ambient, showParticles, showIcon, (MobEffectInstance)null, type.createFactorData()); ++ // Purpur start ++ this(type, duration, amplifier, ambient, showParticles, showIcon, (MobEffectInstance)null, type.createFactorData(), (NamespacedKey)null); ++ } ++ ++ public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon, @Nullable NamespacedKey key) { ++ this(type, duration, amplifier, ambient, showParticles, showIcon, (MobEffectInstance)null, type.createFactorData(), key); ++ // Purpur end + } + + public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon, @Nullable MobEffectInstance hiddenEffect, Optional factorCalculationData) { ++ // Purpur start ++ this(type, duration, amplifier, ambient, showParticles, showIcon, hiddenEffect, factorCalculationData, (NamespacedKey) null); ++ } ++ ++ public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon, @Nullable MobEffectInstance hiddenEffect, Optional factorCalculationData, @Nullable NamespacedKey key) { ++ // Purpur end + this.effect = type; + this.duration = duration; + this.amplifier = amplifier; + this.ambient = ambient; + this.visible = showParticles; + this.showIcon = showIcon; ++ this.key = key; // Purpur - add key + this.hiddenEffect = hiddenEffect; + this.factorData = factorCalculationData; + } +@@ -73,6 +94,7 @@ public class MobEffectInstance implements Comparable { + this.ambient = that.ambient; + this.visible = that.visible; + this.showIcon = that.showIcon; ++ this.key = that.key; // Purpur - add key + } + + public boolean update(MobEffectInstance that) { +@@ -125,6 +147,13 @@ public class MobEffectInstance implements Comparable { + bl = true; + } + ++ // Purpur start ++ if (that.key != this.key) { ++ this.key = that.key; ++ bl = true; ++ } ++ // Purpur end ++ + return bl; + } + +@@ -152,6 +181,17 @@ public class MobEffectInstance implements Comparable { + return this.showIcon; + } + ++ // Purpur start ++ public boolean hasKey() { ++ return this.key != null; ++ } ++ ++ @Nullable ++ public NamespacedKey getKey() { ++ return this.key; ++ } ++ // Purpur end ++ + public boolean tick(LivingEntity entity, Runnable overwriteCallback) { + if (this.duration > 0) { + if (this.effect.isDurationEffectTick(this.duration, this.amplifier)) { +@@ -208,6 +248,12 @@ public class MobEffectInstance implements Comparable { + string = string + ", Show Icon: false"; + } + ++ // Purpur start ++ if (this.hasKey()) { ++ string = string + ", Key: " + this.key; ++ } ++ // Purpur end ++ + return string; + } + +@@ -219,7 +265,7 @@ public class MobEffectInstance implements Comparable { + return false; + } else { + MobEffectInstance mobEffectInstance = (MobEffectInstance)object; +- return this.duration == mobEffectInstance.duration && this.amplifier == mobEffectInstance.amplifier && this.ambient == mobEffectInstance.ambient && this.effect.equals(mobEffectInstance.effect); ++ return this.duration == mobEffectInstance.duration && this.amplifier == mobEffectInstance.amplifier && this.ambient == mobEffectInstance.ambient && this.effect.equals(mobEffectInstance.effect) && this.key == mobEffectInstance.key; // Purpur - add key + } + } + +@@ -243,6 +289,11 @@ public class MobEffectInstance implements Comparable { + nbt.putBoolean("Ambient", this.isAmbient()); + nbt.putBoolean("ShowParticles", this.isVisible()); + nbt.putBoolean("ShowIcon", this.showIcon()); ++ // Purpur start ++ if (this.key != null) { ++ nbt.putString("Key", this.key.toString()); ++ } ++ // Purpur end + if (this.hiddenEffect != null) { + CompoundTag compoundTag = new CompoundTag(); + this.hiddenEffect.save(compoundTag); +@@ -277,6 +328,13 @@ public class MobEffectInstance implements Comparable { + bl3 = nbt.getBoolean("ShowIcon"); + } + ++ // Purpur start ++ NamespacedKey key = null; ++ if (nbt.contains("Key")) { ++ key = NamespacedKey.fromString(nbt.getString("Key")); ++ } ++ // Purpur end ++ + MobEffectInstance mobEffectInstance = null; + if (nbt.contains("HiddenEffect", 10)) { + mobEffectInstance = loadSpecifiedEffect(type, nbt.getCompound("HiddenEffect")); +@@ -289,7 +347,7 @@ public class MobEffectInstance implements Comparable { + optional = Optional.empty(); + } + +- return new MobEffectInstance(type, j, Math.max(i, 0), bl, bl2, bl3, mobEffectInstance, optional); ++ return new MobEffectInstance(type, j, Math.max(i, 0), bl, bl2, bl3, mobEffectInstance, optional, key); // Purpur - add key + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 3073b34a0e0281b6b0330721bb0440147de28511..1ae7ef3d94d4d3d8413a5419eeb9fadad89d2f8d 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -155,7 +155,7 @@ import org.bukkit.plugin.PluginManager; + // CraftBukkit end + + public abstract class Entity implements Nameable, EntityAccess, CommandSource { +- ++ public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; + public boolean preserveMotion = true; // Paper - keep initial motion on first setPositionRotation +@@ -361,7 +361,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + private final Set tags; + private final double[] pistonDeltas; + private long pistonDeltasGameTime; +- private EntityDimensions dimensions; ++ protected EntityDimensions dimensions; // Purpur - private -> protected + private float eyeHeight; + public boolean isInPowderSnow; + public boolean wasInPowderSnow; +@@ -398,6 +398,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + private UUID originWorld; + public boolean freezeLocked = false; // Paper - Freeze Tick Lock API + public boolean collidingWithWorldBorder; // Paper ++ public @Nullable Boolean immuneToFire = null; // Purpur - Fire immune API + + public void setOrigin(@javax.annotation.Nonnull Location location) { + this.origin = location.toVector(); +@@ -419,7 +420,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + public int activatedPriority = gg.pufferfish.pufferfish.PufferfishConfig.maximumActivationPrio; // golf score + public final BlockPos.MutableBlockPos cachedBlockPos = new BlockPos.MutableBlockPos(); // used where needed + // Pufferfish end +- ++ + public float getBukkitYaw() { + return this.yRot; + } +@@ -577,7 +578,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.bb = Entity.INITIAL_AABB; + this.stuckSpeedMultiplier = Vec3.ZERO; + this.nextStep = 1.0F; +- this.random = SHARED_RANDOM; // Paper ++ this.random = world == null || world.purpurConfig.entitySharedRandom ? SHARED_RANDOM : RandomSource.create(); // Paper // Purpur + this.remainingFireTicks = -this.getFireImmuneTicks(); + this.fluidHeight = new Object2DoubleArrayMap(2); + this.fluidOnEyes = new HashSet(); +@@ -822,7 +823,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return; + } + // Pufferfish end - entity TTL +- this.level.getProfiler().push("entityBaseTick"); ++ //this.level.getProfiler().push("entityBaseTick"); // Purpur + if (firstTick && this instanceof net.minecraft.world.entity.NeutralMob neutralMob) neutralMob.tickInitialPersistentAnger(level); // Paper - Update last hurt when ticking + this.feetBlockState = null; + if (this.isPassenger() && this.getVehicle().isRemoved()) { +@@ -883,7 +884,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + this.firstTick = false; +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + public void setSharedFlagOnFire(boolean onFire) { +@@ -892,10 +893,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + public void checkOutOfWorld() { + // Paper start - Configurable nether ceiling damage +- if (this.getY() < (double) (this.level.getMinBuildHeight() - 64) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER ++ if (this.getY() < (double) (this.level.getMinBuildHeight() + level.purpurConfig.voidDamageHeight) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER // Purpur + && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v) + && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) { + // Paper end ++ if (this.level.purpurConfig.teleportOnNetherCeilingDamage && this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER && this instanceof ServerPlayer player) player.teleport(MCUtil.toLocation(this.level, this.level.getSharedSpawnPos())); else // Purpur + this.outOfWorld(); + } + +@@ -1057,7 +1059,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + } + +- this.level.getProfiler().push("move"); ++ //this.level.getProfiler().push("move"); // Purpur + if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) { + movement = movement.multiply(this.stuckSpeedMultiplier); + this.stuckSpeedMultiplier = Vec3.ZERO; +@@ -1066,7 +1068,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // Paper start - ignore movement changes while inactive. + if (isTemporarilyActive && !(this instanceof ItemEntity || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart) && movement == getDeltaMovement() && movementType == MoverType.SELF) { + setDeltaMovement(Vec3.ZERO); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + return; + } + // Paper end +@@ -1087,8 +1089,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.setPos(this.getX() + vec3d1.x, this.getY() + vec3d1.y, this.getZ() + vec3d1.z); + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("rest"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("rest"); // Purpur + boolean flag = !Mth.equal(movement.x, vec3d1.x); + boolean flag1 = !Mth.equal(movement.z, vec3d1.z); + +@@ -1107,7 +1109,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + this.checkFallDamage(vec3d1.y, this.onGround, iblockdata, blockposition); + if (this.isRemoved()) { +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } else { + if (this.horizontalCollision) { + Vec3 vec3d2 = this.getDeltaMovement(); +@@ -1248,7 +1250,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.setRemainingFireTicks(-this.getFireImmuneTicks()); + } + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + } + // Paper start - detailed watchdog information +@@ -1675,7 +1677,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public boolean fireImmune() { +- return this.getType().fireImmune(); ++ return this.immuneToFire != null ? immuneToFire : this.getType().fireImmune(); // Purpur - add fire immune API + } + + public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { +@@ -1740,7 +1742,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return this.isInWater() || flag; + } + +- void updateInWaterStateAndDoWaterCurrentPushing() { ++ public void updateInWaterStateAndDoWaterCurrentPushing() { // Purpur - package-private -> public + Entity entity = this.getVehicle(); + + if (entity instanceof Boat) { +@@ -2326,6 +2328,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + nbt.putBoolean("Paper.FreezeLock", true); + } + // Paper end ++ // Purpur start ++ if (immuneToFire != null) { ++ nbt.putBoolean("Purpur.FireImmune", immuneToFire); ++ } ++ // Purpur end + return nbt; + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); +@@ -2493,6 +2500,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + freezeLocked = nbt.getBoolean("Paper.FreezeLock"); + } + // Paper end ++ // Purpur start ++ if (nbt.contains("Purpur.FireImmune")) { ++ immuneToFire = nbt.getBoolean("Purpur.FireImmune"); ++ } ++ // Purpur end + + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); +@@ -2811,6 +2823,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.passengers = ImmutableList.copyOf(list); + } + ++ // Purpur start ++ if (isRidable() && this.passengers.get(0) == entity && entity instanceof Player player) { ++ onMount(player); ++ this.rider = player; ++ } ++ // Purpur end + } + return true; // CraftBukkit + } +@@ -2851,6 +2869,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return false; + } + // Spigot end ++ ++ // Purpur start ++ if (this.rider != null && this.passengers.get(0) == this.rider) { ++ onDismount(this.rider); ++ this.rider = null; ++ } ++ // Purpur end ++ + if (this.passengers.size() == 1 && this.passengers.get(0) == entity) { + this.passengers = ImmutableList.of(); + } else { +@@ -2905,12 +2931,15 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return Vec3.directionFromRotation(this.getRotationVector()); + } + ++ public BlockPos portalPos = BlockPos.ZERO; // Purpur + public void handleInsidePortal(BlockPos pos) { + if (this.isOnPortalCooldown()) { ++ if (!(level.purpurConfig.playerFixStuckPortal && this instanceof Player && !pos.equals(portalPos))) // Purpur + this.setPortalCooldown(); +- } else { ++ } else if (level.purpurConfig.entitiesCanUsePortals || this instanceof ServerPlayer) { // Purpur + if (!this.level.isClientSide && !pos.equals(this.portalEntrancePos)) { + this.portalEntrancePos = pos.immutable(); ++ portalPos = BlockPos.ZERO; // Purpur + } + + this.isInsidePortal = true; +@@ -2928,7 +2957,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + ServerLevel worldserver1 = minecraftserver.getLevel(resourcekey); + + if (true && !this.isPassenger() && this.portalTime++ >= i) { // CraftBukkit +- this.level.getProfiler().push("portal"); ++ //this.level.getProfiler().push("portal"); // Purpur + this.portalTime = i; + // Paper start + io.papermc.paper.event.entity.EntityPortalReadyEvent event = new io.papermc.paper.event.entity.EntityPortalReadyEvent(this.getBukkitEntity(), worldserver1 == null ? null : worldserver1.getWorld(), org.bukkit.PortalType.NETHER); +@@ -2946,7 +2975,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + } // Paper + // CraftBukkit end +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + this.isInsidePortal = false; +@@ -2961,7 +2990,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + this.processPortalCooldown(); +- this.tickEndPortal(); // Paper - make end portalling safe ++ if (this.level.purpurConfig.endPortalSafeTeleporting) this.tickEndPortal(); // Paper - make end portalling safe // Purpur + } + } + +@@ -3141,7 +3170,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public int getMaxAirSupply() { +- return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() ++ return this.level == null? this.maxAirTicks : this.level.purpurConfig.drowningAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() // Purpur + } + + public int getAirSupply() { +@@ -3411,14 +3440,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + // Paper end + if (this.level instanceof ServerLevel && !this.isRemoved()) { +- this.level.getProfiler().push("changeDimension"); ++ //this.level.getProfiler().push("changeDimension"); // Purpur + // CraftBukkit start + // this.decouple(); + if (worldserver == null) { + return null; + } + // CraftBukkit end +- this.level.getProfiler().push("reposition"); ++ //this.level.getProfiler().push("reposition"); // Purpur + PortalInfo shapedetectorshape = (location == null) ? this.findDimensionEntryPoint(worldserver) : new PortalInfo(new Vec3(location.x(), location.y(), location.z()), Vec3.ZERO, this.yRot, this.xRot, worldserver, null); // CraftBukkit + + if (shapedetectorshape == null) { +@@ -3452,7 +3481,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.unRide(); + // CraftBukkit end + +- this.level.getProfiler().popPush("reloading"); ++ //this.level.getProfiler().popPush("reloading"); // Purpur + // Paper start - Change lead drop timing to prevent dupe + if (this instanceof Mob) { + ((Mob) this).dropLeash(true, true); // Paper drop lead +@@ -3475,10 +3504,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + this.removeAfterChangingDimensions(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + ((ServerLevel) this.level).resetEmptyTime(); + worldserver.resetEmptyTime(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + return entity; + } + } else { +@@ -3598,7 +3627,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public boolean canChangeDimensions() { +- return isAlive() && valid; // Paper ++ return isAlive() && valid && (level.purpurConfig.entitiesCanUsePortals || this instanceof ServerPlayer); // Paper // Purpur + } + + public float getBlockExplosionResistance(Explosion explosion, BlockGetter world, BlockPos pos, BlockState blockState, FluidState fluidState, float max) { +@@ -3863,6 +3892,20 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return SlotAccess.NULL; + } + ++ // Purpur Start ++ public void sendMiniMessage(@Nullable String message) { ++ if (message != null && !message.isEmpty()) { ++ this.sendMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message)); ++ } ++ } ++ ++ public void sendMessage(@Nullable net.kyori.adventure.text.Component message) { ++ if (message != null) { ++ this.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ } ++ // Purpur end ++ + @Override + public void sendSystemMessage(Component message) {} + +@@ -4125,6 +4168,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.yRotO = this.getYRot(); + } + ++ // Purpur start ++ public AABB getAxisForFluidCheck() { ++ return this.getBoundingBox().deflate(0.001D); ++ } ++ // Purpur end ++ + public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { + if (false && this.touchingUnloadedChunk()) { // Pufferfish - cost of a lookup here is the same cost as below, so skip + return false; +@@ -4637,4 +4686,64 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return ((net.minecraft.server.level.ServerChunkCache) level.getChunkSource()).isPositionTicking(this); + } + // Paper end ++ ++ // Purpur start ++ @Nullable ++ private Player rider = null; ++ ++ @Nullable ++ public Player getRider() { ++ return rider; ++ } ++ ++ public boolean isRidable() { ++ return false; ++ } ++ ++ public boolean isControllable() { ++ return true; ++ } ++ ++ public void onMount(Player rider) { ++ if (this instanceof Mob) { ++ ((Mob) this).setTarget(null, null, false); ++ ((Mob) this).getNavigation().stop(); ++ } ++ rider.setJumping(false); // fixes jump on mount ++ } ++ ++ public void onDismount(Player player) { ++ } ++ ++ public boolean onSpacebar() { ++ return false; ++ } ++ ++ public boolean onClick(InteractionHand hand) { ++ return false; ++ } ++ ++ public boolean processClick(InteractionHand hand) { ++ return false; ++ } ++ ++ public boolean canSaveToDisk() { ++ return true; ++ } ++ ++ // Purpur start - copied from Mob ++ public boolean isSunBurnTick() { ++ if (this.level.isDay() && !this.level.isClientSide) { ++ float f = this.getLightLevelDependentMagicValue(); ++ BlockPos blockposition = new BlockPos(this.getX(), this.getEyeY(), this.getZ()); ++ boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; ++ ++ if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level.canSeeSky(blockposition)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java +index 72abebff2018cde2922e97ad6478f93da9aed3ec..412963d7af38a53b6010007278d959a5b11b83c3 100644 +--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java ++++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java +@@ -39,6 +39,7 @@ public final class EntitySelector { + return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks; + }; + // Paper end ++ public static Predicate notAfk = (player) -> !player.isAfk(); // Purpur + + private EntitySelector() {} + // Paper start +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index ac7ee31f2bfe5d4139b793a698317db50b39fe40..f22ceffdc95224f2de8d19f501b43b266de196d4 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -301,13 +301,24 @@ public class EntityType implements FeatureElement, EntityTypeT + private Component description; + @Nullable + private ResourceLocation lootTable; +- private final EntityDimensions dimensions; ++ private EntityDimensions dimensions; // Purpur - remove final ++ public void setDimensions(EntityDimensions dimensions) { this.dimensions = dimensions; } // Purpur + private final FeatureFlagSet requiredFeatures; + + private static EntityType register(String id, EntityType.Builder type) { // CraftBukkit - decompile error + return (EntityType) Registry.register(BuiltInRegistries.ENTITY_TYPE, id, (EntityType) type.build(id)); // CraftBukkit - decompile error + } + ++ // Purpur start ++ public static EntityType getFromBukkitType(org.bukkit.entity.EntityType bukkitType) { ++ return getFromKey(new ResourceLocation(bukkitType.getKey().toString())); ++ } ++ ++ public static EntityType getFromKey(ResourceLocation location) { ++ return BuiltInRegistries.ENTITY_TYPE.get(location); ++ } ++ // Purpur end ++ + public static ResourceLocation getKey(EntityType type) { + return BuiltInRegistries.ENTITY_TYPE.getKey(type); + } +@@ -522,6 +533,16 @@ public class EntityType implements FeatureElement, EntityTypeT + return this.category; + } + ++ // Purpur start ++ public String getName() { ++ return BuiltInRegistries.ENTITY_TYPE.getKey(this).getPath(); ++ } ++ ++ public String getTranslatedName() { ++ return getDescription().getString(); ++ } ++ // Purpur end ++ + public String getDescriptionId() { + if (this.descriptionId == null) { + this.descriptionId = Util.makeDescriptionId("entity", BuiltInRegistries.ENTITY_TYPE.getKey(this)); +@@ -583,6 +604,12 @@ public class EntityType implements FeatureElement, EntityTypeT + entity.load(nbt); + }, () -> { + EntityType.LOGGER.warn("Skipping Entity with id {}", nbt.getString("id")); ++ // Purpur start - log skipped entity's position ++ try { ++ ListTag pos = nbt.getList("Pos", 6); ++ EntityType.LOGGER.warn("Location: {} {},{},{}", world.getWorld().getName(), pos.getDouble(0), pos.getDouble(1), pos.getDouble(2)); ++ } catch (Throwable ignore) {} ++ // Purpur end + }); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +index cf5c7e8557b0084039a94ef881a36aa9e3f58daf..a1d271c931cf35f6a73191a1c21933ab0203bf7d 100644 +--- a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java ++++ b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +@@ -304,8 +304,8 @@ public class ExperienceOrb extends Entity { + @Override + public void playerTouch(Player player) { + if (!this.level.isClientSide) { +- if (player.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper +- player.takeXpDelay = 2; ++ if (player.takeXpDelay <= 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper ++ player.takeXpDelay = this.level.purpurConfig.playerExpPickupDelay; // Purpur + player.take(this, 1); + int i = this.repairPlayerItems(player, this.value); + +@@ -323,7 +323,7 @@ public class ExperienceOrb extends Entity { + } + + private int repairPlayerItems(Player player, int amount) { +- Entry entry = EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged); ++ Entry entry = level.purpurConfig.useBetterMending ? EnchantmentHelper.getMostDamagedEquipment(Enchantments.MENDING, player) : EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged); // Purpur + + if (entry != null) { + ItemStack itemstack = (ItemStack) entry.getValue(); +diff --git a/src/main/java/net/minecraft/world/entity/GlowSquid.java b/src/main/java/net/minecraft/world/entity/GlowSquid.java +index c1e9b40a4a0f9cdc650caa88b5ea132e06ee2496..5d6cddc221887be20ef75d688817dfe527e73362 100644 +--- a/src/main/java/net/minecraft/world/entity/GlowSquid.java ++++ b/src/main/java/net/minecraft/world/entity/GlowSquid.java +@@ -18,11 +18,49 @@ import net.minecraft.world.level.block.Blocks; + + public class GlowSquid extends Squid { + private static final EntityDataAccessor DATA_DARK_TICKS_REMAINING = SynchedEntityData.defineId(GlowSquid.class, EntityDataSerializers.INT); ++ private static final net.minecraft.network.syncher.EntityDataAccessor SQUID_COLOR = net.minecraft.network.syncher.SynchedEntityData.defineId(GlowSquid.class, net.minecraft.network.syncher.EntityDataSerializers.STRING); // Purpur + + public GlowSquid(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.glowSquidRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.glowSquidControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.glowSquidMaxHealth); ++ } ++ ++ @Override ++ public boolean canFly() { ++ return this.level.purpurConfig.glowSquidsCanFly; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.glowSquidTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.glowSquidAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected ParticleOptions getInkParticle() { + return ParticleTypes.GLOW_SQUID_INK; +@@ -32,6 +70,7 @@ public class GlowSquid extends Squid { + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(DATA_DARK_TICKS_REMAINING, 0); ++ this.entityData.define(SQUID_COLOR, this.level.purpurConfig.glowSquidColorMode.getRandom(this.random).toString()); // Purpur + } + + @Override +@@ -58,12 +97,14 @@ public class GlowSquid extends Squid { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("DarkTicksRemaining", this.getDarkTicksRemaining()); ++ nbt.putString("Colour", this.entityData.get(SQUID_COLOR)); // Purpur - key must match rainglow + } + + @Override + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.setDarkTicks(nbt.getInt("DarkTicksRemaining")); ++ if (nbt.contains("Colour")) this.entityData.set(SQUID_COLOR, nbt.getString("Colour")); // Purpur - key must match rainglow + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 413652284e229a51e3eefe47f239e8fa9a09ccb2..b2e4b5c463ceb19356da18e7fc52d20801b674cd 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -221,9 +221,9 @@ public abstract class LivingEntity extends Entity { + protected int deathScore; + public float lastHurt; + public boolean jumping; +- public float xxa; +- public float yya; +- public float zza; ++ public float xxa; public float getStrafeMot() { return xxa; } public void setStrafeMot(float strafe) { xxa = strafe; } // Purpur - OBFHELPER ++ public float yya; public float getVerticalMot() { return yya; } public void setVerticalMot(float vertical) { yya = vertical; } // Purpur - OBFHELPER ++ public float zza; public float getForwardMot() { return zza; } public void setForwardMot(float forward) { zza = forward; } // Purpur - OBFHELPER + protected int lerpSteps; + protected double lerpX; + protected double lerpY; +@@ -256,6 +256,7 @@ public abstract class LivingEntity extends Entity { + private boolean skipDropExperience; + // CraftBukkit start + public int expToDrop; ++ public float safeFallDistance = 3.0F; // Purpur + public boolean forceDrops; + public ArrayList drops = new ArrayList(); + public final org.bukkit.craftbukkit.attribute.CraftAttributeMap craftAttributes; +@@ -265,6 +266,7 @@ public abstract class LivingEntity extends Entity { + public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper + public boolean silentDeath = false; // Paper - mark entity as dying silently for cancellable death event + public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper ++ protected boolean shouldBurnInDay = false; public boolean shouldBurnInDay() { return this.shouldBurnInDay; } public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } // Purpur + + @Override + public float getBukkitYaw() { +@@ -289,7 +291,8 @@ public abstract class LivingEntity extends Entity { + this.effectsDirty = true; + this.useItem = ItemStack.EMPTY; + this.lastClimbablePos = Optional.empty(); +- this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type)); ++ this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type), this); // Purpur ++ this.initAttributes(); // Purpur + this.craftAttributes = new CraftAttributeMap(this.attributes); // CraftBukkit + // CraftBukkit - setHealth(getMaxHealth()) inlined and simplified to skip the instanceof check for EntityPlayer, as getBukkitEntity() is not initialized in constructor + this.entityData.set(LivingEntity.DATA_HEALTH_ID, (float) this.getAttribute(Attributes.MAX_HEALTH).getValue()); +@@ -305,6 +308,8 @@ public abstract class LivingEntity extends Entity { + this.brain = this.makeBrain(new Dynamic(dynamicopsnbt, (Tag) dynamicopsnbt.createMap((Map) ImmutableMap.of(dynamicopsnbt.createString("memories"), (Tag) dynamicopsnbt.emptyMap())))); + } + ++ protected void initAttributes() {}// Purpur ++ + public Brain getBrain() { + return this.brain; + } +@@ -340,6 +345,7 @@ public abstract class LivingEntity extends Entity { + public static AttributeSupplier.Builder createLivingAttributes() { + return AttributeSupplier.builder().add(Attributes.MAX_HEALTH).add(Attributes.KNOCKBACK_RESISTANCE).add(Attributes.MOVEMENT_SPEED).add(Attributes.ARMOR).add(Attributes.ARMOR_TOUGHNESS); + } ++ public boolean shouldSendAttribute(Attribute attribute) { return true; } // Purpur + + @Override + protected void checkFallDamage(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) { +@@ -352,8 +358,8 @@ public abstract class LivingEntity extends Entity { + this.tryAddSoulSpeed(); + } + +- if (!this.level.isClientSide && this.fallDistance > 3.0F && onGround) { +- float f = (float) Mth.ceil(this.fallDistance - 3.0F); ++ if (!this.level.isClientSide && this.fallDistance > this.safeFallDistance && onGround) { // Purpur ++ float f = (float) Mth.ceil(this.fallDistance - this.safeFallDistance); // Purpur + + if (!state.isAir()) { + double d1 = Math.min((double) (0.2F + f / 15.0F), 2.5D); +@@ -392,7 +398,7 @@ public abstract class LivingEntity extends Entity { + } + + super.baseTick(); +- this.level.getProfiler().push("livingEntityBaseTick"); ++ //this.level.getProfiler().push("livingEntityBaseTick"); // Purpur + if (this.fireImmune() || this.level.isClientSide) { + this.clearFire(); + } +@@ -410,6 +416,7 @@ public abstract class LivingEntity extends Entity { + double d1 = this.level.getWorldBorder().getDamagePerBlock(); + + if (d1 > 0.0D) { ++ if (level.purpurConfig.teleportIfOutsideBorder && this instanceof ServerPlayer) { ((ServerPlayer) this).teleport(io.papermc.paper.util.MCUtil.toLocation(level, ((ServerLevel) level).getSharedSpawnPos())); return; } // Purpur + this.hurt(DamageSource.IN_WALL, (float) Math.max(1, Mth.floor(-d0 * d1))); + } + } +@@ -421,7 +428,7 @@ public abstract class LivingEntity extends Entity { + + if (flag1) { + this.setAirSupply(this.decreaseAirSupply(this.getAirSupply())); +- if (this.getAirSupply() == -20) { ++ if (this.getAirSupply() == -this.level.purpurConfig.drowningDamageInterval) { // Purpur + this.setAirSupply(0); + Vec3 vec3d = this.getDeltaMovement(); + +@@ -433,7 +440,7 @@ public abstract class LivingEntity extends Entity { + this.level.addParticle(ParticleTypes.BUBBLE, this.getX() + d2, this.getY() + d3, this.getZ() + d4, vec3d.x, vec3d.y, vec3d.z); + } + +- this.hurt(DamageSource.DROWN, 2.0F); ++ this.hurt(DamageSource.DROWN, (float) this.level.purpurConfig.damageFromDrowning); // Purpur + } + } + +@@ -494,7 +501,7 @@ public abstract class LivingEntity extends Entity { + this.yHeadRotO = this.yHeadRot; + this.yRotO = this.getYRot(); + this.xRotO = this.getXRot(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + public boolean canSpawnSoulSpeedParticle() { +@@ -787,6 +794,7 @@ public abstract class LivingEntity extends Entity { + dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> { + nbt.put("Brain", nbtbase); + }); ++ nbt.putBoolean("Purpur.ShouldBurnInDay", shouldBurnInDay); // Purpur + } + + @Override +@@ -871,6 +879,11 @@ public abstract class LivingEntity extends Entity { + this.brain = this.makeBrain(new Dynamic(NbtOps.INSTANCE, nbt.get("Brain"))); + } + ++ // Purpur start ++ if (nbt.contains("Purpur.ShouldBurnInDay")) { ++ shouldBurnInDay = nbt.getBoolean("Purpur.ShouldBurnInDay"); ++ } ++ // Purpur end + } + + // CraftBukkit start +@@ -1016,9 +1029,31 @@ public abstract class LivingEntity extends Entity { + ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); + EntityType entitytypes = entity.getType(); + +- if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL) || entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD) || entitytypes == EntityType.PIGLIN && itemstack.is(Items.PIGLIN_HEAD) || entitytypes == EntityType.PIGLIN_BRUTE && itemstack.is(Items.PIGLIN_HEAD) || entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { +- d0 *= 0.5D; ++ // Purpur start ++ if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL)) { ++ d0 *= entity.level.purpurConfig.skeletonHeadVisibilityPercent; ++ } ++ else if (entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD)) { ++ d0 *= entity.level.purpurConfig.zombieHeadVisibilityPercent; ++ } ++ else if (entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { ++ d0 *= entity.level.purpurConfig.creeperHeadVisibilityPercent; ++ } ++ else if ((entitytypes == EntityType.PIGLIN || entitytypes == EntityType.PIGLIN_BRUTE) && itemstack.is(Items.PIGLIN_HEAD)) { ++ d0 *= entity.level.purpurConfig.piglinHeadVisibilityPercent; ++ } ++ // Purpur end ++ ++ // Purpur start ++ if (entity instanceof LivingEntity entityliving) { ++ if (entityliving.hasEffect(MobEffects.BLINDNESS)) { ++ int amplifier = entityliving.getEffect(MobEffects.BLINDNESS).getAmplifier(); ++ for (int i = 0; i < amplifier; i++) { ++ d0 *= this.level.purpurConfig.mobsBlindnessMultiplier; ++ } ++ } + } ++ // Purpur end + } + + return d0; +@@ -1078,6 +1113,7 @@ public abstract class LivingEntity extends Entity { + for (flag = false; iterator.hasNext(); flag = true) { + // CraftBukkit start + MobEffectInstance effect = (MobEffectInstance) iterator.next(); ++ if (cause == EntityPotionEffectEvent.Cause.MILK && !this.level.purpurConfig.milkClearsBeneficialEffects && effect.getEffect().isBeneficial()) continue; // Purpur + EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, null, cause, EntityPotionEffectEvent.Action.CLEARED); + if (event.isCancelled()) { + continue; +@@ -1425,13 +1461,13 @@ public abstract class LivingEntity extends Entity { + } + + if (entity1 instanceof net.minecraft.world.entity.player.Player) { +- this.lastHurtByPlayerTime = 100; ++ this.lastHurtByPlayerTime = this.level.purpurConfig.mobLastHurtByPlayerTime; // Purpur + this.lastHurtByPlayer = (net.minecraft.world.entity.player.Player) entity1; + } else if (entity1 instanceof Wolf) { + Wolf entitywolf = (Wolf) entity1; + + if (entitywolf.isTame()) { +- this.lastHurtByPlayerTime = 100; ++ this.lastHurtByPlayerTime = this.level.purpurConfig.mobLastHurtByPlayerTime; // Purpur + LivingEntity entityliving1 = entitywolf.getOwner(); + + if (entityliving1 != null && entityliving1.getType() == EntityType.PLAYER) { +@@ -1556,6 +1592,18 @@ public abstract class LivingEntity extends Entity { + } + } + ++ // Purpur start ++ if (level.purpurConfig.totemOfUndyingWorksInInventory && this instanceof ServerPlayer player && (itemstack == null || itemstack.getItem() != Items.TOTEM_OF_UNDYING) && player.getBukkitEntity().hasPermission("purpur.inventory_totem")) { ++ for (ItemStack item : player.getInventory().items) { ++ if (item.getItem() == Items.TOTEM_OF_UNDYING) { ++ itemstack1 = item; ++ itemstack = item.cloneItemStack(false); ++ break; ++ } ++ } ++ } ++ // Purpur end ++ + org.bukkit.inventory.EquipmentSlot handSlot = (hand != null) ? org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand) : null; + EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot); + event.setCancelled(itemstack == null); +@@ -1716,7 +1764,7 @@ public abstract class LivingEntity extends Entity { + boolean flag = false; + + if (this.dead && adversary instanceof WitherBoss) { // Paper +- if (this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.level.purpurConfig.witherBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + BlockPos blockposition = this.blockPosition(); + BlockState iblockdata = Blocks.WITHER_ROSE.defaultBlockState(); + +@@ -1762,6 +1810,7 @@ public abstract class LivingEntity extends Entity { + + this.dropEquipment(); // CraftBukkit - from below + if (this.shouldDropLoot() && this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { ++ if (!(source == DamageSource.CRAMMING && level.purpurConfig.disableDropsOnCrammingDeath)) { // Purpur + this.dropFromLootTable(source, flag); + // Paper start + final boolean prev = this.clearEquipmentSlots; +@@ -1770,6 +1819,7 @@ public abstract class LivingEntity extends Entity { + // Paper end + this.dropCustomDeathLoot(source, i, flag); + this.clearEquipmentSlots = prev; // Paper ++ } // Purpur + } + // CraftBukkit start - Call death event // Paper start - call advancement triggers with correct entity equipment + org.bukkit.event.entity.EntityDeathEvent deathEvent = CraftEventFactory.callEntityDeathEvent(this, this.drops, () -> { +@@ -2016,7 +2066,7 @@ public abstract class LivingEntity extends Entity { + MobEffectInstance mobeffect = this.getEffect(MobEffects.JUMP); + float f2 = mobeffect == null ? 0.0F : (float) (mobeffect.getAmplifier() + 1); + +- return Mth.ceil((fallDistance - 3.0F - f2) * damageMultiplier); ++ return Mth.ceil((fallDistance - this.safeFallDistance - f2) * damageMultiplier); // Purpur + } + + protected void playBlockFallSound() { +@@ -2233,6 +2283,20 @@ public abstract class LivingEntity extends Entity { + ((ServerPlayer) damagesource.getEntity()).awardStat(Stats.DAMAGE_DEALT_ABSORBED, Math.round(f2 * 10.0F)); + } + ++ // Purpur start ++ if (damagesource.getEntity() instanceof net.minecraft.world.entity.player.Player player && damagesource.getEntity().level.purpurConfig.creativeOnePunch) { ++ if (player.isCreative()) { ++ double attackDamage = 0; ++ for (AttributeModifier modifier : player.getMainHandItem().getAttributeModifiers(EquipmentSlot.MAINHAND).get(Attributes.ATTACK_DAMAGE)) { ++ attackDamage += modifier.getAmount(); ++ } ++ if (attackDamage == 0) { ++ this.setHealth(0); ++ } ++ } ++ } ++ // Purpur end ++ + if (f > 0 || !human) { + if (human) { + // PAIL: Be sure to drag all this code from the EntityHuman subclass each update. +@@ -2515,7 +2579,7 @@ public abstract class LivingEntity extends Entity { + + @Override + protected void outOfWorld() { +- this.hurt(DamageSource.OUT_OF_WORLD, 4.0F); ++ this.hurt(DamageSource.OUT_OF_WORLD, (float) level.purpurConfig.voidDamageDealt); // Purpur + } + + protected void updateSwingTime() { +@@ -2712,7 +2776,7 @@ public abstract class LivingEntity extends Entity { + } + + protected long lastJumpTime = 0L; // Paper +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + double d0 = (double) this.getJumpPower() + this.getJumpBoostPower(); + Vec3 vec3d = this.getDeltaMovement(); + // Paper start +@@ -2866,6 +2930,7 @@ public abstract class LivingEntity extends Entity { + + if (f3 > 0.0F) { + this.playSound(this.getFallDamageSound((int) f3), 1.0F, 1.0F); ++ if (level.purpurConfig.elytraKineticDamage) // Purpur + this.hurt(DamageSource.FLY_INTO_WALL, f3); + } + } +@@ -3062,10 +3127,10 @@ public abstract class LivingEntity extends Entity { + } + + this.run += (f3 - this.run) * 0.3F; +- this.level.getProfiler().push("headTurn"); ++ //this.level.getProfiler().push("headTurn"); // Purpur + f2 = this.tickHeadTurn(f1, f2); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("rangeChecks"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("rangeChecks"); // Purpur + + // Paper start - stop large pitch and yaw changes from crashing the server + this.yRotO += Math.round((this.getYRot() - this.yRotO) / 360.0F) * 360.0F; +@@ -3077,7 +3142,7 @@ public abstract class LivingEntity extends Entity { + this.yHeadRotO += Math.round((this.yHeadRot - this.yHeadRotO) / 360.0F) * 360.0F; + // Paper end + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.animStep += f2; + if (this.isFallFlying()) { + ++this.fallFlyTicks; +@@ -3374,19 +3439,19 @@ public abstract class LivingEntity extends Entity { + } + + this.setDeltaMovement(d4, d5, d6); +- this.level.getProfiler().push("ai"); ++ //this.level.getProfiler().push("ai"); // Purpur + if (this.isImmobile()) { + this.jumping = false; + this.xxa = 0.0F; + this.zza = 0.0F; + } else if (this.isEffectiveAi()) { +- this.level.getProfiler().push("newAi"); ++ //this.level.getProfiler().push("newAi"); // Purpur + this.serverAiStep(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("jump"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("jump"); // Purpur + if (this.jumping && this.isAffectedByFluids()) { + double d7; + +@@ -3413,8 +3478,8 @@ public abstract class LivingEntity extends Entity { + this.noJumpDelay = 0; + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("travel"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("travel"); // Purpur + this.xxa *= 0.98F; + this.zza *= 0.98F; + this.updateFallFlying(); +@@ -3423,8 +3488,8 @@ public abstract class LivingEntity extends Entity { + // SpigotTimings.timerEntityAIMove.startTiming(); // Spigot // Paper + this.travel(new Vec3((double) this.xxa, (double) this.yya, (double) this.zza)); + // SpigotTimings.timerEntityAIMove.stopTiming(); // Spigot // Paper +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("freezing"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("freezing"); // Purpur + boolean flag1 = this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES); + int i; + +@@ -3444,18 +3509,20 @@ public abstract class LivingEntity extends Entity { + this.hurt(DamageSource.FREEZE, (float) i); + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("push"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("push"); // Purpur + if (this.autoSpinAttackTicks > 0) { + --this.autoSpinAttackTicks; + this.checkAutoSpinAttack(axisalignedbb, this.getBoundingBox()); + } + + this.pushEntities(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + // Paper start +- if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { +- if (this.xo != getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { ++ // Purpur start ++ if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { ++ if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { ++ // Purpur end + Location from = new Location(this.level.getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); + Location to = new Location (this.level.getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); + io.papermc.paper.event.entity.EntityMoveEvent event = new io.papermc.paper.event.entity.EntityMoveEvent(this.getBukkitLivingEntity(), from, to.clone()); +@@ -3465,12 +3532,48 @@ public abstract class LivingEntity extends Entity { + absMoveTo(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), event.getTo().getYaw(), event.getTo().getPitch()); + } + } ++ // Purpur start ++ if (getRider() != null) { ++ getRider().resetLastActionTime(); ++ if (((ServerLevel) level).hasRidableMoveEvent && this instanceof Mob) { ++ Location from = new Location(level.getWorld(), xo, yo, zo, this.yRotO, this.xRotO); ++ Location to = new Location(level.getWorld(), getX(), getY(), getZ(), this.getYRot(), this.getXRot()); ++ org.purpurmc.purpur.event.entity.RidableMoveEvent event = new org.purpurmc.purpur.event.entity.RidableMoveEvent((org.bukkit.entity.Mob) getBukkitLivingEntity(), (Player) getRider().getBukkitEntity(), from, to.clone()); ++ if (!event.callEvent()) { ++ absMoveTo(from.getX(), from.getY(), from.getZ(), from.getYaw(), from.getPitch()); ++ } else if (!to.equals(event.getTo())) { ++ absMoveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); ++ } ++ } ++ } ++ // Purpur end + } + // Paper end + if (!this.level.isClientSide && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) { + this.hurt(DamageSource.DROWN, 1.0F); + } + ++ // Purpur start - copied from Zombie ++ if (this.isAlive()) { ++ boolean flag = this.shouldBurnInDay() && this.isSunBurnTick(); ++ if (flag) { ++ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); ++ if (!itemstack.isEmpty()) { ++ if (itemstack.isDamageableItem()) { ++ itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); ++ if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { ++ this.broadcastBreakEvent(EquipmentSlot.HEAD); ++ this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); ++ } ++ } ++ flag = false; ++ } ++ if (flag) { ++ this.setSecondsOnFire(8); ++ } ++ } ++ } ++ // Purpur end + } + + public boolean isSensitiveToWater() { +@@ -3491,7 +3594,16 @@ public abstract class LivingEntity extends Entity { + int j = i / 10; + + if (j % 2 == 0) { +- itemstack.hurtAndBreak(1, this, (entityliving) -> { ++ // Purpur start ++ int damage = level.purpurConfig.elytraDamagePerSecond; ++ if (level.purpurConfig.elytraDamageMultiplyBySpeed > 0) { ++ double speed = getDeltaMovement().lengthSqr(); ++ if (speed > level.purpurConfig.elytraDamageMultiplyBySpeed) { ++ damage *= (int) speed; ++ } ++ } ++ itemstack.hurtAndBreak(damage, this, (entityliving) -> { ++ // Purpur end + entityliving.broadcastBreakEvent(EquipmentSlot.CHEST); + }); + } +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 94b45579dc371ee980565aed2f5dee78ebd44427..43cc8e8f07adecef21c70954918b8945a7c3ef62 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -65,6 +65,7 @@ import net.minecraft.world.item.ProjectileWeaponItem; + import net.minecraft.world.item.SpawnEggItem; + import net.minecraft.world.item.SwordItem; + import net.minecraft.world.item.enchantment.EnchantmentHelper; ++import net.minecraft.world.item.enchantment.Enchantments; + import net.minecraft.world.level.GameRules; + import net.minecraft.world.level.ItemLike; + import net.minecraft.world.level.Level; +@@ -133,6 +134,7 @@ public abstract class Mob extends LivingEntity { + private BlockPos restrictCenter; + private float restrictRadius; + ++ public int ticksSinceLastInteraction; // Purpur + public boolean aware = true; // CraftBukkit + + protected Mob(EntityType type, Level world) { +@@ -146,8 +148,8 @@ public abstract class Mob extends LivingEntity { + this.restrictRadius = -1.0F; + this.goalSelector = new GoalSelector(world.getProfilerSupplier()); + this.targetSelector = new GoalSelector(world.getProfilerSupplier()); +- this.lookControl = new LookControl(this); +- this.moveControl = new MoveControl(this); ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this); // Purpur ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this); // Purpur + this.jumpControl = new JumpControl(this); + this.bodyRotationControl = this.createBodyControl(); + this.navigation = this.createNavigation(world); +@@ -289,6 +291,7 @@ public abstract class Mob extends LivingEntity { + entityliving = null; + } + } ++ if (entityliving instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + this.target = entityliving; + return true; + // CraftBukkit end +@@ -329,15 +332,35 @@ public abstract class Mob extends LivingEntity { + @Override + public void baseTick() { + super.baseTick(); +- this.level.getProfiler().push("mobBaseTick"); ++ //this.level.getProfiler().push("mobBaseTick"); // Purpur + if (this.isAlive() && this.random.nextInt(1000) < this.ambientSoundTime++) { + this.resetAmbientSoundTime(); + this.playAmbientSound(); + } + +- this.level.getProfiler().pop(); ++ incrementTicksSinceLastInteraction(); // Purpur ++ //this.level.getProfiler().pop(); // Purpur + } + ++ // Purpur start ++ private void incrementTicksSinceLastInteraction() { ++ ++this.ticksSinceLastInteraction; ++ if (getRider() != null) { ++ this.ticksSinceLastInteraction = 0; ++ return; ++ } ++ if (this.level.purpurConfig.entityLifeSpan <= 0) { ++ return; // feature disabled ++ } ++ if (!this.removeWhenFarAway(0) || isPersistenceRequired() || requiresCustomPersistence() || hasCustomName()) { ++ return; // mob persistent ++ } ++ if (this.ticksSinceLastInteraction > this.level.purpurConfig.entityLifeSpan) { ++ this.discard(); ++ } ++ } ++ // Purpur end ++ + @Override + protected void playHurtSound(DamageSource source) { + this.resetAmbientSoundTime(); +@@ -527,6 +550,7 @@ public abstract class Mob extends LivingEntity { + } + + nbt.putBoolean("Bukkit.Aware", this.aware); // CraftBukkit ++ nbt.putInt("Purpur.ticksSinceLastInteraction", this.ticksSinceLastInteraction); // Purpur + } + + @Override +@@ -597,6 +621,11 @@ public abstract class Mob extends LivingEntity { + this.aware = nbt.getBoolean("Bukkit.Aware"); + } + // CraftBukkit end ++ // Purpur start ++ if (nbt.contains("Purpur.ticksSinceLastInteraction")) { ++ this.ticksSinceLastInteraction = nbt.getInt("Purpur.ticksSinceLastInteraction"); ++ } ++ // Purpur end + } + + @Override +@@ -640,8 +669,8 @@ public abstract class Mob extends LivingEntity { + @Override + public void aiStep() { + super.aiStep(); +- this.level.getProfiler().push("looting"); +- if (!this.level.isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ //this.level.getProfiler().push("looting"); // Purpur ++ if (!this.level.isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && (this.level.purpurConfig.entitiesPickUpLootBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { + Vec3i baseblockposition = this.getPickupReach(); + List list = this.level.getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate((double) baseblockposition.getX(), (double) baseblockposition.getY(), (double) baseblockposition.getZ())); + Iterator iterator = list.iterator(); +@@ -660,7 +689,7 @@ public abstract class Mob extends LivingEntity { + } + } + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + protected Vec3i getPickupReach() { +@@ -873,46 +902,46 @@ public abstract class Mob extends LivingEntity { + return; + } + // Paper end +- this.level.getProfiler().push("sensing"); ++ //this.level.getProfiler().push("sensing"); // Purpur + this.sensing.tick(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + int i = this.level.getServer().getTickCount() + this.getId(); + + if (i % 2 != 0 && this.tickCount > 1) { +- this.level.getProfiler().push("targetSelector"); ++ //this.level.getProfiler().push("targetSelector"); // Purpur + if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.targetSelector.tickRunningGoals(false); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("goalSelector"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("goalSelector"); // Purpur + if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.goalSelector.tickRunningGoals(false); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } else { +- this.level.getProfiler().push("targetSelector"); ++ //this.level.getProfiler().push("targetSelector"); // Purpur + if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.targetSelector.tick(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("goalSelector"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("goalSelector"); // Purpur + if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.goalSelector.tick(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + +- this.level.getProfiler().push("navigation"); ++ //this.level.getProfiler().push("navigation"); // Purpur + this.navigation.tick(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("mob tick"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("mob tick"); // Purpur + this.customServerAiStep(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("controls"); +- this.level.getProfiler().push("move"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("controls"); // Purpur ++ //this.level.getProfiler().push("move"); // Purpur + this.moveControl.tick(); +- this.level.getProfiler().popPush("look"); ++ //this.level.getProfiler().popPush("look"); // Purpur + this.lookControl.tick(); +- this.level.getProfiler().popPush("jump"); ++ //this.level.getProfiler().popPush("jump"); // Purpur + this.jumpControl.tick(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().pop(); // Purpur + this.sendDebugPackets(); + } + +@@ -1134,6 +1163,12 @@ public abstract class Mob extends LivingEntity { + + } + ++ // Purpur start ++ public static @Nullable EquipmentSlot getSlotForDispenser(ItemStack itemstack) { ++ return EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BINDING_CURSE, itemstack) > 0 ? null : getEquipmentSlotForItem(itemstack); ++ } ++ // Purpur end ++ + @Nullable + public static Item getEquipmentForSlot(EquipmentSlot equipmentSlot, int equipmentLevel) { + switch (equipmentSlot) { +@@ -1228,7 +1263,7 @@ public abstract class Mob extends LivingEntity { + RandomSource randomsource = world.getRandom(); + + this.getAttribute(Attributes.FOLLOW_RANGE).addPermanentModifier(new AttributeModifier("Random spawn bonus", randomsource.triangle(0.0D, 0.11485000000000001D), AttributeModifier.Operation.MULTIPLY_BASE)); +- if (randomsource.nextFloat() < 0.05F) { ++ if (randomsource.nextFloat() < world.getLevel().purpurConfig.entityLeftHandedChance) { // Purpur + this.setLeftHanded(true); + } else { + this.setLeftHanded(false); +@@ -1276,6 +1311,7 @@ public abstract class Mob extends LivingEntity { + if (!this.isAlive()) { + return InteractionResult.PASS; + } else if (this.getLeashHolder() == player) { ++ if (hand == InteractionHand.OFF_HAND && (level.purpurConfig.villagerCanBeLeashed || level.purpurConfig.wanderingTraderCanBeLeashed) && this instanceof net.minecraft.world.entity.npc.AbstractVillager) return InteractionResult.CONSUME; // Purpur + // CraftBukkit start - fire PlayerUnleashEntityEvent + // Paper start - drop leash variable + org.bukkit.event.player.PlayerUnleashEntityEvent event = CraftEventFactory.callPlayerUnleashEntityEvent(this, player, hand, !player.getAbilities().instabuild); +@@ -1347,7 +1383,7 @@ public abstract class Mob extends LivingEntity { + protected void onOffspringSpawnedFromEgg(Player player, Mob child) {} + + protected InteractionResult mobInteract(Player player, InteractionHand hand) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + public boolean isWithinRestriction() { +@@ -1658,6 +1694,7 @@ public abstract class Mob extends LivingEntity { + this.setLastHurtMob(target); + } + ++ if (target instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + return flag; + } + +@@ -1674,17 +1711,7 @@ public abstract class Mob extends LivingEntity { + } + + public boolean isSunBurnTick() { +- if (this.level.isDay() && !this.level.isClientSide) { +- float f = this.getLightLevelDependentMagicValue(); +- BlockPos blockposition = new BlockPos(this.getX(), this.getEyeY(), this.getZ()); +- boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; +- +- if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level.canSeeSky(blockposition)) { +- return true; +- } +- } +- +- return false; ++ return super.isSunBurnTick(); // Purpur - moved contents to Entity + } + + @Override +@@ -1728,4 +1755,56 @@ public abstract class Mob extends LivingEntity { + + return itemmonsteregg == null ? null : new ItemStack(itemmonsteregg); + } ++ ++ // Purpur start ++ public double getMaxY() { ++ return level.getHeight(); ++ } ++ ++ public InteractionResult tryRide(Player player, InteractionHand hand) { ++ return tryRide(player, hand, InteractionResult.PASS); ++ } ++ ++ public InteractionResult tryRide(Player player, InteractionHand hand, InteractionResult result) { ++ if (!isRidable()) { ++ return result; ++ } ++ if (hand != InteractionHand.MAIN_HAND) { ++ return InteractionResult.PASS; ++ } ++ if (player.isShiftKeyDown()) { ++ return InteractionResult.PASS; ++ } ++ if (!player.getItemInHand(hand).isEmpty()) { ++ return InteractionResult.PASS; ++ } ++ if (!passengers.isEmpty() || player.isPassenger()) { ++ return InteractionResult.PASS; ++ } ++ if (this instanceof TamableAnimal tamable) { ++ if (tamable.isTame() && !tamable.isOwnedBy(player)) { ++ return InteractionResult.PASS; ++ } ++ if (!tamable.isTame() && !level.purpurConfig.untamedTamablesAreRidable) { ++ return InteractionResult.PASS; ++ } ++ } ++ if (this instanceof AgeableMob ageable) { ++ if (ageable.isBaby() && !level.purpurConfig.babiesAreRidable) { ++ return InteractionResult.PASS; ++ } ++ } ++ if (!player.getBukkitEntity().hasPermission("allow.ride." + net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(getType()).getPath())) { ++ player.sendMiniMessage(org.purpurmc.purpur.PurpurConfig.cannotRideMob); ++ return InteractionResult.PASS; ++ } ++ player.setYRot(this.getYRot()); ++ player.setXRot(this.getXRot()); ++ if (player.startRiding(this)) { ++ return InteractionResult.SUCCESS; ++ } else { ++ return InteractionResult.PASS; ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/Shearable.java b/src/main/java/net/minecraft/world/entity/Shearable.java +index 5e8cc5cfac8888628c6d513148f41be09ca65a2c..a089fc61ec09be6b7490375489178dc6ba5a644b 100644 +--- a/src/main/java/net/minecraft/world/entity/Shearable.java ++++ b/src/main/java/net/minecraft/world/entity/Shearable.java +@@ -3,7 +3,13 @@ package net.minecraft.world.entity; + import net.minecraft.sounds.SoundSource; + + public interface Shearable { +- void shear(SoundSource shearedSoundCategory); ++ // Purpur start ++ default void shear(SoundSource shearedSoundCategory) { ++ shear(shearedSoundCategory, 0); ++ } ++ ++ void shear(SoundSource shearedSoundCategory, int looting); ++ // Purpur end + + boolean readyForShearing(); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +index e283eb57c25f7de222f9d09dca851169f5f6e488..210a0bee1227e4671909dd553ab22027cfc868fb 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +@@ -24,14 +24,21 @@ public class AttributeMap { + private final Set dirtyAttributes = Sets.newHashSet(); + private final AttributeSupplier supplier; + private final java.util.function.Function createInstance; // Pufferfish ++ private final net.minecraft.world.entity.LivingEntity entity; // Purpur + + public AttributeMap(AttributeSupplier defaultAttributes) { ++ // Purpur start ++ this(defaultAttributes, null); ++ } ++ public AttributeMap(AttributeSupplier defaultAttributes, net.minecraft.world.entity.LivingEntity entity) { ++ this.entity = entity; ++ // Purpur end + this.supplier = defaultAttributes; + this.createInstance = attribute -> this.supplier.createInstance(this::onAttributeModified, attribute); // Pufferfish + } + + private void onAttributeModified(AttributeInstance instance) { +- if (instance.getAttribute().isClientSyncable()) { ++ if (instance.getAttribute().isClientSyncable() && (entity == null || entity.shouldSendAttribute(instance.getAttribute()))) { // Purpur + this.dirtyAttributes.add(instance); + } + +@@ -43,7 +50,7 @@ public class AttributeMap { + + public Collection getSyncableAttributes() { + return this.attributes.values().stream().filter((attribute) -> { +- return attribute.getAttribute().isClientSyncable(); ++ return attribute.getAttribute().isClientSyncable() && (entity == null || entity.shouldSendAttribute(attribute.getAttribute())); // Purpur + }).collect(Collectors.toList()); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java +index 3f7c6349a99c1cd4e0b2d7fc7a43bedcf5a9d980..3ce566840032e0b7728c6bcc4dd6e12cbd114268 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java +@@ -2,7 +2,9 @@ package net.minecraft.world.entity.ai.attributes; + + import com.google.common.collect.ImmutableMap; + import com.mojang.logging.LogUtils; ++ + import java.util.Map; ++ + import net.minecraft.Util; + import net.minecraft.core.registries.BuiltInRegistries; + import net.minecraft.world.entity.EntityType; +@@ -79,7 +81,87 @@ import org.slf4j.Logger; + + public class DefaultAttributes { + private static final Logger LOGGER = LogUtils.getLogger(); +- private static final Map, AttributeSupplier> SUPPLIERS = ImmutableMap., AttributeSupplier>builder().put(EntityType.ALLAY, Allay.createAttributes().build()).put(EntityType.ARMOR_STAND, LivingEntity.createLivingAttributes().build()).put(EntityType.AXOLOTL, Axolotl.createAttributes().build()).put(EntityType.BAT, Bat.createAttributes().build()).put(EntityType.BEE, Bee.createAttributes().build()).put(EntityType.BLAZE, Blaze.createAttributes().build()).put(EntityType.CAT, Cat.createAttributes().build()).put(EntityType.CAMEL, Camel.createAttributes().build()).put(EntityType.CAVE_SPIDER, CaveSpider.createCaveSpider().build()).put(EntityType.CHICKEN, Chicken.createAttributes().build()).put(EntityType.COD, AbstractFish.createAttributes().build()).put(EntityType.COW, Cow.createAttributes().build()).put(EntityType.CREEPER, Creeper.createAttributes().build()).put(EntityType.DOLPHIN, Dolphin.createAttributes().build()).put(EntityType.DONKEY, AbstractChestedHorse.createBaseChestedHorseAttributes().build()).put(EntityType.DROWNED, Zombie.createAttributes().build()).put(EntityType.ELDER_GUARDIAN, ElderGuardian.createAttributes().build()).put(EntityType.ENDERMAN, EnderMan.createAttributes().build()).put(EntityType.ENDERMITE, Endermite.createAttributes().build()).put(EntityType.ENDER_DRAGON, EnderDragon.createAttributes().build()).put(EntityType.EVOKER, Evoker.createAttributes().build()).put(EntityType.FOX, Fox.createAttributes().build()).put(EntityType.FROG, Frog.createAttributes().build()).put(EntityType.GHAST, Ghast.createAttributes().build()).put(EntityType.GIANT, Giant.createAttributes().build()).put(EntityType.GLOW_SQUID, GlowSquid.createAttributes().build()).put(EntityType.GOAT, Goat.createAttributes().build()).put(EntityType.GUARDIAN, Guardian.createAttributes().build()).put(EntityType.HOGLIN, Hoglin.createAttributes().build()).put(EntityType.HORSE, AbstractHorse.createBaseHorseAttributes().build()).put(EntityType.HUSK, Zombie.createAttributes().build()).put(EntityType.ILLUSIONER, Illusioner.createAttributes().build()).put(EntityType.IRON_GOLEM, IronGolem.createAttributes().build()).put(EntityType.LLAMA, Llama.createAttributes().build()).put(EntityType.MAGMA_CUBE, MagmaCube.createAttributes().build()).put(EntityType.MOOSHROOM, Cow.createAttributes().build()).put(EntityType.MULE, AbstractChestedHorse.createBaseChestedHorseAttributes().build()).put(EntityType.OCELOT, Ocelot.createAttributes().build()).put(EntityType.PANDA, Panda.createAttributes().build()).put(EntityType.PARROT, Parrot.createAttributes().build()).put(EntityType.PHANTOM, Monster.createMonsterAttributes().build()).put(EntityType.PIG, Pig.createAttributes().build()).put(EntityType.PIGLIN, Piglin.createAttributes().build()).put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build()).put(EntityType.PILLAGER, Pillager.createAttributes().build()).put(EntityType.PLAYER, Player.createAttributes().build()).put(EntityType.POLAR_BEAR, PolarBear.createAttributes().build()).put(EntityType.PUFFERFISH, AbstractFish.createAttributes().build()).put(EntityType.RABBIT, Rabbit.createAttributes().build()).put(EntityType.RAVAGER, Ravager.createAttributes().build()).put(EntityType.SALMON, AbstractFish.createAttributes().build()).put(EntityType.SHEEP, Sheep.createAttributes().build()).put(EntityType.SHULKER, Shulker.createAttributes().build()).put(EntityType.SILVERFISH, Silverfish.createAttributes().build()).put(EntityType.SKELETON, AbstractSkeleton.createAttributes().build()).put(EntityType.SKELETON_HORSE, SkeletonHorse.createAttributes().build()).put(EntityType.SLIME, Monster.createMonsterAttributes().build()).put(EntityType.SNOW_GOLEM, SnowGolem.createAttributes().build()).put(EntityType.SPIDER, Spider.createAttributes().build()).put(EntityType.SQUID, Squid.createAttributes().build()).put(EntityType.STRAY, AbstractSkeleton.createAttributes().build()).put(EntityType.STRIDER, Strider.createAttributes().build()).put(EntityType.TADPOLE, Tadpole.createAttributes().build()).put(EntityType.TRADER_LLAMA, Llama.createAttributes().build()).put(EntityType.TROPICAL_FISH, AbstractFish.createAttributes().build()).put(EntityType.TURTLE, Turtle.createAttributes().build()).put(EntityType.VEX, Vex.createAttributes().build()).put(EntityType.VILLAGER, Villager.createAttributes().build()).put(EntityType.VINDICATOR, Vindicator.createAttributes().build()).put(EntityType.WARDEN, Warden.createAttributes().build()).put(EntityType.WANDERING_TRADER, Mob.createMobAttributes().build()).put(EntityType.WITCH, Witch.createAttributes().build()).put(EntityType.WITHER, WitherBoss.createAttributes().build()).put(EntityType.WITHER_SKELETON, AbstractSkeleton.createAttributes().build()).put(EntityType.WOLF, Wolf.createAttributes().build()).put(EntityType.ZOGLIN, Zoglin.createAttributes().build()).put(EntityType.ZOMBIE, Zombie.createAttributes().build()).put(EntityType.ZOMBIE_HORSE, ZombieHorse.createAttributes().build()).put(EntityType.ZOMBIE_VILLAGER, Zombie.createAttributes().build()).put(EntityType.ZOMBIFIED_PIGLIN, ZombifiedPiglin.createAttributes().build()).build(); ++ private static final Map, AttributeSupplier> SUPPLIERS = ImmutableMap., AttributeSupplier>builder() ++ .put(EntityType.ALLAY, Allay.createAttributes().build()) ++ .put(EntityType.ARMOR_STAND, LivingEntity.createLivingAttributes().build()) ++ .put(EntityType.AXOLOTL, Axolotl.createAttributes().build()) ++ .put(EntityType.BAT, Bat.createAttributes().build()) ++ .put(EntityType.BEE, Bee.createAttributes().build()) ++ .put(EntityType.BLAZE, Blaze.createAttributes().build()) ++ .put(EntityType.CAT, Cat.createAttributes().build()) ++ .put(EntityType.CAMEL, Camel.createAttributes().build()) ++ .put(EntityType.CAVE_SPIDER, CaveSpider.createCaveSpider().build()) ++ .put(EntityType.CHICKEN, Chicken.createAttributes().build()) ++ .put(EntityType.COD, AbstractFish.createAttributes().build()) ++ .put(EntityType.COW, Cow.createAttributes().build()) ++ .put(EntityType.CREEPER, Creeper.createAttributes().build()) ++ .put(EntityType.DOLPHIN, Dolphin.createAttributes().build()) ++ .put(EntityType.DONKEY, AbstractChestedHorse.createBaseChestedHorseAttributes().build()) ++ .put(EntityType.DROWNED, Zombie.createAttributes().build()) ++ .put(EntityType.ELDER_GUARDIAN, ElderGuardian.createAttributes().build()) ++ .put(EntityType.ENDERMAN, EnderMan.createAttributes().build()) ++ .put(EntityType.ENDERMITE, Endermite.createAttributes().build()) ++ .put(EntityType.ENDER_DRAGON, EnderDragon.createAttributes().build()) ++ .put(EntityType.EVOKER, Evoker.createAttributes().build()) ++ .put(EntityType.FOX, Fox.createAttributes().build()) ++ .put(EntityType.FROG, Frog.createAttributes().build()) ++ .put(EntityType.GHAST, Ghast.createAttributes().build()) ++ .put(EntityType.GIANT, Giant.createAttributes().build()) ++ .put(EntityType.GLOW_SQUID, GlowSquid.createAttributes().build()) ++ .put(EntityType.GOAT, Goat.createAttributes().build()) ++ .put(EntityType.GUARDIAN, Guardian.createAttributes().build()) ++ .put(EntityType.HOGLIN, Hoglin.createAttributes().build()) ++ .put(EntityType.HORSE, AbstractHorse.createBaseHorseAttributes().build()) ++ .put(EntityType.HUSK, Zombie.createAttributes().build()) ++ .put(EntityType.ILLUSIONER, Illusioner.createAttributes().build()) ++ .put(EntityType.IRON_GOLEM, IronGolem.createAttributes().build()) ++ .put(EntityType.LLAMA, Llama.createAttributes().build()) ++ .put(EntityType.MAGMA_CUBE, MagmaCube.createAttributes().build()) ++ .put(EntityType.MOOSHROOM, Cow.createAttributes().build()) ++ .put(EntityType.MULE, AbstractChestedHorse.createBaseChestedHorseAttributes().build()) ++ .put(EntityType.OCELOT, Ocelot.createAttributes().build()) ++ .put(EntityType.PANDA, Panda.createAttributes().build()) ++ .put(EntityType.PARROT, Parrot.createAttributes().build()) ++ .put(EntityType.PHANTOM, net.minecraft.world.entity.monster.Phantom.createAttributes().build()) // Purpur ++ .put(EntityType.PIG, Pig.createAttributes().build()) ++ .put(EntityType.PIGLIN, Piglin.createAttributes().build()) ++ .put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build()) ++ .put(EntityType.PILLAGER, Pillager.createAttributes().build()) ++ .put(EntityType.PLAYER, Player.createAttributes().build()) ++ .put(EntityType.POLAR_BEAR, PolarBear.createAttributes().build()) ++ .put(EntityType.PUFFERFISH, AbstractFish.createAttributes().build()) ++ .put(EntityType.RABBIT, Rabbit.createAttributes().build()) ++ .put(EntityType.RAVAGER, Ravager.createAttributes().build()) ++ .put(EntityType.SALMON, AbstractFish.createAttributes().build()) ++ .put(EntityType.SHEEP, Sheep.createAttributes().build()) ++ .put(EntityType.SHULKER, Shulker.createAttributes().build()) ++ .put(EntityType.SILVERFISH, Silverfish.createAttributes().build()) ++ .put(EntityType.SKELETON, AbstractSkeleton.createAttributes().build()) ++ .put(EntityType.SKELETON_HORSE, SkeletonHorse.createAttributes().build()) ++ .put(EntityType.SLIME, Monster.createMonsterAttributes().build()) ++ .put(EntityType.SNOW_GOLEM, SnowGolem.createAttributes().build()) ++ .put(EntityType.SPIDER, Spider.createAttributes().build()) ++ .put(EntityType.SQUID, Squid.createAttributes().build()) ++ .put(EntityType.STRAY, AbstractSkeleton.createAttributes().build()) ++ .put(EntityType.STRIDER, Strider.createAttributes().build()) ++ .put(EntityType.TADPOLE, Tadpole.createAttributes().build()) ++ .put(EntityType.TRADER_LLAMA, Llama.createAttributes().build()) ++ .put(EntityType.TROPICAL_FISH, AbstractFish.createAttributes().build()) ++ .put(EntityType.TURTLE, Turtle.createAttributes().build()) ++ .put(EntityType.VEX, Vex.createAttributes().build()) ++ .put(EntityType.VILLAGER, Villager.createAttributes().build()) ++ .put(EntityType.VINDICATOR, Vindicator.createAttributes().build()) ++ .put(EntityType.WARDEN, Warden.createAttributes().build()) ++ .put(EntityType.WANDERING_TRADER, Mob.createMobAttributes().build()) ++ .put(EntityType.WITCH, Witch.createAttributes().build()) ++ .put(EntityType.WITHER, WitherBoss.createAttributes().build()) ++ .put(EntityType.WITHER_SKELETON, AbstractSkeleton.createAttributes().build()) ++ .put(EntityType.WOLF, Wolf.createAttributes().build()) ++ .put(EntityType.ZOGLIN, Zoglin.createAttributes().build()) ++ .put(EntityType.ZOMBIE, Zombie.createAttributes().build()) ++ .put(EntityType.ZOMBIE_HORSE, ZombieHorse.createAttributes().build()) ++ .put(EntityType.ZOMBIE_VILLAGER, Zombie.createAttributes().build()) ++ .put(EntityType.ZOMBIFIED_PIGLIN, ZombifiedPiglin.createAttributes().build()).build(); + + public static AttributeSupplier getSupplier(EntityType type) { + return SUPPLIERS.get(type); +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java b/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java +index f0703302e7dbbda88de8c648d20d87c55ed9b1e0..a913ebabaa5f443afa987b972355a8f8d1723c78 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java +@@ -29,6 +29,7 @@ public class RangedAttribute extends Attribute { + + @Override + public double sanitizeValue(double value) { ++ if (!org.purpurmc.purpur.PurpurConfig.clampAttributes) return Double.isNaN(value) ? this.minValue : value; // Purpur + return Double.isNaN(value) ? this.minValue : Mth.clamp(value, this.minValue, this.maxValue); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java +index 57ef7fbba3028c28231abf7b7ae78aa019323536..651c156dc8a5aad04d461add02e22147af657d07 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java +@@ -58,9 +58,9 @@ public abstract class Behavior implements BehaviorContro + this.status = Behavior.Status.RUNNING; + int i = this.minDuration + world.getRandom().nextInt(this.maxDuration + 1 - this.minDuration); + this.endTimestamp = time + (long)i; +- this.timing.startTiming(); // Paper - behavior timings ++ //this.timing.startTiming(); // Paper - behavior timings // Purpur + this.start(world, entity, time); +- this.timing.stopTiming(); // Paper - behavior timings ++ //this.timing.stopTiming(); // Paper - behavior timings // Purpur + return true; + } else { + return false; +@@ -72,13 +72,13 @@ public abstract class Behavior implements BehaviorContro + + @Override + public final void tickOrStop(ServerLevel world, E entity, long time) { +- this.timing.startTiming(); // Paper - behavior timings ++ //this.timing.startTiming(); // Paper - behavior timings // Purpur + if (!this.timedOut(time) && this.canStillUse(world, entity, time)) { + this.tick(world, entity, time); + } else { + this.doStop(world, entity, time); + } +- this.timing.stopTiming(); // Paper - behavior timings ++ //this.timing.stopTiming(); // Paper - behavior timings // Purpur + + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +index 7ad71f2c139c2288b49d6b0fde3f8b8013f5e095..2dca8e45b9b1f5451db2734cba4c2b03c9dd303b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +@@ -34,17 +34,19 @@ public class HarvestFarmland extends Behavior { + private long nextOkStartTime; + private int timeWorkedSoFar; + private final List validFarmlandAroundVillager = Lists.newArrayList(); ++ private boolean clericWartFarmer = false; // Purpur + + public HarvestFarmland() { + super(ImmutableMap.of(MemoryModuleType.LOOK_TARGET, MemoryStatus.VALUE_ABSENT, MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT, MemoryModuleType.SECONDARY_JOB_SITE, MemoryStatus.VALUE_PRESENT)); + } + + protected boolean checkExtraStartConditions(ServerLevel world, Villager entity) { +- if (!world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!world.purpurConfig.villagerBypassMobGriefing && !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; +- } else if (entity.getVillagerData().getProfession() != VillagerProfession.FARMER) { ++ } else if (entity.getVillagerData().getProfession() != VillagerProfession.FARMER && !(world.purpurConfig.villagerClericsFarmWarts && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC)) { // Purpur + return false; + } else { ++ if (!this.clericWartFarmer && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC) this.clericWartFarmer = true; // Purpur + BlockPos.MutableBlockPos blockposition_mutableblockposition = entity.blockPosition().mutable(); + + this.validFarmlandAroundVillager.clear(); +@@ -75,6 +77,7 @@ public class HarvestFarmland extends Behavior { + Block block = iblockdata.getBlock(); + Block block1 = world.getBlockState(pos.below()).getBlock(); + ++ if (this.clericWartFarmer) return block == Blocks.NETHER_WART && iblockdata.getValue(net.minecraft.world.level.block.NetherWartBlock.AGE) == 3 || iblockdata.isAir() && block1 == Blocks.SOUL_SAND; // Purpur + return block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata) || iblockdata.isAir() && block1 instanceof FarmBlock; + } + +@@ -100,7 +103,7 @@ public class HarvestFarmland extends Behavior { + Block block = iblockdata.getBlock(); + Block block1 = world.getBlockState(this.aboveFarmlandPos.below()).getBlock(); + +- if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata)) { ++ if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata) && !this.clericWartFarmer || this.clericWartFarmer && block == Blocks.NETHER_WART && iblockdata.getValue(net.minecraft.world.level.block.NetherWartBlock.AGE) == 3) { // Purpur + // CraftBukkit start + if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, this.aboveFarmlandPos, Blocks.AIR.defaultBlockState()).isCancelled()) { + world.destroyBlock(this.aboveFarmlandPos, true, entity); +@@ -108,7 +111,7 @@ public class HarvestFarmland extends Behavior { + // CraftBukkit end + } + +- if (iblockdata.isAir() && block1 instanceof FarmBlock && entity.hasFarmSeeds()) { ++ if (iblockdata.isAir() && (block1 instanceof FarmBlock && !this.clericWartFarmer || this.clericWartFarmer && block1 == Blocks.SOUL_SAND) && entity.hasFarmSeeds()) { // Purpur + SimpleContainer inventorysubcontainer = entity.getInventory(); + + for (int j = 0; j < inventorysubcontainer.getContainerSize(); ++j) { +@@ -119,6 +122,12 @@ public class HarvestFarmland extends Behavior { + BlockState iblockdata1; + + // CraftBukkit start ++ // Purpur start ++ if (this.clericWartFarmer && itemstack.getItem() == Items.NETHER_WART) { ++ iblockdata1 = Blocks.NETHER_WART.defaultBlockState(); ++ flag = true; ++ } else ++ // Purpur end + if (itemstack.is(Items.WHEAT_SEEDS)) { + iblockdata1 = Blocks.WHEAT.defaultBlockState(); + flag = true; +@@ -145,7 +154,7 @@ public class HarvestFarmland extends Behavior { + } + + if (flag) { +- world.playSound((Player) null, (double) this.aboveFarmlandPos.getX(), (double) this.aboveFarmlandPos.getY(), (double) this.aboveFarmlandPos.getZ(), SoundEvents.CROP_PLANTED, SoundSource.BLOCKS, 1.0F, 1.0F); ++ world.playSound((Player) null, (double) this.aboveFarmlandPos.getX(), (double) this.aboveFarmlandPos.getY(), (double) this.aboveFarmlandPos.getZ(), this.clericWartFarmer ? SoundEvents.NETHER_WART_PLANTED : SoundEvents.CROP_PLANTED, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur + itemstack.shrink(1); + if (itemstack.isEmpty()) { + inventorysubcontainer.setItem(j, ItemStack.EMPTY); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java b/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java +index c3fb86dc3d94d3a0d2464f2dbb83cda2fb9f7bbe..fd77dd0c0bfaba57e5bdfd13f7a90241ecdf813a 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java +@@ -57,7 +57,7 @@ public class InteractWithDoor { + + if (iblockdata.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) && !DoorBlock.requiresRedstone(entityliving.level, iblockdata, blockposition)) { // Purpur + DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock(); + + if (!blockdoor.isOpen(iblockdata)) { +@@ -79,7 +79,7 @@ public class InteractWithDoor { + + if (iblockdata1.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) && !DoorBlock.requiresRedstone(entityliving.level, iblockdata, blockposition1)) { // Purpur + DoorBlock blockdoor1 = (DoorBlock) iblockdata1.getBlock(); + + if (!blockdoor1.isOpen(iblockdata1)) { +@@ -122,7 +122,7 @@ public class InteractWithDoor { + + if (!iblockdata.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) || DoorBlock.requiresRedstone(entity.level, iblockdata, blockposition)) { // Purpur + iterator.remove(); + } else { + DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java b/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java +index 98373e013748817209b811d4adbb40a8787242a6..567b501f4de7556e55e2418d2f5700b4e4265235 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java +@@ -42,6 +42,7 @@ public class ShowTradesToPlayer extends Behavior { + + @Override + public boolean canStillUse(ServerLevel world, Villager entity, long time) { ++ if (!entity.level.purpurConfig.villagerDisplayTradeItem) return false; // Purpur + return this.checkExtraStartConditions(world, entity) && this.lookTime > 0 && entity.getBrain().getMemory(MemoryModuleType.INTERACTION_TARGET).isPresent(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java b/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java +index 3af715e2f3f3949af614a8fcebbc4a835d48ca49..ade1e411ea1f3b4c9a417265e77b0d6861b222f9 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java +@@ -56,6 +56,12 @@ public class TradeWithVillager extends Behavior { + throwHalfStack(entity, ImmutableSet.of(Items.WHEAT), villager); + } + ++ // Purpur start ++ if (world.purpurConfig.villagerClericsFarmWarts && world.purpurConfig.villagerClericFarmersThrowWarts && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC && entity.getInventory().countItem(Items.NETHER_WART) > Items.NETHER_WART.getMaxStackSize() / 2) { ++ throwHalfStack(entity, ImmutableSet.of(Items.NETHER_WART), villager); ++ } ++ // Purpur end ++ + if (!this.trades.isEmpty() && entity.getInventory().hasAnyOf(this.trades)) { + throwHalfStack(entity, this.trades, villager); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java +index cd7a90ec1073b2b452ca70decefe6a594445003b..47672e48c1cae73cffe532d622b296343fc12ef0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java +@@ -30,8 +30,13 @@ public class VillagerGoalPackages { + } + + public static ImmutableList>> getWorkPackage(VillagerProfession profession, float speed) { ++ // Purpur start ++ return getWorkPackage(profession, speed, false); ++ } ++ public static ImmutableList>> getWorkPackage(VillagerProfession profession, float speed, boolean clericsFarmWarts) { ++ // Purpur end + WorkAtPoi workAtPoi; +- if (profession == VillagerProfession.FARMER) { ++ if (profession == VillagerProfession.FARMER || (clericsFarmWarts && profession == VillagerProfession.CLERIC)) { // Purpur + workAtPoi = new WorkAtComposter(); + } else { + workAtPoi = new WorkAtPoi(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java +index 0951c04533e7c39b969d041271684355770b53c2..02d4ba2ccdce99ca97614baa7c8e49213126af96 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java +@@ -123,8 +123,10 @@ public class VillagerMakeLove extends Behavior { + return Optional.empty(); + } + // CraftBukkit end +- parent.setAge(6000); +- partner.setAge(6000); ++ // Purpur start ++ parent.setAge(world.purpurConfig.villagerBreedingTicks); ++ partner.setAge(world.purpurConfig.villagerBreedingTicks); ++ // Purpur end + world.addFreshEntityWithPassengers(entityvillager2, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason + world.broadcastEntityEvent(entityvillager2, (byte) 12); + return Optional.of(entityvillager2); +diff --git a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java +index e304d7bc0b6b7c167cfc163a9df4d7a3126037e3..bde157ec8f591445cf4660922f70fa904dac213b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java ++++ b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java +@@ -29,6 +29,20 @@ public class MoveControl implements Control { + this.mob = entity; + } + ++ // Purpur start ++ public void setSpeedModifier(double speed) { ++ this.speedModifier = speed; ++ } ++ ++ public void setForward(float forward) { ++ this.strafeForwards = forward; ++ } ++ ++ public void setStrafe(float strafe) { ++ this.strafeRight = strafe; ++ } ++ // Purpur end ++ + public boolean hasWanted() { + return this.operation == MoveControl.Operation.MOVE_TO; + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java +index 7df56705a4a0de2dc4ff7ab133fc26612c219162..60d21d6171b9af20a4c6fcc0d564a31aaa4ecdba 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java ++++ b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java +@@ -3,7 +3,7 @@ package net.minecraft.world.entity.ai.control; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.Mob; + +-public class SmoothSwimmingLookControl extends LookControl { ++public class SmoothSwimmingLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + private final int maxYRotFromCenter; + private static final int HEAD_TILT_X = 10; + private static final int HEAD_TILT_Y = 20; +@@ -14,7 +14,7 @@ public class SmoothSwimmingLookControl extends LookControl { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.lookAtCooldown > 0) { + --this.lookAtCooldown; + this.getYRotD().ifPresent((yaw) -> { +@@ -32,9 +32,9 @@ public class SmoothSwimmingLookControl extends LookControl { + } + + float f = Mth.wrapDegrees(this.mob.yHeadRot - this.mob.yBodyRot); +- if (f < (float)(-this.maxYRotFromCenter)) { ++ if (f < (float) (-this.maxYRotFromCenter)) { + this.mob.yBodyRot -= 4.0F; +- } else if (f > (float)this.maxYRotFromCenter) { ++ } else if (f > (float) this.maxYRotFromCenter) { + this.mob.yBodyRot += 4.0F; + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java +index 529435cf648d61f80a37f041cee3c6fc0b74ceb6..6c7195c93b5968845da35450e80022c70839487d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java +@@ -32,7 +32,7 @@ public class BreakDoorGoal extends DoorInteractGoal { + + @Override + public boolean canUse() { +- return !super.canUse() ? false : (!this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.isValidDifficulty(this.mob.level.getDifficulty()) && !this.isOpen()); ++ return !super.canUse() ? false : ((!this.mob.level.purpurConfig.zombieBypassMobGriefing && !this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) ? false : this.isValidDifficulty(this.mob.level.getDifficulty()) && !this.isOpen()); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java +index 80aa539f7c6a6ee44338de084cdcdf5fb4ef996a..c55118a4d2237a33039b63dc797ccdb86b63344f 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java +@@ -31,6 +31,12 @@ public class EatBlockGoal extends Goal { + + @Override + public boolean canUse() { ++ // Purpur start ++ net.minecraft.world.level.chunk.LevelChunk chunk = this.mob.level.getChunkIfLoaded(this.mob.blockPosition()); ++ if (chunk == null || chunk.playerChunk == null || !((net.minecraft.server.level.ServerLevel) this.mob.level).getChunkSource().chunkMap.anyPlayerCloseEnoughForSpawning(chunk.playerChunk, this.mob.chunkPosition(), false)) { ++ return false; ++ } ++ // Purpur end + if (this.mob.getRandom().nextInt(this.mob.isBaby() ? 50 : 1000) != 0) { + return false; + } else { +@@ -69,7 +75,7 @@ public class EatBlockGoal extends Goal { + + if (EatBlockGoal.IS_TALL_GRASS.test(this.level.getBlockState(blockposition))) { + // CraftBukkit +- if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, Blocks.AIR.defaultBlockState(), !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { ++ if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, Blocks.AIR.defaultBlockState(), !this.level.purpurConfig.sheepBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // Purpur + this.level.destroyBlock(blockposition, false); + } + +@@ -79,7 +85,7 @@ public class EatBlockGoal extends Goal { + + if (this.level.getBlockState(blockposition1).is(Blocks.GRASS_BLOCK)) { + // CraftBukkit +- if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // Paper - Fix wrong block state ++ if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !this.level.purpurConfig.sheepBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // Paper - Fix wrong block state // Purpur + this.level.levelEvent(2001, blockposition1, Block.getId(Blocks.GRASS_BLOCK.defaultBlockState())); + this.level.setBlock(blockposition1, Blocks.DIRT.defaultBlockState(), 2); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index 1635818fc4b1788c0d397085239df6dd75b210ab..02978315bc2b828cc603ce7478408f3f82c249c2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -105,8 +105,8 @@ public class GoalSelector { + } + + public void tick() { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("goalCleanup"); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("goalCleanup"); // Purpur + + for(WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { +@@ -123,8 +123,8 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); +- profilerFiller.push("goalUpdate"); ++ //profilerFiller.pop(); // Purpur ++ //profilerFiller.push("goalUpdate"); // Purpur + + for(WrappedGoal wrappedGoal2 : this.availableGoals) { + // Paper start +@@ -144,13 +144,13 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + this.tickRunningGoals(true); + } + + public void tickRunningGoals(boolean tickAll) { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("goalTick"); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("goalTick"); // Purpur + + for(WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.isRunning() && (tickAll || wrappedGoal.requiresUpdateEveryTick())) { +@@ -158,7 +158,7 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + } + + public Set getAvailableGoals() { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +index 721971f7618751a2e95f1c49fdc48a9c0c672cab..ad30f2d678cfc4b0d693e84e6e152c63b1b3cbb8 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +@@ -22,6 +22,7 @@ public class LlamaFollowCaravanGoal extends Goal { + + @Override + public boolean canUse() { ++ if (!this.llama.level.purpurConfig.llamaJoinCaravans || !this.llama.shouldJoinCaravan) return false; // Purpur + if (!this.llama.isLeashed() && !this.llama.inCaravan()) { + List list = this.llama.level.getEntities(this.llama, this.llama.getBoundingBox().inflate(9.0D, 4.0D, 9.0D), (entity) -> { + EntityType entityType = entity.getType(); +@@ -71,6 +72,7 @@ public class LlamaFollowCaravanGoal extends Goal { + + @Override + public boolean canContinueToUse() { ++ if (!this.llama.shouldJoinCaravan) return false; // Purpur + if (this.llama.inCaravan() && this.llama.getCaravanHead().isAlive() && this.firstIsLeashed(this.llama, 0)) { + double d = this.llama.distanceToSqr(this.llama.getCaravanHead()); + if (d > 676.0D) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java +index 6558b0d4bea99948fdc2b51751f3cfdc239d4b67..d85dabebbbbe213e791b8a3be3c6df05b959e40c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java +@@ -111,9 +111,9 @@ public class RangedBowAttackGoal extends Go + + this.mob.getMoveControl().strafe(this.strafingBackwards ? -0.5F : 0.5F, this.strafingClockwise ? 0.5F : -0.5F); + this.mob.lookAt(livingEntity, 30.0F, 30.0F); +- } else { ++ } //else { // Purpur - fix MC-121706 + this.mob.getLookControl().setLookAt(livingEntity, 30.0F, 30.0F); +- } ++ //} // Purpur + + if (this.mob.isUsingItem()) { + if (!bl && this.seeTime < -60) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +index bd0cbf4390fc7d00b4bd5008cdf8f6f49df4f69b..27e96c4c1377c49f03df032683aac32d96ae1c6f 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +@@ -40,7 +40,7 @@ public class RemoveBlockGoal extends MoveToBlockGoal { + + @Override + public boolean canUse() { +- if (!this.removerMob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.removerMob.level.purpurConfig.zombieBypassMobGriefing && !this.removerMob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } else if (this.nextStartTick > 0) { + --this.nextStartTick; +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java +index 5c64905e90ccca6e0b347241ddf9cc3f71058b8e..3bd7521b131b2b40f807bdc7ab95e64cf9bcdadc 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java +@@ -63,7 +63,7 @@ public class RunAroundLikeCrazyGoal extends Goal { + int j = this.horse.getMaxTemper(); + + // CraftBukkit - fire EntityTameEvent +- if (j > 0 && this.horse.getRandom().nextInt(j) < i && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this.horse, ((org.bukkit.craftbukkit.entity.CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled()) { ++ if ((this.horse.level.purpurConfig.alwaysTameInCreative && ((Player) entity).getAbilities().instabuild) || (j > 0 && this.horse.getRandom().nextInt(j) < i && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this.horse, ((org.bukkit.craftbukkit.entity.CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled())) { // Purpur + this.horse.tameWithName((Player) entity); + return; + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java +index e241ae250f4f04a17ef2c583d00b065a4ca56a4c..02b567e4e808e1a809d285ef39e1abc54e1e6ad2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java +@@ -54,6 +54,14 @@ public class SwellGoal extends Goal { + this.creeper.setSwellDir(-1); + } else { + this.creeper.setSwellDir(1); ++ // Purpur start ++ if (this.creeper.getLevel().purpurConfig.creeperEncircleTarget) { ++ net.minecraft.world.phys.Vec3 relative = this.creeper.position().subtract(this.target.position()); ++ relative = relative.yRot((float) Math.PI / 3).normalize().multiply(2, 2, 2); ++ net.minecraft.world.phys.Vec3 destination = this.target.position().add(relative); ++ this.creeper.getNavigation().moveTo(destination.x, destination.y, destination.z, 1); ++ } ++ // Purpur end + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java +index 79bb13c5614bab1f0749c5f8f57f762c6216c564..2cbc9adc8e417def48be03d08174a5833068ec65 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java +@@ -62,7 +62,7 @@ public class TemptGoal extends Goal { + } + + private boolean shouldFollow(LivingEntity entity) { +- return this.items.test(entity.getMainHandItem()) || this.items.test(entity.getOffhandItem()); ++ return (this.items.test(entity.getMainHandItem()) || this.items.test(entity.getOffhandItem())) && (!(this.mob instanceof net.minecraft.world.entity.npc.Villager villager) || !villager.isSleeping()); // Purpur Fix #512 + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index 97257b450e848f53fdb9b5b7affa57b03ea5f459..2f2d9bb31194618ef5bba39cd1cbe7c4919e82c5 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -171,12 +171,12 @@ public abstract class PathNavigation { + } + } + // Paper end +- this.level.getProfiler().push("pathfind"); ++ //this.level.getProfiler().push("pathfind"); // Purpur + BlockPos blockPos = useHeadPos ? this.mob.blockPosition().above() : this.mob.blockPosition(); + int i = (int)(followRange + (float)range); + PathNavigationRegion pathNavigationRegion = new PathNavigationRegion(this.level, blockPos.offset(-i, -i, -i), blockPos.offset(i, i, i)); + Path path = this.pathFinder.findPath(pathNavigationRegion, this.mob, positions, followRange, distance, this.maxVisitedNodesMultiplier); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + if (path != null && path.getTarget() != null) { + this.targetPos = path.getTarget(); + this.reachRange = distance; +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +index cb1d91f9fe98f21c2afbe3894dfd9bca3bdd3ba6..d2703432af207c74ea8d298a784329c3219d2f13 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +@@ -22,6 +22,13 @@ public class SecondaryPoiSensor extends Sensor { + + @Override + protected void doTick(ServerLevel world, Villager entity) { ++ // Purpur start - make sure clerics don't wander to soul sand when the option is off ++ Brain brain = entity.getBrain(); ++ if (!world.purpurConfig.villagerClericsFarmWarts && entity.getVillagerData().getProfession() == net.minecraft.world.entity.npc.VillagerProfession.CLERIC) { ++ brain.eraseMemory(MemoryModuleType.SECONDARY_JOB_SITE); ++ return; ++ } ++ // Purpur end + ResourceKey resourceKey = world.dimension(); + BlockPos blockPos = entity.blockPosition(); + List list = Lists.newArrayList(); +@@ -38,7 +45,7 @@ public class SecondaryPoiSensor extends Sensor { + } + } + +- Brain brain = entity.getBrain(); ++ //Brain brain = entity.getBrain(); // Purpur - moved up + if (!list.isEmpty()) { + brain.setMemory(MemoryModuleType.SECONDARY_JOB_SITE, list); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java +index 288c6627906d07c0d223eacd84ae4eb31a349998..9babe636176da3c40598eb5bdac0919a1704eaa0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java +@@ -26,9 +26,9 @@ public class Sensing { + } else if (this.unseen.contains(i)) { + return false; + } else { +- this.mob.level.getProfiler().push("hasLineOfSight"); ++ //this.mob.level.getProfiler().push("hasLineOfSight"); // Purpur + boolean bl = this.mob.hasLineOfSight(entity); +- this.mob.level.getProfiler().pop(); ++ //this.mob.level.getProfiler().pop(); // Purpur + if (bl) { + this.seen.add(i); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java +index fcdb9bde8e1605e30dde3e580491522d4b62cdc0..7094701d213c73ba47ace806962244c10fdf4dda 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java +@@ -46,10 +46,10 @@ public abstract class Sensor { + if (--this.timeToTick <= 0L) { + // Paper start - configurable sensor tick rate and timings + this.timeToTick = java.util.Objects.requireNonNullElse(world.paperConfig().tickRates.sensor.get(entity.getType(), this.configKey), this.scanRate); +- this.timing.startTiming(); ++ //this.timing.startTiming(); // Purpur + // Paper end + this.doTick(world, entity); +- this.timing.stopTiming(); // Paper - sensor timings ++ //this.timing.stopTiming(); // Paper - sensor timings // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +index e752c83df50fb9b670ecea2abc95426c2a009b6f..baa4f9026d31de92210300ecb8ee8c1b6d575435 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java ++++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +@@ -64,6 +64,10 @@ public class TargetingConditions { + return false; + } else if (this.selector != null && !this.selector.test(targetEntity)) { + return false; ++ // Purpur start ++ } else if (!targetEntity.level.purpurConfig.idleTimeoutTargetPlayer && targetEntity instanceof net.minecraft.server.level.ServerPlayer player && player.isAfk()) { ++ return false; ++ // Purpur end + } else { + if (baseEntity == null) { + if (this.isCombat && (!targetEntity.canBeSeenAsEnemy() || targetEntity.level.getDifficulty() == Difficulty.PEACEFUL)) { +diff --git a/src/main/java/net/minecraft/world/entity/ambient/Bat.java b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +index 1572a81ce1718964d795f2a2a411402f88901c73..efcbb0be9984562fc2777af89a655725960bcfbe 100644 +--- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java ++++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +@@ -18,6 +18,7 @@ import net.minecraft.world.entity.EntityDimensions; + import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.MobSpawnType; ++import net.minecraft.world.entity.MoverType; + import net.minecraft.world.entity.Pose; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; +@@ -41,12 +42,81 @@ public class Bat extends AmbientCreature { + + public Bat(EntityType type, Level world) { + super(type, world); ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.075F); // Purpur + if (!world.isClientSide) { + this.setResting(true); + } + + } + ++ // Purpur start ++ @Override ++ public boolean shouldSendAttribute(net.minecraft.world.entity.ai.attributes.Attribute attribute) { return attribute != Attributes.FLYING_SPEED; } // Fixes log spam on clients ++ ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.batRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.batRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.batControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.batMaxY; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ if (isResting()) { ++ setResting(false); ++ level.levelEvent(null, 1025, new BlockPos(this).above(), 0); ++ } ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, 0.25, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.batMaxHealth); ++ this.getAttribute(Attributes.FOLLOW_RANGE).setBaseValue(this.level.purpurConfig.batFollowRange); ++ this.getAttribute(Attributes.KNOCKBACK_RESISTANCE).setBaseValue(this.level.purpurConfig.batKnockbackResistance); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level.purpurConfig.batMovementSpeed); ++ this.getAttribute(Attributes.FLYING_SPEED).setBaseValue(this.level.purpurConfig.batFlyingSpeed); ++ this.getAttribute(Attributes.ARMOR).setBaseValue(this.level.purpurConfig.batArmor); ++ this.getAttribute(Attributes.ARMOR_TOUGHNESS).setBaseValue(this.level.purpurConfig.batArmorToughness); ++ this.getAttribute(Attributes.ATTACK_KNOCKBACK).setBaseValue(this.level.purpurConfig.batAttackKnockback); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.batTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.batAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return !this.isResting() && this.tickCount % Bat.TICKS_PER_FLAP == 0; +@@ -96,7 +166,7 @@ public class Bat extends AmbientCreature { + protected void pushEntities() {} + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + public boolean isResting() { +@@ -128,6 +198,14 @@ public class Bat extends AmbientCreature { + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z()); ++ return; ++ } ++ // Purpur end ++ + super.customServerAiStep(); + BlockPos blockposition = this.blockPosition(); + BlockPos blockposition1 = blockposition.above(); +@@ -246,7 +324,7 @@ public class Bat extends AmbientCreature { + int i = world.getMaxLocalRawBrightness(pos); + byte b0 = 4; + +- if (Bat.isHalloween()) { ++ if (Bat.isHalloweenSeason(world.getMinecraftWorld())) { // Purpur + b0 = 7; + } else if (random.nextBoolean()) { + return false; +@@ -260,6 +338,7 @@ public class Bat extends AmbientCreature { + private static boolean isSpookySeason = false; + private static final int ONE_HOUR = 20 * 60 * 60; + private static int lastSpookyCheck = -ONE_HOUR; ++ public static boolean isHalloweenSeason(Level level) { return level.purpurConfig.forceHalloweenSeason || isHalloween(); } // Purpur + private static boolean isHalloween() { + if (net.minecraft.server.MinecraftServer.currentTick - lastSpookyCheck > ONE_HOUR) { + LocalDate localdate = LocalDate.now(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java +index 1f85f34c1e50f34fb270d2fac7d307c82a550bfa..324f52edd95b5f9a498e46def8c14435cfd00abb 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java +@@ -94,7 +94,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + @Override + protected void registerGoals() { + super.registerGoals(); +- this.goalSelector.addGoal(0, new PanicGoal(this, 1.25D)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 1.6D, 1.4D, EntitySelector.NO_SPECTATORS::test)); + this.goalSelector.addGoal(4, new AbstractFish.FishSwimGoal(this)); + } +@@ -107,7 +107,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + @Override + public void travel(Vec3 movementInput) { + if (this.isEffectiveAi() && this.isInWater()) { +- this.moveRelative(0.01F, movementInput); ++ this.moveRelative(getRider() != null ? getSpeed() : 0.01F, movementInput); // Purpur + this.move(MoverType.SELF, this.getDeltaMovement()); + this.setDeltaMovement(this.getDeltaMovement().scale(0.9D)); + if (this.getTarget() == null) { +@@ -166,7 +166,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + protected void playStepSound(BlockPos pos, BlockState state) { + } + +- static class FishMoveControl extends MoveControl { ++ static class FishMoveControl extends org.purpurmc.purpur.controller.WaterMoveControllerWASD { // Purpur + private final AbstractFish fish; + + FishMoveControl(AbstractFish owner) { +@@ -174,14 +174,22 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + this.fish = owner; + } + ++ // Purpur start + @Override +- public void tick() { ++ public void purpurTick(Player rider) { ++ super.purpurTick(rider); ++ fish.setDeltaMovement(fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ } ++ // Purpur end ++ ++ @Override ++ public void vanillaTick() { // Purpur + if (this.fish.isEyeInFluid(FluidTags.WATER)) { + this.fish.setDeltaMovement(this.fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); + } + + if (this.operation == MoveControl.Operation.MOVE_TO && !this.fish.getNavigation().isDone()) { +- float f = (float)(this.speedModifier * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f = (float)(this.getSpeedModifier() * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + this.fish.setSpeed(Mth.lerp(0.125F, this.fish.getSpeed(), f)); + double d = this.wantedX - this.fish.getX(); + double e = this.wantedY - this.fish.getY(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Animal.java b/src/main/java/net/minecraft/world/entity/animal/Animal.java +index 3c4d142e982c34a23bdb5da1f51c8dcacc0532c1..2ac88f06ebb79e515cd9934ac1e3e2c8003d9e3c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Animal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Animal.java +@@ -39,6 +39,7 @@ public abstract class Animal extends AgeableMob { + @Nullable + public UUID loveCause; + public ItemStack breedItem; // CraftBukkit - Add breedItem variable ++ public abstract int getPurpurBreedTime(); // Purpur + + protected Animal(EntityType type, Level world) { + super(type, world); +@@ -150,7 +151,7 @@ public abstract class Animal extends AgeableMob { + if (this.isFood(itemstack)) { + int i = this.getAge(); + +- if (!this.level.isClientSide && i == 0 && this.canFallInLove()) { ++ if (!this.level.isClientSide && i == 0 && this.canFallInLove() && (this.level.purpurConfig.animalBreedingCooldownSeconds <= 0 || !this.level.hasBreedingCooldown(player.getUUID(), this.getClass()))) { // Purpur + this.usePlayerItem(player, hand, itemstack); + this.setInLove(player); + return InteractionResult.SUCCESS; +@@ -237,6 +238,14 @@ public abstract class Animal extends AgeableMob { + if (entityplayer == null && other.getLoveCause() != null) { + entityplayer = other.getLoveCause(); + } ++ // Purpur start ++ if (entityplayer != null && world.purpurConfig.animalBreedingCooldownSeconds > 0) { ++ if (world.hasBreedingCooldown(entityplayer.getUUID(), this.getClass())) { ++ return; ++ } ++ world.addBreedingCooldown(entityplayer.getUUID(), this.getClass()); ++ } ++ // Purpur end + // CraftBukkit start - call EntityBreedEvent + entityageable.setBaby(true); + entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); +@@ -253,8 +262,10 @@ public abstract class Animal extends AgeableMob { + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer, this, other, entityageable); + } + +- this.setAge(6000); +- other.setAge(6000); ++ // Purpur start ++ this.setAge(this.getPurpurBreedTime()); ++ other.setAge(other.getPurpurBreedTime()); ++ // Purpur end + this.resetLove(); + other.resetLove(); + world.addFreshEntityWithPassengers(entityageable, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason +diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java +index 337a88a7cd6445004d005ef8d56af1b1cdf800d9..87bd7991a81a2e30ecfccb60e614d7f13acd3744 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java +@@ -43,6 +43,7 @@ import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.LivingEntity; + import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.MobType; ++import net.minecraft.world.entity.MoverType; + import net.minecraft.world.entity.NeutralMob; + import net.minecraft.world.entity.PathfinderMob; + import net.minecraft.world.entity.Pose; +@@ -143,6 +144,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + public Bee(EntityType type, Level world) { + super(type, world); + this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(this.random, 20, 60); ++ final org.purpurmc.purpur.controller.FlyingMoveControllerWASD flyingController = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this, 0.25F, 1.0F, false); // Purpur + // Paper start - apply gravity to bees when they get stuck in the void, fixes MC-167279 + class BeeFlyingMoveControl extends FlyingMoveControl { + public BeeFlyingMoveControl(final Mob entity, final int maxPitchChange, final boolean noGravity) { +@@ -151,22 +153,89 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + public void tick() { ++ // Purpur start ++ if (mob.getRider() != null && mob.isControllable()) { ++ flyingController.purpurTick(mob.getRider()); ++ return; ++ } ++ // Purpur end + if (this.mob.getY() <= Bee.this.level.getMinBuildHeight()) { + this.mob.setNoGravity(false); + } + super.tick(); + } ++ ++ // Purpur start ++ @Override ++ public boolean hasWanted() { ++ return mob.getRider() != null || !mob.isControllable() || super.hasWanted(); ++ } ++ // Purpur end + } + this.moveControl = new BeeFlyingMoveControl(this, 20, true); + // Paper end + this.lookControl = new Bee.BeeLookControl(this); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, -1.0F); +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + this.setPathfindingMalus(BlockPathTypes.WATER_BORDER, 16.0F); + this.setPathfindingMalus(BlockPathTypes.COCOA, -1.0F); + this.setPathfindingMalus(BlockPathTypes.FENCE, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.beeRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.beeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.beeControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.beeMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, speed, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.beeMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.beeBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.beeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.beeAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -181,6 +250,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new Bee.BeeAttackGoal(this, 1.399999976158142D, true)); + this.goalSelector.addGoal(1, new Bee.BeeEnterHiveGoal()); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); +@@ -196,6 +266,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.goalSelector.addGoal(7, new Bee.BeeGrowCropGoal()); + this.goalSelector.addGoal(8, new Bee.BeeWanderGoal()); + this.goalSelector.addGoal(9, new FloatGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new Bee.BeeHurtByOtherGoal(this)).setAlertOthers(new Class[0])); + this.targetSelector.addGoal(2, new Bee.BeeBecomeAngryTargetGoal(this)); + this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true)); +@@ -344,7 +415,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + boolean wantsToEnterHive() { + if (this.stayOutOfHiveCountdown <= 0 && !this.beePollinateGoal.isPollinating() && !this.hasStung() && this.getTarget() == null) { +- boolean flag = this.isTiredOfLookingForNectar() || this.level.isRaining() || this.level.isNight() || this.hasNectar(); ++ boolean flag = this.isTiredOfLookingForNectar() || (this.level.isRaining() && !this.level.purpurConfig.beeCanWorkInRain) || (this.level.isNight() && !this.level.purpurConfig.beeCanWorkAtNight) || this.hasNectar(); // Purpur + + return flag && !this.isHiveNearFire(); + } else { +@@ -384,6 +455,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.hurt(DamageSource.DROWN, 1.0F); + } + ++ if (flag && !this.level.purpurConfig.beeDiesAfterSting) setHasStung(false); else // Purpur + if (flag) { + ++this.timeSinceSting; + if (this.timeSinceSting % 5 == 0 && this.random.nextInt(Mth.clamp(1200 - this.timeSinceSting, (int) 1, (int) 1200)) == 0) { +@@ -737,6 +809,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + if (optional.isPresent()) { + Bee.this.savedFlowerPos = (BlockPos) optional.get(); + Bee.this.navigation.moveTo((double) Bee.this.savedFlowerPos.getX() + 0.5D, (double) Bee.this.savedFlowerPos.getY() + 0.5D, (double) Bee.this.savedFlowerPos.getZ() + 0.5D, 1.2000000476837158D); ++ new org.purpurmc.purpur.event.entity.BeeFoundFlowerEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(Bee.this.level, Bee.this.savedFlowerPos)).callEvent(); // Purpur + return true; + } else { + Bee.this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(Bee.this.random, 20, 60); +@@ -793,6 +866,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.pollinating = false; + Bee.this.navigation.stop(); + Bee.this.remainingCooldownBeforeLocatingNewFlower = 200; ++ new org.purpurmc.purpur.event.entity.BeeStopPollinatingEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), Bee.this.savedFlowerPos == null ? null : io.papermc.paper.util.MCUtil.toLocation(Bee.this.level, Bee.this.savedFlowerPos), Bee.this.hasNectar()).callEvent(); // Purpur + } + + @Override +@@ -839,6 +913,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.setWantedPos(); + } + ++ if (this.successfulPollinatingTicks == 0) new org.purpurmc.purpur.event.entity.BeeStartedPollinatingEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(Bee.this.level, Bee.this.savedFlowerPos)).callEvent(); // Purpur + ++this.successfulPollinatingTicks; + if (Bee.this.random.nextFloat() < 0.05F && this.successfulPollinatingTicks > this.lastSoundPlayedTick + 60) { + this.lastSoundPlayedTick = this.successfulPollinatingTicks; +@@ -883,16 +958,16 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } + } + +- private class BeeLookControl extends LookControl { ++ private class BeeLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + BeeLookControl(Mob entity) { + super(entity); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (!Bee.this.isAngry()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java +index 0114c1cf3b6b0500149a77ebc190cb7fa2832184..fd7fc2d6a28110050b2050355897d551737939a7 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java +@@ -96,6 +96,51 @@ public class Cat extends TamableAnimal implements VariantHolder { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.catRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.catRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.catControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setInSittingPose(false); ++ setLying(false); ++ setRelaxStateOne(false); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.catMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.catBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.catTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.catAlwaysDropExp; ++ } ++ // Purpur end ++ + public ResourceLocation getResourceLocation() { + return this.getVariant().texture(); + } +@@ -104,6 +149,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + protected void registerGoals() { + this.temptGoal = new Cat.CatTemptGoal(this, 0.6D, Cat.TEMPT_INGREDIENT, true); + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new SitWhenOrderedToGoal(this)); + this.goalSelector.addGoal(2, new Cat.CatRelaxOnOwnerGoal(this)); + this.goalSelector.addGoal(3, this.temptGoal); +@@ -115,6 +161,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + this.goalSelector.addGoal(10, new BreedGoal(this, 0.8D)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 0.8D, 1.0000001E-5F)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 10.0F)); ++ this.targetSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Rabbit.class, false, (Predicate) null)); + this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -311,6 +358,14 @@ public class Cat extends TamableAnimal implements VariantHolder { + return Mth.lerp(tickDelta, this.relaxStateOneAmountO, this.relaxStateOneAmount); + } + ++ // Purpur start ++ @Override ++ public void tame(Player player) { ++ setCollarColor(level.purpurConfig.catDefaultCollarColor); ++ super.tame(player); ++ } ++ // Purpur end ++ + @Nullable + @Override + public Cat getBreedOffspring(ServerLevel world, AgeableMob entity) { +@@ -376,6 +431,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (getRider() != null) return InteractionResult.PASS; // Purpur + ItemStack itemstack = player.getItemInHand(hand); + Item item = itemstack.getItem(); + +@@ -422,7 +478,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + } + } else if (this.isFood(itemstack)) { + this.usePlayerItem(player, hand, itemstack); +- if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit ++ if ((this.level.purpurConfig.alwaysTameInCreative && player.getAbilities().instabuild) || (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled())) { // CraftBukkit // Purpur + this.tame(player); + this.setOrderedToSit(true); + this.level.broadcastEntityEvent(this, (byte) 7); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Chicken.java b/src/main/java/net/minecraft/world/entity/animal/Chicken.java +index 72edd4d0698dd18cf2d91c39d68d3b3302d86d62..3355a5b2e906c247ef7af4e1a2c74d49fb050616 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Chicken.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Chicken.java +@@ -54,16 +54,65 @@ public class Chicken extends Animal { + this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.chickenRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.chickenRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.chickenControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.chickenMaxHealth); ++ if (level.purpurConfig.chickenRetaliate) { ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(2.0D); ++ } ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.chickenBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.chickenTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.chickenAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); +- this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ // this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); // Purpur - moved down + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.0D, Chicken.FOOD_ITEMS, false)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.1D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ // Purpur start ++ if (level.purpurConfig.chickenRetaliate) { ++ this.goalSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.MeleeAttackGoal(this, 1.0D, false)); ++ this.targetSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal(this)); ++ } else { ++ this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); ++ } ++ // Purpur end + } + + @Override +@@ -72,7 +121,7 @@ public class Chicken extends Animal { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 4.0D).add(Attributes.MOVEMENT_SPEED, 0.25D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 4.0D).add(Attributes.MOVEMENT_SPEED, 0.25D).add(Attributes.ATTACK_DAMAGE, 0.0D); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cod.java b/src/main/java/net/minecraft/world/entity/animal/Cod.java +index 824e5e4fe7619ae46061c3c978c9a044db8c84ab..de70208403ef6c6c9c82ca4c1fd3b641a40bb45c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cod.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cod.java +@@ -13,6 +13,38 @@ public class Cod extends AbstractSchoolingFish { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.codRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.codControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.codMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.codTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.codAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public ItemStack getBucketItemStack() { + return new ItemStack(Items.COD_BUCKET); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cow.java b/src/main/java/net/minecraft/world/entity/animal/Cow.java +index abae850f5babfd75c7547e88fb7637e9775991d3..35b97e48b19fad137cab03e3599e4c81101eb87a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cow.java +@@ -2,6 +2,7 @@ package net.minecraft.world.entity.animal; + + import javax.annotation.Nullable; + import net.minecraft.core.BlockPos; ++import net.minecraft.core.particles.ParticleTypes; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.sounds.SoundEvent; + import net.minecraft.sounds.SoundEvents; +@@ -29,6 +30,7 @@ import net.minecraft.world.item.ItemUtils; + import net.minecraft.world.item.Items; + import net.minecraft.world.item.crafting.Ingredient; + import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; + // CraftBukkit start + import org.bukkit.craftbukkit.event.CraftEventFactory; +@@ -36,25 +38,74 @@ import org.bukkit.craftbukkit.inventory.CraftItemStack; + // CraftBukkit end + + public class Cow extends Animal { ++ private boolean isNaturallyAggressiveToPlayers; // Purpur + + public Cow(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.cowRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.cowRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.cowControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.cowMaxHealth); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(this.level.purpurConfig.cowNaturallyAggressiveToPlayersDamage); // Purpur ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.cowBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.cowTakeDamageFromWater; ++ } ++ ++ @Override ++ public net.minecraft.world.entity.SpawnGroupData finalizeSpawn(net.minecraft.world.level.ServerLevelAccessor world, net.minecraft.world.DifficultyInstance difficulty, net.minecraft.world.entity.MobSpawnType spawnReason, net.minecraft.world.entity.SpawnGroupData entityData, net.minecraft.nbt.CompoundTag entityNbt) { ++ this.isNaturallyAggressiveToPlayers = world.getLevel().purpurConfig.cowNaturallyAggressiveToPlayersChance > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.cowNaturallyAggressiveToPlayersChance; ++ return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.cowAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D)); ++ this.goalSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); ++ if (level.purpurConfig.cowFeedMushrooms > 0) this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.of(Items.WHEAT, Blocks.RED_MUSHROOM.asItem(), Blocks.BROWN_MUSHROOM.asItem()), false)); else // Purpur + this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.of(Items.WHEAT), false)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, target -> isNaturallyAggressiveToPlayers)); // Purpur + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.MOVEMENT_SPEED, 0.20000000298023224D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.MOVEMENT_SPEED, 0.20000000298023224D).add(Attributes.ATTACK_DAMAGE, 0.0D); // Purpur + } + + @Override +@@ -84,6 +135,7 @@ public class Cow extends Animal { + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (getRider() != null) return InteractionResult.PASS; // Purpur + ItemStack itemstack = player.getItemInHand(hand); + + if (itemstack.is(Items.BUCKET) && !this.isBaby()) { +@@ -91,7 +143,7 @@ public class Cow extends Animal { + org.bukkit.event.player.PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level, player, this.blockPosition(), this.blockPosition(), null, itemstack, Items.MILK_BUCKET, hand); + + if (event.isCancelled()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + // CraftBukkit end + +@@ -100,6 +152,10 @@ public class Cow extends Animal { + + player.setItemInHand(hand, itemstack1); + return InteractionResult.sidedSuccess(this.level.isClientSide); ++ // Purpur start - feed mushroom to change to mooshroom ++ } else if (level.purpurConfig.cowFeedMushrooms > 0 && this.getType() != EntityType.MOOSHROOM && isMushroom(itemstack)) { ++ return this.feedMushroom(player, itemstack); ++ // Purpur end + } else { + return super.mobInteract(player, hand); + } +@@ -115,4 +171,69 @@ public class Cow extends Animal { + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { + return this.isBaby() ? dimensions.height * 0.95F : 1.3F; + } ++ ++ // Purpur start - feed mushroom to change to mooshroom ++ private int redMushroomsFed = 0; ++ private int brownMushroomsFed = 0; ++ ++ private boolean isMushroom(ItemStack stack) { ++ return stack.getItem() == Blocks.RED_MUSHROOM.asItem() || stack.getItem() == Blocks.BROWN_MUSHROOM.asItem(); ++ } ++ ++ private int incrementFeedCount(ItemStack stack) { ++ if (stack.getItem() == Blocks.RED_MUSHROOM.asItem()) { ++ return ++redMushroomsFed; ++ } else { ++ return ++brownMushroomsFed; ++ } ++ } ++ ++ private InteractionResult feedMushroom(Player player, ItemStack stack) { ++ level.broadcastEntityEvent(this, (byte) 18); // hearts ++ playSound(SoundEvents.COW_MILK, 1.0F, 1.0F); ++ if (incrementFeedCount(stack) < level.purpurConfig.cowFeedMushrooms) { ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ return InteractionResult.CONSUME; // require 5 mushrooms to transform (prevents mushroom duping) ++ } ++ MushroomCow mooshroom = EntityType.MOOSHROOM.create(level); ++ if (mooshroom == null) { ++ return InteractionResult.PASS; ++ } ++ if (stack.getItem() == Blocks.BROWN_MUSHROOM.asItem()) { ++ mooshroom.setVariant(MushroomCow.MushroomType.BROWN); ++ } else { ++ mooshroom.setVariant(MushroomCow.MushroomType.RED); ++ } ++ mooshroom.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); ++ mooshroom.setHealth(this.getHealth()); ++ mooshroom.setAge(getAge()); ++ mooshroom.copyPosition(this); ++ mooshroom.setYBodyRot(this.yBodyRot); ++ mooshroom.setYHeadRot(this.getYHeadRot()); ++ mooshroom.yRotO = this.yRotO; ++ mooshroom.xRotO = this.xRotO; ++ if (this.hasCustomName()) { ++ mooshroom.setCustomName(this.getCustomName()); ++ } ++ if (CraftEventFactory.callEntityTransformEvent(this, mooshroom, org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION).isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ if (!new com.destroystokyo.paper.event.entity.EntityTransformedEvent(this.getBukkitEntity(), mooshroom.getBukkitEntity(), com.destroystokyo.paper.event.entity.EntityTransformedEvent.TransformedReason.INFECTED).callEvent()) { ++ return InteractionResult.PASS; ++ } ++ this.level.addFreshEntity(mooshroom); ++ this.remove(RemovalReason.DISCARDED); ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ for (int i = 0; i < 15; ++i) { ++ ((ServerLevel) level).sendParticles(((ServerLevel) level).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 2), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java +index 3f100d847fbce6db5b625e99c4f3694576237372..284c1342695aeb652f39c236d14538647465846e 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java +@@ -78,19 +78,109 @@ public class Dolphin extends WaterAnimal { + public static final Predicate ALLOWED_ITEMS = (entityitem) -> { + return !entityitem.hasPickUpDelay() && entityitem.isAlive() && entityitem.isInWater(); + }; ++ private int spitCooldown; // Purpur ++ private boolean isNaturallyAggressiveToPlayers; // Purpur + + public Dolphin(EntityType type, Level world) { + super(type, world); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ class DolphinMoveControl extends SmoothSwimmingMoveControl { ++ private final org.purpurmc.purpur.controller.WaterMoveControllerWASD waterMoveControllerWASD; ++ private final Dolphin dolphin; ++ ++ public DolphinMoveControl(Dolphin dolphin, int pitchChange, int yawChange, float speedInWater, float speedInAir, boolean buoyant) { ++ super(dolphin, pitchChange, yawChange, speedInWater, speedInAir, buoyant); ++ this.dolphin = dolphin; ++ this.waterMoveControllerWASD = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(dolphin); ++ } ++ ++ @Override ++ public void tick() { ++ if (dolphin.getRider() != null && dolphin.isControllable()) { ++ purpurTick(dolphin.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ ++ public void purpurTick(Player rider) { ++ if (dolphin.getAirSupply() < 150) { ++ // if drowning override player WASD controls to find air ++ super.tick(); ++ } else { ++ waterMoveControllerWASD.purpurTick(rider); ++ dolphin.setDeltaMovement(dolphin.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ } ++ } ++ }; ++ this.moveControl = new DolphinMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur end + this.lookControl = new SmoothSwimmingLookControl(this, 10); + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.dolphinRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.dolphinControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (spitCooldown == 0 && getRider() != null) { ++ spitCooldown = level.purpurConfig.dolphinSpitCooldown; ++ ++ org.bukkit.craftbukkit.entity.CraftPlayer player = (org.bukkit.craftbukkit.entity.CraftPlayer) getRider().getBukkitEntity(); ++ if (!player.hasPermission("allow.special.dolphin")) { ++ return false; ++ } ++ ++ org.bukkit.Location loc = player.getEyeLocation(); ++ loc.setPitch(loc.getPitch() - 10); ++ org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(10).add(loc.toVector()); ++ ++ org.purpurmc.purpur.entity.DolphinSpit spit = new org.purpurmc.purpur.entity.DolphinSpit(level, this); ++ spit.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), level.purpurConfig.dolphinSpitSpeed, 5.0F); ++ ++ level.addFreshEntity(spit); ++ playSound(SoundEvents.DOLPHIN_ATTACK, 1.0F, 1.0F + (random.nextFloat() - random.nextFloat()) * 0.2F); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.dolphinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.dolphinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.dolphinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Nullable + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { + this.setAirSupply(this.getMaxAirSupply()); + this.setXRot(0.0F); ++ this.isNaturallyAggressiveToPlayers = world.getLevel().purpurConfig.dolphinNaturallyAggressiveToPlayersChance > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.dolphinNaturallyAggressiveToPlayersChance; // Purpur + return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); + } + +@@ -160,17 +250,21 @@ public class Dolphin extends WaterAnimal { + protected void registerGoals() { + this.goalSelector.addGoal(0, new BreathAirGoal(this)); + this.goalSelector.addGoal(0, new TryFindWaterGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur + this.goalSelector.addGoal(1, new Dolphin.DolphinSwimToTreasureGoal(this)); + this.goalSelector.addGoal(2, new Dolphin.DolphinSwimWithPlayerGoal(this, 4.0D)); + this.goalSelector.addGoal(4, new RandomSwimmingGoal(this, 1.0D, 10)); + this.goalSelector.addGoal(4, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(5, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(5, new DolphinJumpGoal(this, 10)); +- this.goalSelector.addGoal(6, new MeleeAttackGoal(this, 1.2000000476837158D, true)); ++ //this.goalSelector.addGoal(6, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur - moved up + this.goalSelector.addGoal(8, new Dolphin.PlayWithItemsGoal()); + this.goalSelector.addGoal(8, new FollowBoatGoal(this)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, Guardian.class, 8.0F, 1.0D, 1.0D)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Guardian.class})).setAlertOthers()); ++ this.targetSelector.addGoal(2, new net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, target -> isNaturallyAggressiveToPlayers)); // Purpur + } + + public static AttributeSupplier.Builder createAttributes() { +@@ -221,7 +315,7 @@ public class Dolphin extends WaterAnimal { + + @Override + protected boolean canRide(Entity entity) { +- return true; ++ return boardingCooldown <= 0; // Purpur - make dolphin honor ride cooldown like all other non-boss mobs; + } + + @Override +@@ -256,6 +350,11 @@ public class Dolphin extends WaterAnimal { + @Override + public void tick() { + super.tick(); ++ // Purpur start ++ if (spitCooldown > 0) { ++ spitCooldown--; ++ } ++ // Purpur end + if (this.isNoAi()) { + this.setAirSupply(this.getMaxAirSupply()); + } else { +@@ -401,6 +500,7 @@ public class Dolphin extends WaterAnimal { + + @Override + public boolean canUse() { ++ if (this.dolphin.level.purpurConfig.dolphinDisableTreasureSearching) return false; // Purpur + return this.dolphin.gotFish() && this.dolphin.getAirSupply() >= 100 && this.dolphin.level.getWorld().canGenerateStructures(); // MC-151364, SPIGOT-5494: hangs if generate-structures=false + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Fox.java b/src/main/java/net/minecraft/world/entity/animal/Fox.java +index bc2b98c9f34ad2b289f5da91d704bd836edec8c1..31512fb943690ac82c995bcbb3ffd63225b2c46c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Fox.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Fox.java +@@ -34,6 +34,7 @@ import net.minecraft.util.RandomSource; + import net.minecraft.util.StringRepresentable; + import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.entity.AgeableMob; + import net.minecraft.world.entity.Entity; +@@ -87,6 +88,7 @@ import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.CaveVines; + import net.minecraft.world.level.block.SweetBerryBushBlock; + import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.gameevent.GameEvent; + import net.minecraft.world.level.pathfinder.BlockPathTypes; + import net.minecraft.world.phys.Vec3; + +@@ -140,6 +142,64 @@ public class Fox extends Animal implements VariantHolder { + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.foxRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.foxRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.foxControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return getRider() != null && this.isControllable() ? 0.5F : super.getJumpPower(); ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setCanPickUpLoot(false); ++ clearStates(); ++ setIsPouncing(false); ++ spitOutItem(getItemBySlot(EquipmentSlot.MAINHAND)); ++ setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); ++ } ++ ++ @Override ++ public void onDismount(Player rider) { ++ super.onDismount(rider); ++ setCanPickUpLoot(true); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.foxMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.foxBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.foxTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.foxAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -159,6 +219,7 @@ public class Fox extends Animal implements VariantHolder { + return entityliving instanceof AbstractSchoolingFish; + }); + this.goalSelector.addGoal(0, new Fox.FoxFloatGoal()); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new ClimbOnTopOfPowderSnowGoal(this, this.level)); + this.goalSelector.addGoal(1, new Fox.FaceplantGoal()); + this.goalSelector.addGoal(2, new Fox.FoxPanicGoal(2.2D)); +@@ -185,6 +246,7 @@ public class Fox extends Animal implements VariantHolder { + this.goalSelector.addGoal(11, new Fox.FoxSearchForItemsGoal()); + this.goalSelector.addGoal(12, new Fox.FoxLookAtPlayerGoal(this, Player.class, 24.0F)); + this.goalSelector.addGoal(13, new Fox.PerchAndSearchGoal()); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(3, new Fox.DefendTrustedTargetGoal(LivingEntity.class, false, false, (entityliving) -> { + return Fox.TRUSTED_TARGET_SELECTOR.test(entityliving) && !this.trusts(entityliving.getUUID()); + })); +@@ -341,6 +403,11 @@ public class Fox extends Animal implements VariantHolder { + } + + private void setTargetGoals() { ++ // Purpur start - do not add duplicate goals ++ this.targetSelector.removeGoal(this.landTargetGoal); ++ this.targetSelector.removeGoal(this.turtleEggTargetGoal); ++ this.targetSelector.removeGoal(this.fishTargetGoal); ++ // Purpur end + if (this.getVariant() == Fox.Type.RED) { + this.targetSelector.addGoal(4, this.landTargetGoal); + this.targetSelector.addGoal(4, this.turtleEggTargetGoal); +@@ -374,6 +441,7 @@ public class Fox extends Animal implements VariantHolder { + + public void setVariant(Fox.Type variant) { + this.entityData.set(Fox.DATA_TYPE_ID, variant.getId()); ++ this.setTargetGoals(); // Purpur - fix API bug not updating pathfinders on type change + } + + List getTrustedUUIDs() { +@@ -710,6 +778,29 @@ public class Fox extends Animal implements VariantHolder { + return this.getTrustedUUIDs().contains(uuid); + } + ++ // Purpur start ++ @Override ++ public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (level.purpurConfig.foxTypeChangesWithTulips) { ++ ItemStack itemstack = player.getItemInHand(hand); ++ if (getVariant() == Type.RED && itemstack.getItem() == Items.WHITE_TULIP) { ++ setVariant(Type.SNOW); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ } else if (getVariant() == Type.SNOW && itemstack.getItem() == Items.ORANGE_TULIP) { ++ setVariant(Type.RED); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ } ++ return super.mobInteract(player, hand); ++ } ++ // Purpur end ++ + @Override + // Paper start - Cancellable death event + protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(DamageSource source) { +@@ -757,16 +848,16 @@ public class Fox extends Animal implements VariantHolder { + return new Vec3(0.0D, (double) (0.55F * this.getEyeHeight()), (double) (this.getBbWidth() * 0.4F)); + } + +- public class FoxLookControl extends LookControl { ++ public class FoxLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + public FoxLookControl() { + super(Fox.this); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (!Fox.this.isSleeping()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +@@ -777,16 +868,16 @@ public class Fox extends Animal implements VariantHolder { + } + } + +- private class FoxMoveControl extends MoveControl { ++ private class FoxMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + public FoxMoveControl() { + super(Fox.this); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (Fox.this.canMove()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +@@ -904,8 +995,10 @@ public class Fox extends Animal implements VariantHolder { + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer2, this.animal, this.partner, entityfox); + } + +- this.animal.setAge(6000); +- this.partner.setAge(6000); ++ // Purpur start ++ this.animal.setAge(this.animal.getPurpurBreedTime()); ++ this.partner.setAge(this.partner.getPurpurBreedTime()); ++ // Purpur end + this.animal.resetLove(); + this.partner.resetLove(); + worldserver.addFreshEntityWithPassengers(entityfox, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason +@@ -1293,7 +1386,7 @@ public class Fox extends Animal implements VariantHolder { + } + + protected void onReachedTarget() { +- if (Fox.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (Fox.this.level.purpurConfig.foxBypassMobGriefing || Fox.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + BlockState iblockdata = Fox.this.level.getBlockState(this.blockPos); + + if (iblockdata.is(Blocks.SWEET_BERRY_BUSH)) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +index e73acfa2f5a4066fa1beee1758082a2fe97a43b3..0292690b9c99f66210a03817e512c65ca65bc749 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +@@ -63,14 +63,59 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + private int remainingPersistentAngerTime; + @Nullable + private UUID persistentAngerTarget; ++ @Nullable private UUID summoner; // Purpur + + public IronGolem(EntityType type, Level world) { + super(type, world); + this.maxUpStep = 1.0F; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.ironGolemRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.ironGolemRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.ironGolemControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.ironGolemMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.ironGolemTakeDamageFromWater; ++ } ++ ++ @Nullable ++ public UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.ironGolemAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ if (level.purpurConfig.ironGolemCanSwim) this.goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ if (this.level.purpurConfig.ironGolemPoppyCalm) this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.ReceiveFlower(this)); // Purpur + this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(2, new MoveTowardsTargetGoal(this, 0.9D, 32.0F)); + this.goalSelector.addGoal(2, new MoveBackToVillageGoal(this, 0.6D, false)); +@@ -78,6 +123,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + this.goalSelector.addGoal(5, new OfferFlowerGoal(this)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new DefendVillageTargetGoal(this)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +@@ -148,6 +194,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("PlayerCreated", this.isPlayerCreated()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + this.addPersistentAngerSaveData(nbt); + } + +@@ -155,6 +202,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.setPlayerCreated(nbt.getBoolean("PlayerCreated")); ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + this.readPersistentAngerSaveData(this.level, nbt); + } + +@@ -279,13 +327,13 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + ItemStack itemstack = player.getItemInHand(hand); + + if (!itemstack.is(Items.IRON_INGOT)) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else { + float f = this.getHealth(); + + this.heal(25.0F); + if (this.getHealth() == f) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else { + float f1 = 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F; + +@@ -294,6 +342,8 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + itemstack.shrink(1); + } + ++ if (this.level.purpurConfig.ironGolemHealCalm && isAngry() && getHealth() == getMaxHealth()) stopBeingAngry(); // Purpur ++ + return InteractionResult.sidedSuccess(this.level.isClientSide); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +index 68a5ee85e64802e4509ba0d184fc0ceb3cbe2d11..046e851a5c49e58fa4e84d398ffbe11baa9c4072 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +@@ -63,6 +63,43 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder { +@@ -145,7 +182,7 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder> optional = this.getEffectFromItemStack(itemstack); + + if (!optional.isPresent()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + Pair pair = (Pair) optional.get(); +@@ -170,7 +207,7 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder(this, Chicken.class, false)); + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, false, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java +index 9c1e02c3a990cd0f8bba1c84c170b438278c02a7..841838562ffed67127b03e27f61d692d9933fbe3 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Panda.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java +@@ -108,6 +108,53 @@ public class Panda extends Animal { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.pandaRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.pandaRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.pandaControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setForwardMot(0.0F); ++ sit(false); ++ eat(false); ++ setOnBack(false); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.pandaMaxHealth); ++ setAttributes(); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.pandaBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.pandaTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.pandaAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean canTakeItem(ItemStack stack) { + EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(stack); +@@ -269,6 +316,7 @@ public class Panda extends Animal { + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new Panda.PandaPanicGoal(this, 2.0D)); + this.goalSelector.addGoal(2, new Panda.PandaBreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new Panda.PandaAttackGoal(this, 1.2000000476837158D, true)); +@@ -284,6 +332,7 @@ public class Panda extends Animal { + this.goalSelector.addGoal(12, new Panda.PandaRollGoal(this)); + this.goalSelector.addGoal(13, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(14, new WaterAvoidingRandomStrollGoal(this, 1.0D)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new Panda.PandaHurtByTargetGoal(this, new Class[0])).setAlertOthers(new Class[0])); + } + +@@ -607,7 +656,10 @@ public class Panda extends Animal { + + public void setAttributes() { + if (this.isWeak()) { +- this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(10.0D); ++ // Purpur start ++ net.minecraft.world.entity.ai.attributes.AttributeInstance maxHealth = this.getAttribute(Attributes.MAX_HEALTH); ++ maxHealth.setBaseValue(maxHealth.getValue() / 2); ++ // Purpur end + } + + if (this.isLazy()) { +@@ -630,7 +682,7 @@ public class Panda extends Animal { + ItemStack itemstack = player.getItemInHand(hand); + + if (this.isScared()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else if (this.isOnBack()) { + this.setOnBack(false); + return InteractionResult.sidedSuccess(this.level.isClientSide); +@@ -647,7 +699,7 @@ public class Panda extends Animal { + this.setInLove(player); + } else { + if (this.level.isClientSide || this.isSitting() || this.isInWater()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + this.tryToSit(); +@@ -666,7 +718,7 @@ public class Panda extends Animal { + + return InteractionResult.SUCCESS; + } else { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + } + +@@ -706,7 +758,7 @@ public class Panda extends Animal { + return !this.isOnBack() && !this.isScared() && !this.isEating() && !this.isRolling() && !this.isSitting(); + } + +- private static class PandaMoveControl extends MoveControl { ++ private static class PandaMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Panda panda; + +@@ -716,9 +768,9 @@ public class Panda extends Animal { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.panda.canPerformAction()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Parrot.java b/src/main/java/net/minecraft/world/entity/animal/Parrot.java +index 2d9aa961df034eab21ecfdb6e6d0ce7cf013505d..f11bca0b0c556aa4d6c32c503c4b5f45c645a3fa 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Parrot.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Parrot.java +@@ -129,12 +129,88 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, Level world) { + super(type, world); +- this.moveControl = new FlyingMoveControl(this, 10, false); ++ // Purpur start ++ final org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD flyingController = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); ++ class ParrotMoveControl extends FlyingMoveControl { ++ public ParrotMoveControl(Mob entity, int maxPitchChange, boolean noGravity) { ++ super(entity, maxPitchChange, noGravity); ++ } ++ ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ flyingController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ ++ @Override ++ public boolean hasWanted() { ++ return mob.getRider() != null && mob.isControllable() ? getForwardMot() != 0 || getStrafeMot() != 0 : super.hasWanted(); ++ } ++ } ++ this.moveControl = new ParrotMoveControl(this, 10, false); ++ // Purpur end + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, -1.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, -1.0F); + this.setPathfindingMalus(BlockPathTypes.COCOA, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.parrotRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.parrotRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.parrotControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.parrotMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.25, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.parrotMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return 6000; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.parrotTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.parrotAlwaysDropExp; ++ } ++ // Purpur end ++ + @Nullable + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { +@@ -153,8 +229,11 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { +@@ -311,13 +391,13 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.polarBearRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.polarBearRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.polarBearControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (!isStanding()) { ++ if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0) { ++ setStanding(true); ++ playSound(SoundEvents.POLAR_BEAR_WARNING, 1.0F, 1.0F); ++ } ++ } ++ return false; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.polarBearMaxHealth); ++ } ++ ++ public boolean canMate(Animal other) { ++ if (other == this) { ++ return false; ++ } else if (this.isStanding()) { ++ return false; ++ } else if (this.getTarget() != null) { ++ return false; ++ } else if (!(other instanceof PolarBear)) { ++ return false; ++ } else { ++ PolarBear bear = (PolarBear) other; ++ if (bear.isStanding()) { ++ return false; ++ } ++ if (bear.getTarget() != null) { ++ return false; ++ } ++ return this.isInLove() && bear.isInLove(); ++ } ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.polarBearBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.polarBearTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.polarBearAlwaysDropExp; ++ } ++ // Purpur end ++ + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel world, AgeableMob entity) { +@@ -73,19 +143,27 @@ public class PolarBear extends Animal implements NeutralMob { + + @Override + public boolean isFood(ItemStack stack) { +- return false; ++ return level.purpurConfig.polarBearBreedableItem != null && stack.getItem() == level.purpurConfig.polarBearBreedableItem; // Purpur + } + + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PolarBear.PolarBearMeleeAttackGoal()); + this.goalSelector.addGoal(1, new PolarBear.PolarBearPanicGoal()); ++ // Purpur start ++ if (level.purpurConfig.polarBearBreedableItem != null) { ++ this.goalSelector.addGoal(2, new net.minecraft.world.entity.ai.goal.BreedGoal(this, 1.0D)); ++ this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, net.minecraft.world.item.crafting.Ingredient.of(level.purpurConfig.polarBearBreedableItem), false)); ++ } ++ // Purpur end + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(5, new RandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new PolarBear.PolarBearHurtByTargetGoal()); + this.targetSelector.addGoal(2, new PolarBear.PolarBearAttackPlayersGoal()); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +@@ -202,6 +280,11 @@ public class PolarBear extends Animal implements NeutralMob { + this.updatePersistentAnger((ServerLevel)this.level, true); + } + ++ // Purpur start ++ if (isStanding() && --standTimer <= 0) { ++ setStanding(false); ++ } ++ // Purpur end + } + + @Override +@@ -231,6 +314,7 @@ public class PolarBear extends Animal implements NeutralMob { + + public void setStanding(boolean warning) { + this.entityData.set(DATA_STANDING_ID, warning); ++ standTimer = warning ? 20 : -1; // Purpur + } + + public float getStandingAnimationScale(float tickDelta) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java +index ce02552c1b3c62cf9f48425838a129a3ec40a049..bf9e6b6ca2b2bf8b2a2e96d10cd4fda9c59df1b8 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java +@@ -45,6 +45,38 @@ public class Pufferfish extends AbstractFish { + this.refreshDimensions(); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.pufferfishRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.pufferfishControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.pufferfishMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.pufferfishTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.pufferfishAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +index 7a1fcae6de2dd8247fcb1f1612122edf8f56965a..f158cb5a8a04b9d9fa00c35774d2104a8d4e6416 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +@@ -83,6 +83,7 @@ public class Rabbit extends Animal implements VariantHolder { + private boolean wasOnGround; + private int jumpDelayTicks; + public int moreCarrotTicks; ++ private boolean actualJump; // Purpur + + public Rabbit(EntityType type, Level world) { + super(type, world); +@@ -91,6 +92,71 @@ public class Rabbit extends Animal implements VariantHolder { + this.initializePathFinderGoals(); // CraftBukkit - moved code + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.rabbitRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.rabbitRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.rabbitControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (onGround) { ++ actualJump = true; ++ jumpFromGround(); ++ actualJump = false; ++ } ++ return true; ++ } ++ ++ private void handleJumping() { ++ if (onGround) { ++ RabbitJumpControl jumpController = (RabbitJumpControl) jumpControl; ++ if (!wasOnGround) { ++ setJumping(false); ++ jumpController.setCanJump(false); ++ } ++ if (!jumpController.wantJump()) { ++ if (moveControl.hasWanted()) { ++ startJumping(); ++ } ++ } else if (!jumpController.canJump()) { ++ jumpController.setCanJump(true); ++ } ++ } ++ wasOnGround = onGround; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.rabbitMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.rabbitBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.rabbitTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.rabbitAlwaysDropExp; ++ } ++ // Purpur end ++ + // CraftBukkit start - code from constructor + public void initializePathFinderGoals(){ + this.setSpeedModifier(0.0D); +@@ -100,6 +166,7 @@ public class Rabbit extends Animal implements VariantHolder { + @Override + public void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new ClimbOnTopOfPowderSnowGoal(this, this.level)); + this.goalSelector.addGoal(1, new Rabbit.RabbitPanicGoal(this, 2.2D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 0.8D)); +@@ -114,6 +181,13 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + protected float getJumpPower() { ++ if (getRider() != null && this.isControllable()) { ++ if (getForwardMot() < 0) { ++ setSpeed(getForwardMot() * 2F); ++ } ++ return actualJump ? 0.5F : 0.3F; ++ } ++ // Purpur end + if (!this.horizontalCollision && (!this.moveControl.hasWanted() || this.moveControl.getWantedY() <= this.getY() + 0.5D)) { + Path pathentity = this.navigation.getPath(); + +@@ -132,7 +206,7 @@ public class Rabbit extends Animal implements VariantHolder { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + super.jumpFromGround(); + double d0 = this.moveControl.getSpeedModifier(); + +@@ -182,6 +256,13 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + public void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ handleJumping(); ++ return; ++ } ++ // Purpur end ++ + if (this.jumpDelayTicks > 0) { + --this.jumpDelayTicks; + } +@@ -399,10 +480,23 @@ public class Rabbit extends Animal implements VariantHolder { + } + + this.setVariant(entityrabbit_variant); ++ ++ // Purpur start ++ if (entityrabbit_variant != Variant.EVIL && world.getLevel().purpurConfig.rabbitNaturalToast > 0D && random.nextDouble() <= world.getLevel().purpurConfig.rabbitNaturalToast) { ++ setCustomName(Component.translatable("Toast")); ++ } ++ // Purpur end ++ + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData, entityNbt); + } + + private static Rabbit.Variant getRandomRabbitVariant(LevelAccessor world, BlockPos pos) { ++ // Purpur start ++ Level level = world.getMinecraftWorld(); ++ if (level.purpurConfig.rabbitNaturalKiller > 0D && world.getRandom().nextDouble() <= level.purpurConfig.rabbitNaturalKiller) { ++ return Rabbit.Variant.EVIL; ++ } ++ // Purpur end + Holder holder = world.getBiome(pos); + int i = world.getRandom().nextInt(100); + +@@ -466,7 +560,7 @@ public class Rabbit extends Animal implements VariantHolder { + } + } + +- private static class RabbitMoveControl extends MoveControl { ++ private static class RabbitMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Rabbit rabbit; + private double nextJumpSpeed; +@@ -477,14 +571,14 @@ public class Rabbit extends Animal implements VariantHolder { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.rabbit.onGround && !this.rabbit.jumping && !((Rabbit.RabbitJumpControl) this.rabbit.jumpControl).wantJump()) { + this.rabbit.setSpeedModifier(0.0D); + } else if (this.hasWanted()) { + this.rabbit.setSpeedModifier(this.nextJumpSpeed); + } + +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + @Override +@@ -546,7 +640,7 @@ public class Rabbit extends Animal implements VariantHolder { + @Override + public boolean canUse() { + if (this.nextStartTick <= 0) { +- if (!this.rabbit.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.rabbit.level.purpurConfig.rabbitBypassMobGriefing && !this.rabbit.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Salmon.java b/src/main/java/net/minecraft/world/entity/animal/Salmon.java +index 0af79daa357f53a8871e293b57e16c099e5d3f64..bd1e964c7899a54a2c39afe0691a7573cfe35fc1 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Salmon.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Salmon.java +@@ -13,6 +13,38 @@ public class Salmon extends AbstractSchoolingFish { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.salmonRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.salmonControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.salmonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.salmonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.salmonAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public int getMaxSchoolSize() { + return 5; +diff --git a/src/main/java/net/minecraft/world/entity/animal/Sheep.java b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +index efac4395fdb79a78fbb18a0f828b1a3c90b102fe..017507e7201ffc4a9486f5fb9edc9dac18e989d7 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Sheep.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +@@ -116,10 +116,48 @@ public class Sheep extends Animal implements Shearable { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.sheepRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.sheepRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.sheepControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.sheepMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.sheepBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.sheepTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.sheepAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.eatBlockGoal = new EatBlockGoal(this); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.25D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.1D, Ingredient.of(Items.WHEAT), false)); +@@ -254,7 +292,7 @@ public class Sheep extends Animal implements Shearable { + return InteractionResult.PASS; + } + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + this.gameEvent(GameEvent.SHEAR, player); + itemstack.hurtAndBreak(1, player, (entityhuman1) -> { + entityhuman1.broadcastBreakEvent(hand); +@@ -269,14 +307,15 @@ public class Sheep extends Animal implements Shearable { + } + + @Override +- public void shear(SoundSource shearedSoundCategory) { ++ public void shear(SoundSource shearedSoundCategory, int looting) { // Purpur + this.level.playSound((Player) null, (Entity) this, SoundEvents.SHEEP_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + this.setSheared(true); + int i = 1 + this.random.nextInt(3); ++ if (org.purpurmc.purpur.PurpurConfig.allowShearsLooting) i += looting; // Purpur + + for (int j = 0; j < i; ++j) { + this.forceDrops = true; // CraftBukkit +- ItemEntity entityitem = this.spawnAtLocation((ItemLike) Sheep.ITEM_BY_DYE.get(this.getColor()), 1); ++ ItemEntity entityitem = this.spawnAtLocation((ItemLike) Sheep.ITEM_BY_DYE.get(this.level.purpurConfig.sheepShearJebRandomColor && hasCustomName() && getCustomName().getString().equals("jeb_") ? DyeColor.random(this.level.random) : this.getColor()), 1); // Purpur + this.forceDrops = false; // CraftBukkit + + if (entityitem != null) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +index 35e53663e4a6c4d56ec4577d08e7b040cc0c720f..00d86c57fabbb464a156dfaceadccd978f0d149c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +@@ -49,17 +49,56 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + private static final EntityDataAccessor DATA_PUMPKIN_ID = SynchedEntityData.defineId(SnowGolem.class, EntityDataSerializers.BYTE); + private static final byte PUMPKIN_FLAG = 16; + private static final float EYE_HEIGHT = 1.7F; ++ @Nullable private java.util.UUID summoner; // Purpur + + public SnowGolem(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.snowGolemRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.snowGolemRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.snowGolemControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.snowGolemMaxHealth); ++ } ++ ++ @Nullable ++ public java.util.UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable java.util.UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.snowGolemAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { +- this.goalSelector.addGoal(1, new RangedAttackGoal(this, 1.25D, 20, 10.0F)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.goalSelector.addGoal(1, new RangedAttackGoal(this, level.purpurConfig.snowGolemAttackDistance, level.purpurConfig.snowGolemSnowBallMin, level.purpurConfig.snowGolemSnowBallMax, level.purpurConfig.snowGolemSnowBallModifier)); // Purpur + this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D, 1.0000001E-5F)); + this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(4, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Mob.class, 10, true, false, (entityliving) -> { + return entityliving instanceof Enemy; + })); +@@ -79,6 +118,7 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("Pumpkin", this.hasPumpkin()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + } + + @Override +@@ -87,12 +127,13 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + if (nbt.contains("Pumpkin")) { + this.setPumpkin(nbt.getBoolean("Pumpkin")); + } ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + + } + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.snowGolemTakeDamageFromWater; // Purpur + } + + @Override +@@ -109,10 +150,11 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + this.hurt(CraftEventFactory.MELTING, 1.0F); // CraftBukkit - DamageSource.BURN -> CraftEventFactory.MELTING + } + +- if (!this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.level.purpurConfig.snowGolemBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return; + } + ++ if (getRider() != null && this.isControllable() && !level.purpurConfig.snowGolemLeaveTrailWhenRidden) return; // Purpur - don't leave snow trail when being ridden + BlockState iblockdata = Blocks.SNOW.defaultBlockState(); + + for (int l = 0; l < 4; ++l) { +@@ -160,10 +202,10 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + if (itemstack.is(Items.SHEARS) && this.readyForShearing()) { + // CraftBukkit start + if (!CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand)) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + this.gameEvent(GameEvent.SHEAR, player); + if (!this.level.isClientSide) { + itemstack.hurtAndBreak(1, player, (entityhuman1) -> { +@@ -172,17 +214,27 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + } + + return InteractionResult.sidedSuccess(this.level.isClientSide); ++ // Purpur start ++ } else if (level.purpurConfig.snowGolemPutPumpkinBack && !hasPumpkin() && itemstack.getItem() == Blocks.CARVED_PUMPKIN.asItem()) { ++ setPumpkin(true); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ // Purpur end + } else { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + } + + @Override +- public void shear(SoundSource shearedSoundCategory) { ++ public void shear(SoundSource shearedSoundCategory, int looting) { // Purpur + this.level.playSound((Player) null, (Entity) this, SoundEvents.SNOW_GOLEM_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + if (!this.level.isClientSide()) { + this.setPumpkin(false); + this.forceDrops = true; // CraftBukkit ++ if (level.purpurConfig.snowGolemDropsPumpkin) // Purpur ++ for (int i = 0; i < 1 + (org.purpurmc.purpur.PurpurConfig.allowShearsLooting ? looting : 0); i++) // Purpur + this.spawnAtLocation(new ItemStack(Items.CARVED_PUMPKIN), 1.7F); + this.forceDrops = false; // CraftBukkit + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Squid.java b/src/main/java/net/minecraft/world/entity/animal/Squid.java +index a51424d29ac353cf1bec4d1484db0acb63bebba5..8afdb5d4fecbb45bad2ed801fc0e526d15ef07c5 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Squid.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Squid.java +@@ -46,13 +46,71 @@ public class Squid extends WaterAnimal { + + public Squid(EntityType type, Level world) { + super(type, world); +- //this.random.setSeed((long) this.getId()); // Paper - we set the random to shared, do not clobber the seed ++ if (!world.purpurConfig.entitySharedRandom) this.random.setSeed((long) this.getId()); // Paper - we set the random to shared, do not clobber the seed // Purpur + this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.squidRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.squidControllable; ++ } ++ ++ protected void rotateVectorAroundY(org.bukkit.util.Vector vector, double degrees) { ++ double rad = Math.toRadians(degrees); ++ double cos = Math.cos(rad); ++ double sine = Math.sin(rad); ++ double x = vector.getX(); ++ double z = vector.getZ(); ++ vector.setX(cos * x - sine * z); ++ vector.setZ(sine * x + cos * z); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.squidMaxHealth); ++ } ++ ++ @Override ++ public net.minecraft.world.phys.AABB getAxisForFluidCheck() { ++ // Stops squids from floating just over the water ++ return super.getAxisForFluidCheck().offsetY(level.purpurConfig.squidOffsetWaterCheck); ++ } ++ ++ public boolean canFly() { ++ return this.level.purpurConfig.squidsCanFly; ++ } ++ ++ @Override ++ public boolean isInWater() { ++ return this.wasTouchingWater || canFly(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.squidTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.squidAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new Squid.SquidRandomMovementGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Squid.SquidFleeGoal()); + } + +@@ -121,6 +179,7 @@ public class Squid extends WaterAnimal { + } + + if (this.isInWaterOrBubble()) { ++ if (canFly()) setNoGravity(!wasTouchingWater); // Purpur + if (this.tentacleMovement < 3.1415927F) { + float f = this.tentacleMovement / 3.1415927F; + +@@ -244,11 +303,43 @@ public class Squid extends WaterAnimal { + + @Override + public void tick() { ++ // Purpur start ++ Player rider = squid.getRider(); ++ if (rider != null && squid.isControllable()) { ++ if (rider.jumping) { ++ squid.onSpacebar(); ++ } ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot(); ++ float speed = (float) squid.getAttributeValue(Attributes.MOVEMENT_SPEED) * 10F; ++ if (forward < 0.0F) { ++ speed *= -0.5; ++ } ++ org.bukkit.util.Vector dir = rider.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(speed / 20.0F); ++ if (strafe != 0.0F) { ++ if (forward == 0.0F) { ++ dir.setY(0); ++ rotateVectorAroundY(dir, strafe > 0.0F ? -90 : 90); ++ } else if (forward < 0.0F) { ++ rotateVectorAroundY(dir, strafe > 0.0F ? 45 : -45); ++ } else { ++ rotateVectorAroundY(dir, strafe > 0.0F ? -45 : 45); ++ } ++ } ++ if (forward != 0.0F || strafe != 0.0F) { ++ squid.setMovementVector((float) dir.getX(), (float) dir.getY(), (float) dir.getZ()); ++ } else { ++ squid.setMovementVector(0.0F, 0.0F, 0.0F); ++ } ++ return; ++ } ++ // Purpur end ++ + int i = this.squid.getNoActionTime(); + + if (i > 100) { + this.squid.setMovementVector(0.0F, 0.0F, 0.0F); +- } else if (this.squid.getRandom().nextInt(reducedTickDelay(50)) == 0 || !this.squid.wasTouchingWater || !this.squid.hasMovementVector()) { ++ } else if (this.squid.getRandom().nextInt(reducedTickDelay(50)) == 0 || !this.squid.isInWater() || !this.squid.hasMovementVector()) { // Purpur + float f = this.squid.getRandom().nextFloat() * 6.2831855F; + float f1 = Mth.cos(f) * 0.2F; + float f2 = -0.1F + this.squid.getRandom().nextFloat() * 0.2F; +diff --git a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java +index b05b560b7570e97bc234b75f26233909fcf575b3..a3becf90c3309d52d2701c016d4c16970a318f9c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java +@@ -42,6 +42,38 @@ public class TropicalFish extends AbstractSchoolingFish implements VariantHolder + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.tropicalFishRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.tropicalFishControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.tropicalFishMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.tropicalFishTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.tropicalFishAlwaysDropExp; ++ } ++ // Purpur end ++ + public static String getPredefinedName(int variant) { + return "entity.minecraft.tropical_fish.predefined." + variant; + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 7f9ec1888eb9c02705426d60cf4e3aa7c6d43115..f4d1e6240c30c4a83dccd7f09691b03131b572f0 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -82,6 +82,43 @@ public class Turtle extends Animal { + this.maxUpStep = 1.0F; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.turtleRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.turtleRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.turtleControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.turtleMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.turtleBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.turtleTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.turtleAlwaysDropExp; ++ } ++ // Purpur end ++ + public void setHomePos(BlockPos pos) { + this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... + } +@@ -184,6 +221,7 @@ public class Turtle extends Animal { + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new Turtle.TurtlePanicGoal(this, 1.2D)); + this.goalSelector.addGoal(1, new Turtle.TurtleBreedGoal(this, 1.0D)); + this.goalSelector.addGoal(1, new Turtle.TurtleLayEggGoal(this, 1.0D)); +@@ -341,13 +379,15 @@ public class Turtle extends Animal { + org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit + } + +- private static class TurtleMoveControl extends MoveControl { ++ private static class TurtleMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Turtle turtle; ++ private final org.purpurmc.purpur.controller.WaterMoveControllerWASD waterController; // Purpur + + TurtleMoveControl(Turtle turtle) { + super(turtle); + this.turtle = turtle; ++ waterController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(turtle, 0.25D); // Purpur + } + + private void updateSpeed() { +@@ -366,8 +406,18 @@ public class Turtle extends Animal { + + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ if (turtle.isInWater()) { ++ waterController.purpurTick(rider); ++ } else { ++ super.purpurTick(rider); ++ } ++ } ++ // Purpur end ++ + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + this.updateSpeed(); + if (this.operation == MoveControl.Operation.MOVE_TO && !this.turtle.getNavigation().isDone()) { + double d0 = this.wantedX - this.turtle.getX(); +@@ -380,7 +430,7 @@ public class Turtle extends Animal { + + this.turtle.setYRot(this.rotlerp(this.turtle.getYRot(), f, 90.0F)); + this.turtle.yBodyRot = this.turtle.getYRot(); +- float f1 = (float) (this.speedModifier * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); + + this.turtle.setSpeed(Mth.lerp(0.125F, this.turtle.getSpeed(), f1)); + this.turtle.setDeltaMovement(this.turtle.getDeltaMovement().add(0.0D, (double) this.turtle.getSpeed() * d1 * 0.1D, 0.0D)); +diff --git a/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java b/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java +index 18389f46902bb9879ac6d734723e9a720724dc48..b2b8663a9cff08bacdab91c7bb014ba654241ada 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java +@@ -83,6 +83,6 @@ public abstract class WaterAnimal extends PathfinderMob { + i = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.maximum.or(i); + j = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.minimum.or(j); + // Paper end +- return pos.getY() >= j && pos.getY() <= i && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER); ++ return ((reason == MobSpawnType.SPAWNER && world.getMinecraftWorld().purpurConfig.spawnerFixMC238526) || (pos.getY() >= j && pos.getY() <= i)) && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER); // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Wolf.java b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +index a6a50eb4f4ac85751071571876ac804d44ee1ee6..006d5fc7c96a47bf57ab26f374143400138b8b17 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Wolf.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +@@ -10,6 +10,7 @@ import net.minecraft.network.syncher.EntityDataAccessor; + import net.minecraft.network.syncher.EntityDataSerializers; + import net.minecraft.network.syncher.SynchedEntityData; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; + import net.minecraft.sounds.SoundEvent; + import net.minecraft.sounds.SoundEvents; + import net.minecraft.tags.BlockTags; +@@ -17,9 +18,12 @@ import net.minecraft.util.Mth; + import net.minecraft.util.RandomSource; + import net.minecraft.util.TimeUtil; + import net.minecraft.util.valueproviders.UniformInt; ++import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.AgeableMob; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntityDimensions; +@@ -29,6 +33,7 @@ import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.MobSpawnType; + import net.minecraft.world.entity.NeutralMob; + import net.minecraft.world.entity.Pose; ++import net.minecraft.world.entity.SpawnGroupData; + import net.minecraft.world.entity.TamableAnimal; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; +@@ -37,6 +42,7 @@ import net.minecraft.world.entity.ai.goal.BegGoal; + import net.minecraft.world.entity.ai.goal.BreedGoal; + import net.minecraft.world.entity.ai.goal.FloatGoal; + import net.minecraft.world.entity.ai.goal.FollowOwnerGoal; ++import net.minecraft.world.entity.ai.goal.Goal; + import net.minecraft.world.entity.ai.goal.LeapAtTargetGoal; + import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; + import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +@@ -64,6 +70,7 @@ import net.minecraft.world.item.ItemStack; + import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelAccessor; ++import net.minecraft.world.level.ServerLevelAccessor; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.gameevent.GameEvent; + import net.minecraft.world.level.pathfinder.BlockPathTypes; +@@ -83,6 +90,37 @@ public class Wolf extends TamableAnimal implements NeutralMob { + + return entitytypes == EntityType.SHEEP || entitytypes == EntityType.RABBIT || entitytypes == EntityType.FOX; + }; ++ // Purpur start - rabid wolf spawn chance ++ private boolean isRabid = false; ++ private static final Predicate RABID_PREDICATE = entity -> entity instanceof ServerPlayer || entity instanceof Mob; ++ private final Goal PATHFINDER_VANILLA = new NonTameRandomTargetGoal<>(this, Animal.class, false, PREY_SELECTOR); ++ private final Goal PATHFINDER_RABID = new NonTameRandomTargetGoal<>(this, LivingEntity.class, false, RABID_PREDICATE); ++ private static final class AvoidRabidWolfGoal extends AvoidEntityGoal { ++ private final Wolf wolf; ++ ++ public AvoidRabidWolfGoal(Wolf wolf, float distance, double minSpeed, double maxSpeed) { ++ super(wolf, Wolf.class, distance, minSpeed, maxSpeed); ++ this.wolf = wolf; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && !this.wolf.isRabid() && this.toAvoid != null && this.toAvoid.isRabid(); // wolves which are not rabid run away from rabid wolves ++ } ++ ++ @Override ++ public void start() { ++ this.wolf.setTarget(null); ++ super.start(); ++ } ++ ++ @Override ++ public void tick() { ++ this.wolf.setTarget(null); ++ super.tick(); ++ } ++ } ++ // Purpur end + private static final float START_HEALTH = 8.0F; + private static final float TAME_HEALTH = 20.0F; + private float interestedAngle; +@@ -102,12 +140,93 @@ public class Wolf extends TamableAnimal implements NeutralMob { + this.setPathfindingMalus(BlockPathTypes.DANGER_POWDER_SNOW, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.wolfRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.wolfRidableInWater; ++ } ++ ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setInSittingPose(false); ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.wolfControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.wolfMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.wolfBreedingTicks; ++ } ++ ++ public boolean isRabid() { ++ return this.isRabid; ++ } ++ ++ public void setRabid(boolean isRabid) { ++ this.isRabid = isRabid; ++ updatePathfinders(true); ++ } ++ ++ public void updatePathfinders(boolean modifyEffects) { ++ this.targetSelector.removeGoal(PATHFINDER_VANILLA); ++ this.targetSelector.removeGoal(PATHFINDER_RABID); ++ if (this.isRabid) { ++ setTame(false); ++ setOwnerUUID(null); ++ this.targetSelector.addGoal(5, PATHFINDER_RABID); ++ if (modifyEffects) this.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 1200)); ++ } else { ++ this.targetSelector.addGoal(5, PATHFINDER_VANILLA); ++ this.stopBeingAngry(); ++ if (modifyEffects) this.removeEffect(MobEffects.CONFUSION); ++ } ++ } ++ ++ @Override ++ public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType type, @Nullable SpawnGroupData data, @Nullable CompoundTag nbt) { ++ this.isRabid = world.getLevel().purpurConfig.wolfNaturalRabid > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.wolfNaturalRabid; ++ this.updatePathfinders(false); ++ return super.finalizeSpawn(world, difficulty, type, data, nbt); ++ } ++ ++ @Override ++ public void tame(Player player) { ++ setCollarColor(level.purpurConfig.wolfDefaultCollarColor); ++ super.tame(player); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.wolfTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.wolfAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Wolf.WolfPanicGoal(1.5D)); + this.goalSelector.addGoal(2, new SitWhenOrderedToGoal(this)); + this.goalSelector.addGoal(3, new Wolf.WolfAvoidEntityGoal<>(this, Llama.class, 24.0F, 1.5D, 1.5D)); ++ this.goalSelector.addGoal(3, new AvoidRabidWolfGoal(this, 24.0F, 1.5D, 1.5D)); // Purpur + this.goalSelector.addGoal(4, new LeapAtTargetGoal(this, 0.4F)); + this.goalSelector.addGoal(5, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(6, new FollowOwnerGoal(this, 1.0D, 10.0F, 2.0F, false)); +@@ -116,11 +235,12 @@ public class Wolf extends TamableAnimal implements NeutralMob { + this.goalSelector.addGoal(9, new BegGoal(this, 8.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(10, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new OwnerHurtByTargetGoal(this)); + this.targetSelector.addGoal(2, new OwnerHurtTargetGoal(this)); + this.targetSelector.addGoal(3, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); + this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +- this.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(this, Animal.class, false, Wolf.PREY_SELECTOR)); ++ // this.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(this, Animal.class, false, Wolf.PREY_SELECTOR)); // Purpur - moved to updatePathfinders() + this.targetSelector.addGoal(6, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); + this.targetSelector.addGoal(7, new NearestAttackableTargetGoal<>(this, AbstractSkeleton.class, false)); + this.targetSelector.addGoal(8, new ResetUniversalAngerTargetGoal<>(this, true)); +@@ -165,6 +285,7 @@ public class Wolf extends TamableAnimal implements NeutralMob { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putByte("CollarColor", (byte) this.getCollarColor().getId()); ++ nbt.putBoolean("Purpur.IsRabid", this.isRabid); // Purpur + this.addPersistentAngerSaveData(nbt); + } + +@@ -174,6 +295,10 @@ public class Wolf extends TamableAnimal implements NeutralMob { + if (nbt.contains("CollarColor", 99)) { + this.setCollarColor(DyeColor.byId(nbt.getInt("CollarColor"))); + } ++ // Purpur start ++ this.isRabid = nbt.getBoolean("Purpur.IsRabid"); ++ this.updatePathfinders(false); ++ // Purpur end + + this.readPersistentAngerSaveData(this.level, nbt); + } +@@ -218,6 +343,11 @@ public class Wolf extends TamableAnimal implements NeutralMob { + public void tick() { + super.tick(); + if (this.isAlive()) { ++ // Purpur start ++ if (this.age % 300 == 0 && this.isRabid()) { ++ this.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 400)); ++ } ++ // Purpur end + this.interestedAngleO = this.interestedAngle; + if (this.isInterested()) { + this.interestedAngle += (1.0F - this.interestedAngle) * 0.4F; +@@ -412,7 +542,7 @@ public class Wolf extends TamableAnimal implements NeutralMob { + } + + // CraftBukkit - added event call and isCancelled check. +- if (this.random.nextInt(3) == 0 && !CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { ++ if ((this.level.purpurConfig.alwaysTameInCreative && player.getAbilities().instabuild) || (this.random.nextInt(3) == 0 && !CraftEventFactory.callEntityTameEvent(this, player).isCancelled())) { // Purpur + this.tame(player); + this.navigation.stop(); + this.setTarget((LivingEntity) null); +@@ -424,6 +554,20 @@ public class Wolf extends TamableAnimal implements NeutralMob { + + return InteractionResult.SUCCESS; + } ++ // Purpur start ++ else if (this.level.purpurConfig.wolfMilkCuresRabies && itemstack.getItem() == Items.MILK_BUCKET && this.isRabid()) { ++ if (!player.isCreative()) { ++ player.setItemInHand(hand, new ItemStack(Items.BUCKET)); ++ } ++ this.setRabid(false); ++ for (int i = 0; i < 10; ++i) { ++ ((ServerLevel) level).sendParticles(((ServerLevel) level).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 1.5), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + + return super.mobInteract(player, hand); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +index c66a214dfbde7fd8e7a68efaa82ac260178f297f..e8f42ad6cc32cb21584d8988fcf3d1e4b6552f0c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java ++++ b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +@@ -101,10 +101,23 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + private float spinningAnimationTicks; + private float spinningAnimationTicks0; + public boolean forceDancing = false; // CraftBukkit ++ private org.purpurmc.purpur.controller.FlyingMoveControllerWASD purpurController; // Purpur + + public Allay(EntityType type, Level world) { + super(type, world); +- this.moveControl = new FlyingMoveControl(this, 20, true); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this, 0.1F, 0.5F); ++ this.moveControl = new FlyingMoveControl(this, 20, true) { ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ purpurController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setCanPickUpLoot(this.canPickUpLoot()); + EntityPositionSource entitypositionsource = new EntityPositionSource(this, this.getEyeHeight()); + +@@ -119,6 +132,28 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + } + // CraftBukkit end + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.allayRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.allayRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.allayControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Allay.MEMORY_TYPES, Allay.SENSOR_TYPES); +@@ -231,13 +266,13 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("allayBrain"); ++ //this.level.getProfiler().push("allayBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("allayActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("allayActivityUpdate"); // Purpur + AllayAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -379,9 +414,31 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + + @Override + public boolean wantsToPickUp(ItemStack stack) { +- ItemStack itemstack1 = this.getItemInHand(InteractionHand.MAIN_HAND); +- +- return !itemstack1.isEmpty() && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && this.inventory.canAddItem(stack) && this.allayConsidersItemEqual(itemstack1, stack); ++ // Purpur start ++ if (!this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ return false; ++ } ++ ItemStack itemStack = this.getItemInHand(InteractionHand.MAIN_HAND); ++ if (itemStack.isEmpty()) { ++ return false; ++ } ++ if (!allayConsidersItemEqual(itemStack, stack)) { ++ return false; ++ } ++ if (!this.inventory.canAddItem(stack)) { ++ return false; ++ } ++ for (String tag : this.level.purpurConfig.allayRespectNBT) { ++ if (stack.hasTag() && itemStack.hasTag()) { ++ Tag tag1 = stack.getTag().get(tag); ++ Tag tag2 = itemStack.getTag().get(tag); ++ if (!Objects.equals(tag1, tag2)) { ++ return false; ++ } ++ } ++ } ++ return true; ++ // Purpur end + } + + private boolean allayConsidersItemEqual(ItemStack stack, ItemStack stack2) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +index 02219f5ca614fefffa1ceb3c7036dfe1c90c8676..867091706521dbb16e66bdf5c9f4136759ab2677 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java ++++ b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +@@ -98,6 +98,48 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder getModelRotationValues() { + return this.modelRotationValues; +@@ -288,13 +330,13 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder optional = this.getBrain().getMemory(MemoryModuleType.PLAY_DEAD_TICKS); + +@@ -520,14 +562,22 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder brain = (Brain) this.getBrain(); // Paper - decompile fix + brain.tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("camelActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("camelActivityUpdate"); // Purpur + CamelAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -294,6 +301,23 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Rider + return this.dashCooldown; + } + ++ // Purpur start ++ @Override ++ public float generateRandomMaxHealth(net.minecraft.util.RandomSource random) { ++ return (float) generateRandomMaxHealth(this.level.purpurConfig.camelMaxHealthMin, this.level.purpurConfig.camelMaxHealthMax); ++ } ++ ++ @Override ++ public double generateRandomJumpStrength(net.minecraft.util.RandomSource random) { ++ return generateRandomJumpStrength(this.level.purpurConfig.camelJumpStrengthMin, this.level.purpurConfig.camelJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateRandomSpeed(net.minecraft.util.RandomSource random) { ++ return generateRandomSpeed(this.level.purpurConfig.camelMovementSpeedMin, this.level.purpurConfig.camelMovementSpeedMax); ++ } ++ // Purpur end ++ + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.CAMEL_AMBIENT; +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +index d1cc0ffd153c0902f0110adbbcd98f1d2089fa27..d34600bca83e2742c2f26c300e4528f0bbd203e8 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +@@ -84,16 +84,69 @@ public class Frog extends Animal implements VariantHolder { + public final AnimationState walkAnimationState = new AnimationState(); + public final AnimationState swimAnimationState = new AnimationState(); + public final AnimationState swimIdleAnimationState = new AnimationState(); ++ private org.purpurmc.purpur.controller.MoveControllerWASD purpurLandController; // Purpur ++ private org.purpurmc.purpur.controller.WaterMoveControllerWASD purpurWaterController; // Purpur + + public Frog(EntityType type, Level world) { + super(type, world); + this.lookControl = new Frog.FrogLookControl(this); + this.setPathfindingMalus(BlockPathTypes.WATER, 4.0F); + this.setPathfindingMalus(BlockPathTypes.TRAPDOOR, -1.0F); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ this.purpurLandController = new org.purpurmc.purpur.controller.MoveControllerWASD(this, 0.2F); ++ this.purpurWaterController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(this, 0.5F); ++ this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true) { ++ @Override ++ public void tick() { ++ net.minecraft.world.entity.player.Player rider = mob.getRider(); ++ if (rider != null && mob.isControllable()) { ++ if (mob.isInWater()) { ++ purpurWaterController.purpurTick(rider); ++ mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, -0.005D, 0.0D)); ++ } else { ++ purpurLandController.purpurTick(rider); ++ } ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.maxUpStep = 1.0F; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.frogRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.frogRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.frogControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return (getRider() != null && isControllable()) ? level.purpurConfig.frogRidableJumpHeight * this.getBlockJumpFactor() : super.getJumpPower(); ++ } ++ ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.frogBreedingTicks; ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -180,13 +233,13 @@ public class Frog extends Animal implements VariantHolder { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("frogBrain"); ++ //this.level.getProfiler().push("frogBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("frogActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("frogActivityUpdate"); // Purpur + FrogAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -394,7 +447,7 @@ public class Frog extends Animal implements VariantHolder { + return world.getBlockState(pos.below()).is(BlockTags.FROGS_SPAWNABLE_ON) && isBrightEnoughToSpawn(world, pos); + } + +- class FrogLookControl extends LookControl { ++ class FrogLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + FrogLookControl(Mob entity) { + super(entity); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +index e591b0a09f5a8475b3ec9cd28bd5d5b69809ed73..93fe8efe4ddd4de440fff1ca7a38960203d0ff74 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -45,13 +45,50 @@ public class Tadpole extends AbstractFish { + protected static final ImmutableList>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.HURT_BY, SensorType.FROG_TEMPTATIONS); + protected static final ImmutableList> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.LOOK_TARGET, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, MemoryModuleType.IS_TEMPTED, MemoryModuleType.TEMPTING_PLAYER, MemoryModuleType.BREED_TARGET, MemoryModuleType.IS_PANICKING); + public boolean ageLocked; // Paper ++ private org.purpurmc.purpur.controller.WaterMoveControllerWASD purpurController; // Purpur + + public Tadpole(EntityType type, Level world) { + super(type, world); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(this, 0.5F); ++ this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true) { ++ @Override ++ public void tick() { ++ Player rider = mob.getRider(); ++ if (rider != null && mob.isControllable()) { ++ purpurController.purpurTick(rider); ++ mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, 0.002D, 0.0D)); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.lookControl = new SmoothSwimmingLookControl(this, 10); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.tadpoleRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.tadpoleRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.tadpoleControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + protected PathNavigation createNavigation(Level world) { + return new WaterBoundPathNavigation(this, world); +@@ -80,13 +117,13 @@ public class Tadpole extends AbstractFish { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("tadpoleBrain"); ++ //this.level.getProfiler().push("tadpoleBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("tadpoleActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("tadpoleActivityUpdate"); // Purpur + TadpoleAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +index 0f365b9dbb160d90ddf5fcd40895305df48ce916..8f3817df5996bb63ab15ee1ab1ef38e90715018a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +@@ -89,6 +89,43 @@ public class Goat extends Animal { + return InstrumentItem.create(Items.GOAT_HORN, (Holder) holderset.getRandomElement(randomsource).get()); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.goatRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.goatRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.goatControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.goatMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.goatBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.goatTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.goatAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Goat.MEMORY_TYPES, Goat.SENSOR_TYPES); +@@ -191,13 +228,13 @@ public class Goat extends Animal { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("goatBrain"); +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ //this.level.getProfiler().push("goatBrain"); // Purpur ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("goatActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("goatActivityUpdate"); // Purpur + GoatAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -389,6 +426,7 @@ public class Goat extends Animal { + + // Paper start - Goat ram API + public void ram(net.minecraft.world.entity.LivingEntity entity) { ++ if(!new org.purpurmc.purpur.event.entity.GoatRamEntityEvent((org.bukkit.entity.Goat) getBukkitEntity(), (org.bukkit.entity.LivingEntity) entity.getBukkitLivingEntity()).callEvent()) return; // Purpur + Brain brain = this.getBrain(); + brain.setMemory(MemoryModuleType.RAM_TARGET, entity.position()); + brain.eraseMemory(MemoryModuleType.RAM_COOLDOWN_TICKS); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +index 72d660cd2ade39335024897cffb8b8a151a7cb71..8f27e6b495b82361d331c514c78088d358e4f5b4 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +@@ -118,12 +118,48 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + + protected AbstractHorse(EntityType type, Level world) { + super(type, world); ++ this.moveControl = new net.minecraft.world.entity.ai.control.MoveControl(this); // Purpur - use vanilla controller ++ this.lookControl = new net.minecraft.world.entity.ai.control.LookControl(this); // Purpur - use vanilla controller + this.maxUpStep = 1.0F; + this.createInventory(); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return false; // vanilla handles ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.generateRandomMaxHealth(this.random)); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.generateRandomSpeed(this.random)); ++ this.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(this.generateRandomJumpStrength(this.random)); ++ } ++ ++ protected double generateRandomMaxHealth(double min, double max) { ++ if (min == max) return min; ++ int diff = Mth.floor(max - min); ++ double base = max - diff; ++ int first = Mth.floor((double) diff / 2); ++ int rest = diff - first; ++ return base + random.nextInt(first + 1) + random.nextInt(rest + 1); ++ } ++ ++ protected double generateRandomJumpStrength(double min, double max) { ++ if (min == max) return min; ++ return min + (max - min) * this.random.nextDouble(); ++ } ++ ++ protected double generateRandomSpeed(double min, double max) { ++ if (min == max) return min; ++ return min + (max - min) * this.random.nextDouble(); ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HorseHasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.2D)); + this.goalSelector.addGoal(1, new RunAroundLikeCrazyGoal(this, 1.2D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D, AbstractHorse.class)); +@@ -134,6 +170,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + if (this.canPerformRearing()) { + this.goalSelector.addGoal(9, new RandomStandGoal(this)); + } ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HorseHasRider(this)); // Purpur + + this.addBehaviourGoals(); + } +@@ -310,7 +347,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + + @Override + protected int calculateFallDamage(float fallDistance, float damageMultiplier) { +- return Mth.ceil((fallDistance * 0.5F - 3.0F) * damageMultiplier); ++ return Mth.ceil((fallDistance * 0.5F - this.safeFallDistance) * damageMultiplier); + } + + protected int getInventorySize() { +@@ -1200,7 +1237,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + entityData = new AgeableMob.AgeableMobGroupData(0.2F); + } + +- this.randomizeAttributes(world.getRandom()); ++ // this.randomizeAttributes(world.getRandom()); // Purpur - replaced by initAttributes() + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData, entityNbt); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java +index e0dfee0e0ce091d5ae0ec740e939c2c50915c104..db56ad11133fb1c3ec33f8d05421184b86174762 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java +@@ -15,6 +15,43 @@ public class Donkey extends AbstractChestedHorse { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.donkeyRidableInWater; ++ } ++ ++ @Override ++ public float generateRandomMaxHealth(net.minecraft.util.RandomSource random) { ++ return (float) generateRandomMaxHealth(this.level.purpurConfig.donkeyMaxHealthMin, this.level.purpurConfig.donkeyMaxHealthMax); ++ } ++ ++ @Override ++ public double generateRandomJumpStrength(net.minecraft.util.RandomSource random) { ++ return generateRandomJumpStrength(this.level.purpurConfig.donkeyJumpStrengthMin, this.level.purpurConfig.donkeyJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateRandomSpeed(net.minecraft.util.RandomSource random) { ++ return generateRandomSpeed(this.level.purpurConfig.donkeyMovementSpeedMin, this.level.purpurConfig.donkeyMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.donkeyBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.donkeyTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.donkeyAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.DONKEY_AMBIENT; +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java +index 97a92b5576da2f8572a71cab42c40d1368ecb300..ee00e85a1cf1221f22b60f6a43dfd212e1a8a570 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java +@@ -40,6 +40,43 @@ public class Horse extends AbstractHorse implements VariantHolder { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.horseRidableInWater; ++ } ++ ++ @Override ++ public float generateRandomMaxHealth(RandomSource random) { ++ return (float) generateRandomMaxHealth(this.level.purpurConfig.horseMaxHealthMin, this.level.purpurConfig.horseMaxHealthMax); ++ } ++ ++ @Override ++ public double generateRandomJumpStrength(RandomSource random) { ++ return generateRandomJumpStrength(this.level.purpurConfig.horseJumpStrengthMin, this.level.purpurConfig.horseJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateRandomSpeed(RandomSource random) { ++ return generateRandomSpeed(this.level.purpurConfig.horseMovementSpeedMin, this.level.purpurConfig.horseMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.horseBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.horseTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.horseAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void randomizeAttributes(RandomSource random) { + this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double)this.generateRandomMaxHealth(random)); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +index 7ae0e4b3aa8e861500ddc7b38aa671258b532fcd..beea0545a38b0f044409c2cdb5bbefaf8d783d45 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +@@ -73,11 +73,86 @@ public class Llama extends AbstractChestedHorse implements VariantHolder type, Level world) { + super(type, world); ++ // Purpur start ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this) { ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable() && isSaddled()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ }; ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable() && isSaddled()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.llamaRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.llamaRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.llamaControllable; ++ } ++ ++ @Override ++ public boolean isSaddled() { ++ return super.isSaddled() || (isTamed() && getSwag() != null); ++ } ++ ++ @Override ++ public float generateRandomMaxHealth(RandomSource random) { ++ return (float) generateRandomMaxHealth(this.level.purpurConfig.llamaMaxHealthMin, this.level.purpurConfig.llamaMaxHealthMax); ++ } ++ ++ @Override ++ public double generateRandomJumpStrength(RandomSource random) { ++ return generateRandomJumpStrength(this.level.purpurConfig.llamaJumpStrengthMin, this.level.purpurConfig.llamaJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateRandomSpeed(RandomSource random) { ++ return generateRandomSpeed(this.level.purpurConfig.llamaMovementSpeedMin, this.level.purpurConfig.llamaMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.llamaBreedingTicks; + } + ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.llamaTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.llamaAlwaysDropExp; ++ } ++ // Purpur end ++ + public boolean isTraderLlama() { + return false; + } +@@ -110,7 +185,7 @@ public class Llama extends AbstractChestedHorse implements VariantHolder DATA_SHOW_BOTTOM = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.BOOLEAN); + public int time; + public boolean generatedByDragonFight = false; // Paper - Fix invulnerable end crystals ++ // Purpur start ++ private net.minecraft.world.entity.monster.Phantom targetPhantom; ++ private int phantomBeamTicks = 0; ++ private int phantomDamageCooldown = 0; ++ private int idleCooldown = 0; ++ // Purpur end + + public EndCrystal(EntityType type, Level world) { + super(type, world); +@@ -76,10 +82,70 @@ public class EndCrystal extends Entity { + } + } + // Paper end ++ if (this.level.purpurConfig.endCrystalCramming > 0 && this.level.getEntitiesOfClass(EndCrystal.class, getBoundingBox()).size() > this.level.purpurConfig.endCrystalCramming) this.hurt(DamageSource.CRAMMING, 6.0F); // Purpur + } + ++ // Purpur start ++ if (level.purpurConfig.phantomAttackedByCrystalRadius <= 0 || --idleCooldown > 0) { ++ return; // on cooldown ++ } ++ ++ if (targetPhantom == null) { ++ for (net.minecraft.world.entity.monster.Phantom phantom : level.getEntitiesOfClass(net.minecraft.world.entity.monster.Phantom.class, getBoundingBox().inflate(level.purpurConfig.phantomAttackedByCrystalRadius))) { ++ if (phantom.hasLineOfSight(this)) { ++ attackPhantom(phantom); ++ break; ++ } ++ } ++ } else { ++ setBeamTarget(new BlockPos(targetPhantom).offset(0, -2, 0)); ++ if (--phantomBeamTicks > 0 && targetPhantom.isAlive()) { ++ phantomDamageCooldown--; ++ if (targetPhantom.hasLineOfSight(this)) { ++ if (phantomDamageCooldown <= 0) { ++ phantomDamageCooldown = 20; ++ targetPhantom.hurt(DamageSource.indirectMagic(this, this), level.purpurConfig.phantomAttackedByCrystalDamage); ++ } ++ } else { ++ forgetPhantom(); // no longer in sight ++ } ++ } else { ++ forgetPhantom(); // attacked long enough ++ } ++ } ++ } ++ ++ private void attackPhantom(net.minecraft.world.entity.monster.Phantom phantom) { ++ phantomDamageCooldown = 0; ++ phantomBeamTicks = 60; ++ targetPhantom = phantom; + } + ++ private void forgetPhantom() { ++ targetPhantom = null; ++ setBeamTarget(null); ++ phantomBeamTicks = 0; ++ phantomDamageCooldown = 0; ++ idleCooldown = 60; ++ } ++ ++ public boolean shouldExplode() { ++ return showsBottom() ? level.purpurConfig.basedEndCrystalExplode : level.purpurConfig.baselessEndCrystalExplode; ++ } ++ ++ public float getExplosionPower() { ++ return (float) (showsBottom() ? level.purpurConfig.basedEndCrystalExplosionPower : level.purpurConfig.baselessEndCrystalExplosionPower); ++ } ++ ++ public boolean hasExplosionFire() { ++ return showsBottom() ? level.purpurConfig.basedEndCrystalExplosionFire : level.purpurConfig.baselessEndCrystalExplosionFire; ++ } ++ ++ public Level.ExplosionInteraction getExplosionEffect() { ++ return showsBottom() ? level.purpurConfig.basedEndCrystalExplosionEffect : level.purpurConfig.baselessEndCrystalExplosionEffect; ++ } ++ // Purpur end ++ + @Override + protected void addAdditionalSaveData(CompoundTag nbt) { + if (this.getBeamTarget() != null) { +@@ -123,17 +189,18 @@ public class EndCrystal extends Entity { + // CraftBukkit end + this.remove(Entity.RemovalReason.KILLED); + if (!source.isExplosion()) { ++ if (shouldExplode()) { // Purpur + DamageSource damagesource1 = source.getEntity() != null ? DamageSource.explosion(this, source.getEntity()) : null; +- + // CraftBukkit start +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 6.0F, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), getExplosionPower(), hasExplosionFire()); // Purpur + this.level.getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + this.unsetRemoved(); + return false; + } +- this.level.explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.BLOCK); ++ this.level.explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), getExplosionEffect()); // Purpur + // CraftBukkit end ++ } else this.unsetRemoved(); // Purpur + } + + this.onDestroyedBy(source); +diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +index c7caaebfb4b9f28cbe700d88fdcf232a500e8ca7..84b230d979a91dd776e180e5b828b26bdc98ef12 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java ++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +@@ -98,14 +98,16 @@ public class EnderDragon extends Mob implements Enemy { + private final Node[] nodes = new Node[24]; + private final int[] nodeAdjacency = new int[24]; + private final BinaryHeap openSet = new BinaryHeap(); +- private Explosion explosionSource = new Explosion(null, this, null, null, Double.NaN, Double.NaN, Double.NaN, Float.NaN, true, Explosion.BlockInteraction.DESTROY); // CraftBukkit - reusable source for CraftTNTPrimed.getSource() ++ private Explosion explosionSource; // CraftBukkit - reusable source for CraftTNTPrimed.getSource() // Purpur - moved instantiation to ctor + // Paper start - add var for save custom podium + @Nullable + private BlockPos podium; + // Paper end ++ private boolean hadRider; // Purpur + + public EnderDragon(EntityType entitytypes, Level world) { + super(EntityType.ENDER_DRAGON, world); ++ this.explosionSource = new Explosion(this.level, this, null, null, Double.NaN, Double.NaN, Double.NaN, Float.NaN, true, Explosion.BlockInteraction.DESTROY); // Purpur - moved instantiation from field + this.subEntities = new EnderDragonPart[]{this.head, this.neck, this.body, this.tail1, this.tail2, this.tail3, this.wing1, this.wing2}; + this.setHealth(this.getMaxHealth()); + this.noPhysics = true; +@@ -117,7 +119,59 @@ public class EnderDragon extends Mob implements Enemy { + } + + this.phaseManager = new EnderDragonPhaseManager(this); ++ ++ // Purpur start ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this) { ++ @Override ++ public void vanillaTick() { ++ // dragon doesn't use the controller. do nothing ++ } ++ }; ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void vanillaTick() { ++ // dragon doesn't use the controller. do nothing ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot() - 180F, rider.xRotO * 0.5F); ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.enderDragonRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.enderDragonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.enderDragonControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.enderDragonMaxY; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.enderDragonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.enderDragonTakeDamageFromWater; + } ++ // Purpur end + + public static AttributeSupplier.Builder createAttributes() { + return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0D); +@@ -180,6 +234,37 @@ public class EnderDragon extends Mob implements Enemy { + + @Override + public void aiStep() { ++ // Purpur start ++ boolean hasRider = getRider() != null && this.isControllable(); ++ if (hasRider) { ++ if (!hadRider) { ++ hadRider = true; ++ noPhysics = false; ++ this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(4.0F, 2.0F); ++ } ++ ++ // dragon doesn't use controllers, so must tick manually ++ moveControl.tick(); ++ lookControl.tick(); ++ ++ moveRelative((float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F, new Vec3(-getStrafeMot(), getVerticalMot(), -getForwardMot())); ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot); ++ move(MoverType.PLAYER, mot); ++ ++ mot = mot.multiply(0.9F, 0.9F, 0.9F); ++ setDeltaMovement(mot); ++ ++ // control wing flap speed on client ++ phaseManager.setPhase(mot.x() * mot.x() + mot.z() * mot.z() < 0.005F ? EnderDragonPhase.HOVERING : EnderDragonPhase.HOLDING_PATTERN); ++ } else if (hadRider) { ++ hadRider = false; ++ noPhysics = true; ++ this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(16.0F, 8.0F); ++ phaseManager.setPhase(EnderDragonPhase.HOLDING_PATTERN); // HoldingPattern ++ } ++ // Purpur end ++ + this.processFlappingMovement(); + if (this.level.isClientSide) { + this.setHealth(this.getHealth()); +@@ -193,6 +278,8 @@ public class EnderDragon extends Mob implements Enemy { + float f; + + if (this.isDeadOrDying()) { ++ if (hasRider) ejectPassengers(); // Purpur ++ + float f1 = (this.random.nextFloat() - 0.5F) * 8.0F; + + f = (this.random.nextFloat() - 0.5F) * 4.0F; +@@ -205,9 +292,9 @@ public class EnderDragon extends Mob implements Enemy { + + f = 0.2F / ((float) vec3d.horizontalDistance() * 10.0F + 1.0F); + f *= (float) Math.pow(2.0D, vec3d.y); +- if (this.phaseManager.getCurrentPhase().isSitting()) { ++ if (!hasRider && this.phaseManager.getCurrentPhase().isSitting()) { // Purpur + this.flapTime += 0.1F; +- } else if (this.inWall) { ++ } else if (!hasRider && this.inWall) { // Purpur + this.flapTime += f * 0.5F; + } else { + this.flapTime += f; +@@ -252,7 +339,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + this.phaseManager.getCurrentPhase().doClientTick(); +- } else { ++ } else if (!hasRider) { // Purpur + DragonPhaseInstance idragoncontroller = this.phaseManager.getCurrentPhase(); + + idragoncontroller.doServerTick(); +@@ -321,7 +408,7 @@ public class EnderDragon extends Mob implements Enemy { + this.tickPart(this.body, (double) (f11 * 0.5F), 0.0D, (double) (-f12 * 0.5F)); + this.tickPart(this.wing1, (double) (f12 * 4.5F), 2.0D, (double) (f11 * 4.5F)); + this.tickPart(this.wing2, (double) (f12 * -4.5F), 2.0D, (double) (f11 * -4.5F)); +- if (!this.level.isClientSide && this.hurtTime == 0) { ++ if (!hasRider && !this.level.isClientSide && this.hurtTime == 0) { // Purpur + this.knockBack(this.level.getEntities((Entity) this, this.wing1.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); + this.knockBack(this.level.getEntities((Entity) this, this.wing2.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); + this.hurt(this.level.getEntities((Entity) this, this.head.getBoundingBox().inflate(1.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); +@@ -365,7 +452,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + if (!this.level.isClientSide) { +- this.inWall = this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); ++ this.inWall = !hasRider && this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); // Purpur + if (this.dragonFight != null) { + this.dragonFight.updateDragon(this); + } +@@ -497,7 +584,7 @@ public class EnderDragon extends Mob implements Enemy { + BlockState iblockdata = this.level.getBlockState(blockposition); + + if (!iblockdata.isAir() && !iblockdata.is(BlockTags.DRAGON_TRANSPARENT)) { +- if (this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) { ++ if ((this.level.purpurConfig.enderDragonBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) { // Purpur + // CraftBukkit start - Add blocks to list rather than destroying them + // flag1 = this.level.removeBlock(blockposition, false) || flag1; + flag1 = true; +@@ -632,7 +719,7 @@ public class EnderDragon extends Mob implements Enemy { + boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT); + short short0 = 500; + +- if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) { ++ if (this.dragonFight != null && (level.purpurConfig.enderDragonAlwaysDropsFullExp || !this.dragonFight.hasPreviouslyKilledDragon())) { + short0 = 12000; + } + +@@ -1069,6 +1156,7 @@ public class EnderDragon extends Mob implements Enemy { + + @Override + protected boolean canRide(Entity entity) { ++ if (this.level.purpurConfig.enderDragonCanRideVehicles) return this.boardingCooldown <= 0; // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +index 08a432f76a72ad628b8febe393196286182fbe07..30bce9055aa4a3f108362e57a9937dd80a39e54b 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java ++++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +@@ -83,16 +83,31 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + return entityliving.getMobType() != MobType.UNDEAD && entityliving.attackable(); + }; + private static final TargetingConditions TARGETING_CONDITIONS = TargetingConditions.forCombat().range(20.0D).selector(WitherBoss.LIVING_ENTITY_SELECTOR); ++ private int shootCooldown = 0; // Purpur ++ @Nullable private java.util.UUID summoner; // Purpur + // Paper start + private boolean canPortal = false; + + public void setCanTravelThroughPortals(boolean canPortal) { this.canPortal = canPortal; } + // Paper end ++ private org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD purpurController; // Purpur + + public WitherBoss(EntityType type, Level world) { + super(type, world); + this.bossEvent = (ServerBossEvent) (new ServerBossEvent(this.getDisplayName(), BossEvent.BossBarColor.PURPLE, BossEvent.BossBarOverlay.PROGRESS)).setDarkenScreen(true); +- this.moveControl = new FlyingMoveControl(this, 10, false); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.1F); ++ this.moveControl = new FlyingMoveControl(this, 10, false) { ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ purpurController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setHealth(this.getMaxHealth()); + this.xpReward = 50; + } +@@ -107,13 +122,148 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + return navigationflying; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.witherRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.witherRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.witherControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.witherMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 5F; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.5, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ this.entityData.set(DATA_TARGETS.get(0), 0); ++ this.entityData.set(DATA_TARGETS.get(1), 0); ++ this.entityData.set(DATA_TARGETS.get(2), 0); ++ getNavigation().stop(); ++ shootCooldown = 20; ++ } ++ ++ @Override ++ public boolean onClick(net.minecraft.world.InteractionHand hand) { ++ return shoot(getRider(), hand == net.minecraft.world.InteractionHand.MAIN_HAND ? new int[]{1} : new int[]{2}); ++ } ++ ++ public boolean shoot(@Nullable Player rider, int[] heads) { ++ if (shootCooldown > 0) { ++ return false; ++ } ++ ++ shootCooldown = 20; ++ if (rider == null) { ++ return false; ++ } ++ ++ org.bukkit.craftbukkit.entity.CraftHumanEntity player = rider.getBukkitEntity(); ++ if (!player.hasPermission("allow.special.wither")) { ++ return false; ++ } ++ ++ net.minecraft.world.phys.HitResult rayTrace = getRayTrace(120, net.minecraft.world.level.ClipContext.Fluid.NONE); ++ if (rayTrace == null) { ++ return false; ++ } ++ ++ Vec3 loc; ++ if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.BLOCK) { ++ BlockPos pos = ((net.minecraft.world.phys.BlockHitResult) rayTrace).getBlockPos(); ++ loc = new Vec3(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D); ++ } else if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.ENTITY) { ++ Entity target = ((net.minecraft.world.phys.EntityHitResult) rayTrace).getEntity(); ++ loc = new Vec3(target.getX(), target.getY() + (target.getEyeHeight() / 2), target.getZ()); ++ } else { ++ org.bukkit.block.Block block = player.getTargetBlock(null, 120); ++ loc = new Vec3(block.getX() + 0.5D, block.getY() + 0.5D, block.getZ() + 0.5D); ++ } ++ ++ for (int head : heads) { ++ shoot(head, loc.x(), loc.y(), loc.z(), rider); ++ } ++ ++ return true; // handled ++ } ++ ++ public void shoot(int head, double x, double y, double z, Player rider) { ++ level.levelEvent(null, 1024, blockPosition(), 0); ++ double headX = getHeadX(head); ++ double headY = getHeadY(head); ++ double headZ = getHeadZ(head); ++ WitherSkull skull = new WitherSkull(level, this, x - headX, y - headY, z - headZ) { ++ @Override ++ public boolean canHitEntity(Entity target) { ++ // do not hit rider ++ return target != rider && super.canHitEntity(target); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ }; ++ skull.setPosRaw(headX, headY, headZ); ++ level.addFreshEntity(skull); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.witherMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.witherTakeDamageFromWater; ++ } ++ ++ @Nullable ++ public java.util.UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable java.util.UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.witherAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new WitherBoss.WitherDoNothingGoal()); + this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 40, 20.0F)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomFlyingGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 0, false, false, WitherBoss.LIVING_ENTITY_SELECTOR)); + } +@@ -131,6 +281,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("Invul", this.getInvulnerableTicks()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + } + + @Override +@@ -140,6 +291,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + if (this.hasCustomName()) { + this.bossEvent.setName(this.getDisplayName()); + } ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + + } + +@@ -255,6 +407,16 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z()); ++ } ++ if (shootCooldown > 0) { ++ shootCooldown--; ++ } ++ // Purpur end ++ + int i; + + if (this.getInvulnerableTicks() > 0) { +@@ -271,7 +433,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + // CraftBukkit end + +- if (!this.isSilent()) { ++ if (!this.isSilent() && level.purpurConfig.witherPlaySpawnSound) { + // CraftBukkit start - Use relative location for far away sounds + // this.world.globalLevelEvent(1023, new BlockPosition(this), 0); + int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; +@@ -295,7 +457,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + this.setInvulnerableTicks(i); + if (this.tickCount % 10 == 0) { +- this.heal(10.0F, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit ++ this.heal(this.getMaxHealth() / 33, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit // Purpur + } + + } else { +@@ -355,7 +517,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + if (this.destroyBlocksTick > 0) { + --this.destroyBlocksTick; +- if (this.destroyBlocksTick == 0 && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.destroyBlocksTick == 0 && (this.level.purpurConfig.witherBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // Purpur + i = Mth.floor(this.getY()); + j = Mth.floor(this.getX()); + int i1 = Mth.floor(this.getZ()); +@@ -388,8 +550,10 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + } + +- if (this.tickCount % 20 == 0) { +- this.heal(1.0F, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit ++ // Purpur start - customizable heal rate and amount ++ if (this.tickCount % level.purpurConfig.witherHealthRegenDelay == 0) { ++ this.heal(level.purpurConfig.witherHealthRegenAmount, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit ++ // Purpur end + } + + this.bossEvent.setProgress(this.getHealth() / this.getMaxHealth()); +@@ -580,11 +744,11 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + + public int getAlternativeTarget(int headIndex) { +- return (Integer) this.entityData.get((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex)); ++ return getRider() != null && this.isControllable() ? 0 : this.entityData.get(WitherBoss.DATA_TARGETS.get(headIndex)); // Purpur + } + + public void setAlternativeTarget(int headIndex, int id) { +- this.entityData.set((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex), id); ++ if (getRider() == null || !this.isControllable()) this.entityData.set(WitherBoss.DATA_TARGETS.get(headIndex), id); // Purpur + } + + @Override +@@ -599,6 +763,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + @Override + protected boolean canRide(Entity entity) { ++ if (this.level.purpurConfig.witherCanRideVehicles) return this.boardingCooldown <= 0; // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +index 713a47b38d6054e0b0c50dcdc23ecd3b0077f040..6f433787fe69a3647d5836e22b6634cc33b9838d 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -98,10 +98,12 @@ public class ArmorStand extends LivingEntity { + private boolean noTickPoseDirty = false; + private boolean noTickEquipmentDirty = false; + // Paper end ++ public boolean canMovementTick = true; // Purpur + + public ArmorStand(EntityType type, Level world) { + super(type, world); + if (world != null) this.canTick = world.paperConfig().entities.armorStands.tick; // Paper - armour stand ticking ++ if (world != null) this.canMovementTick = world.purpurConfig.armorstandMovement; // Purpur + this.handItems = NonNullList.withSize(2, ItemStack.EMPTY); + this.armorItems = NonNullList.withSize(4, ItemStack.EMPTY); + this.headPose = ArmorStand.DEFAULT_HEAD_POSE; +@@ -111,6 +113,7 @@ public class ArmorStand extends LivingEntity { + this.leftLegPose = ArmorStand.DEFAULT_LEFT_LEG_POSE; + this.rightLegPose = ArmorStand.DEFAULT_RIGHT_LEG_POSE; + this.maxUpStep = 0.0F; ++ this.setShowArms(world != null && world.purpurConfig.armorstandPlaceWithArms); // Purpur + } + + public ArmorStand(Level world, double x, double y, double z) { +@@ -595,7 +598,13 @@ public class ArmorStand extends LivingEntity { + } + + private org.bukkit.event.entity.EntityDeathEvent brokenByPlayer(DamageSource damageSource) { // Paper +- drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(new ItemStack(Items.ARMOR_STAND))); // CraftBukkit - add to drops ++ // Purpur start ++ final ItemStack armorStand = new ItemStack(Items.ARMOR_STAND); ++ if (this.level.purpurConfig.persistentDroppableEntityDisplayNames && this.hasCustomName()) { ++ armorStand.setHoverName(this.getCustomName()); ++ } ++ drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(armorStand)); // CraftBukkit - add to drops ++ // Purpur end + return this.brokenByAnything(damageSource); // Paper + } + +@@ -667,6 +676,7 @@ public class ArmorStand extends LivingEntity { + + @Override + public void tick() { ++ maxUpStep = level.purpurConfig.armorstandStepHeight; + // Paper start + if (!this.canTick) { + if (this.noTickPoseDirty) { +@@ -988,4 +998,18 @@ public class ArmorStand extends LivingEntity { + } + // Paper end + // Paper end ++ ++ // Purpur start ++ @Override ++ public void updateInWaterStateAndDoWaterCurrentPushing() { ++ if (this.level.purpurConfig.armorstandWaterMovement && ++ (this.level.purpurConfig.armorstandWaterFence || !(level.getBlockState(blockPosition().below()).getBlock() instanceof net.minecraft.world.level.block.FenceBlock))) ++ super.updateInWaterStateAndDoWaterCurrentPushing(); ++ } ++ ++ @Override ++ public void aiStep() { ++ if (this.canMovementTick && this.canMove) super.aiStep(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +index 428523feaa4f30260e32ba03937e88200246c693..d2cd7629a69d04937180df04829d12425815588c 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +@@ -268,7 +268,13 @@ public class ItemFrame extends HangingEntity { + } + + if (alwaysDrop) { +- this.spawnAtLocation(this.getFrameItemStack()); ++ // Purpur start ++ final ItemStack itemFrame = this.getFrameItemStack(); ++ if (this.level.purpurConfig.persistentDroppableEntityDisplayNames && this.hasCustomName()) { ++ itemFrame.setHoverName(this.getCustomName()); ++ } ++ this.spawnAtLocation(itemFrame); ++ // Purpur end + } + + if (!itemstack.isEmpty()) { +@@ -283,6 +289,13 @@ public class ItemFrame extends HangingEntity { + } + } + ++ // Purpur start ++ @Nullable ++ public net.minecraft.world.entity.item.ItemEntity spawnAtLocation(ItemStack stack) { ++ return this.spawnAtLocation(stack, getDirection().equals(Direction.DOWN) ? -0.6F : 0.0F); ++ } ++ // Purpur end ++ + private void removeFramedMap(ItemStack itemstack) { + // Paper start - fix MC-252817 (green map markers do not disappear) + this.getFramedMapIdFromItem(itemstack).ifPresent((i) -> { +diff --git a/src/main/java/net/minecraft/world/entity/decoration/Painting.java b/src/main/java/net/minecraft/world/entity/decoration/Painting.java +index 0c5caad2a5bfc14450cf8d37f988ee176e8d1450..320dce948023e23df32601820146fa64e1b2fa71 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/Painting.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/Painting.java +@@ -124,7 +124,7 @@ public class Painting extends HangingEntity implements VariantHolder { + return entry; +- }).orElseGet(Painting::getDefaultVariant); ++ }).orElseGet(() -> (Holder.Reference) getDefaultVariant()); // Purpur - decompile error + this.setVariant(holder); + this.direction = Direction.from2DDataValue(nbt.getByte("facing")); + super.readAdditionalSaveData(nbt); +@@ -152,7 +152,13 @@ public class Painting extends HangingEntity implements VariantHolder type, Level world) { + super(type, world); +@@ -340,6 +346,15 @@ public class ItemEntity extends Entity { + return false; + } else if (!this.getItem().getItem().canBeHurtBy(source)) { + return false; ++ // Purpur start ++ } else if ( ++ (immuneToCactus && source == DamageSource.CACTUS) || ++ (immuneToFire && (source.isFire() || source == DamageSource.IN_FIRE)) || ++ (immuneToLightning && source == DamageSource.LIGHTNING_BOLT) || ++ (immuneToExplosion && source.isExplosion()) ++ ) { ++ return false; ++ // Purpur end + } else if (this.level.isClientSide) { + return true; + } else { +@@ -541,6 +556,12 @@ public class ItemEntity extends Entity { + this.getEntityData().set(ItemEntity.DATA_ITEM, stack); + this.getEntityData().markDirty(ItemEntity.DATA_ITEM); // CraftBukkit - SPIGOT-4591, must mark dirty + this.despawnRate = level.paperConfig().entities.spawning.altItemDespawnRate.enabled ? level.paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), level.spigotConfig.itemDespawnRate) : level.spigotConfig.itemDespawnRate; // Paper ++ // Purpur start ++ if (level.purpurConfig.itemImmuneToCactus.contains(stack.getItem())) immuneToCactus = true; ++ if (level.purpurConfig.itemImmuneToExplosion.contains(stack.getItem())) immuneToExplosion = true; ++ if (level.purpurConfig.itemImmuneToFire.contains(stack.getItem())) immuneToFire = true; ++ if (level.purpurConfig.itemImmuneToLightning.contains(stack.getItem())) immuneToLightning = true; ++ // level end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +index b8abee145fc92faddef98da913eca7715b6bfc03..0cfe5cb3ce0ac8554bbdb68c6658369306ce634c 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +@@ -65,16 +65,19 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + protected AbstractSkeleton(EntityType type, Level world) { + super(type, world); + this.reassessWeaponGoal(); ++ this.setShouldBurnInDay(true); // Purpur + } + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new RestrictSunGoal(this)); + this.goalSelector.addGoal(3, new FleeSunGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Wolf.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); +@@ -98,35 +101,14 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + } + + // Paper start +- private boolean shouldBurnInDay = true; ++ // private boolean shouldBurnInDay = true; // Purpur - moved to LivingEntity - keep methods for ABI compatibility + public boolean shouldBurnInDay() { return shouldBurnInDay; } + public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } + // Paper end + + @Override + public void aiStep() { +- boolean flag = shouldBurnInDay && this.isSunBurnTick(); // Paper - Configurable Burning +- +- if (flag) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); +- +- if (!itemstack.isEmpty()) { +- if (itemstack.isDamageableItem()) { +- itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); +- if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { +- this.broadcastBreakEvent(EquipmentSlot.HEAD); +- this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); +- } +- } +- +- flag = false; +- } +- +- if (flag) { +- this.setSecondsOnFire(8); +- } +- } +- ++ // Purpur start - implemented in LivingEntity + super.aiStep(); + } + +@@ -158,11 +140,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + this.reassessWeaponGoal(); + this.setCanPickUpLoot(this.level.paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.skeletons || randomsource.nextFloat() < 0.55F * difficulty.getSpecialMultiplier()); // Paper + if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) { +- LocalDate localdate = LocalDate.now(); +- int i = localdate.get(ChronoField.DAY_OF_MONTH); +- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- +- if (j == 10 && i == 31 && randomsource.nextFloat() < 0.25F) { ++ if (net.minecraft.world.entity.ambient.Bat.isHalloweenSeason(world.getMinecraftWorld()) && this.random.nextFloat() < this.level.purpurConfig.chanceHeadHalloweenOnEntity) { // Purpur + this.setItemSlot(EquipmentSlot.HEAD, new ItemStack(randomsource.nextFloat() < 0.1F ? Blocks.JACK_O_LANTERN : Blocks.CARVED_PUMPKIN)); + this.armorDropChances[EquipmentSlot.HEAD.getIndex()] = 0.0F; + } +@@ -189,7 +167,6 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + } else { + this.goalSelector.addGoal(4, this.meleeGoal); + } +- + } + } + +@@ -202,7 +179,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + double d2 = target.getZ() - this.getZ(); + double d3 = Math.sqrt(d0 * d0 + d2 * d2); + +- entityarrow.shoot(d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, (float) (14 - this.level.getDifficulty().getId() * 4)); ++ entityarrow.shoot(d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, this.level.purpurConfig.skeletonBowAccuracyMap.getOrDefault(this.level.getDifficulty().getId(), (float) (14 - this.level.getDifficulty().getId() * 4))); // Purpur + // CraftBukkit start + org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), entityarrow.getPickupItem(), entityarrow, net.minecraft.world.InteractionHand.MAIN_HAND, 0.8F, true); // Paper + if (event.isCancelled()) { +@@ -233,7 +210,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + this.reassessWeaponGoal(); + // Paper start + if (nbt.contains("Paper.ShouldBurnInDay")) { +- this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); ++ // this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); // Purpur - implemented in LivingEntity + } + // Paper end + } +@@ -242,7 +219,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +- nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); ++ // nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Purpur - implemented in LivingEntity + } + // Paper end + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Blaze.java b/src/main/java/net/minecraft/world/entity/monster/Blaze.java +index 4595b734abb88df7da6dddf7b24c6c5ffcf6556a..b9d901239b4647d96f4318acd1b80400967718e7 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Blaze.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Blaze.java +@@ -32,26 +32,73 @@ public class Blaze extends Monster { + + public Blaze(EntityType type, Level world) { + super(type, world); +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); // Purpur ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F); + this.xpReward = 10; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.blazeRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.blazeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.blazeControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.blazeMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.blazeMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.blazeAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Blaze.BlazeAttackGoal(this)); + this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + public static AttributeSupplier.Builder createAttributes() { +- return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0D).add(Attributes.MOVEMENT_SPEED, (double)0.23F).add(Attributes.FOLLOW_RANGE, 48.0D); ++ return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0D).add(Attributes.MOVEMENT_SPEED, (double)0.23F).add(Attributes.FOLLOW_RANGE, 48.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + @Override +@@ -101,11 +148,19 @@ public class Blaze extends Monster { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.blazeTakeDamageFromWater; // Purpur + } + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), getVerticalMot() > 0 ? 0.07D : -0.07D, mot.z()); ++ return; ++ } ++ // Purpur end ++ + --this.nextHeightOffsetChangeTick; + if (this.nextHeightOffsetChangeTick <= 0) { + this.nextHeightOffsetChangeTick = 100; +diff --git a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java +index d980b906d9206560741576fa4153c57212f307a0..0ac5264a16c9121c0f6233e83c426199784fe4c9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java +@@ -28,6 +28,38 @@ public class CaveSpider extends Spider { + return Spider.createAttributes().add(Attributes.MAX_HEALTH, 12.0D); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.caveSpiderRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.caveSpiderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.caveSpiderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.caveSpiderMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.caveSpiderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.caveSpiderAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean doHurtTarget(Entity target) { + if (super.doHurtTarget(target)) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +index 338161d2eb15d9264027961b71678b8d2f020fd8..84e172508ec4591c57a2668d11fac84be6e75f0a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +@@ -59,21 +59,130 @@ public class Creeper extends Monster implements PowerableMob { + public int maxSwell = 30; + public int explosionRadius = 3; + private int droppedSkulls; ++ // Purpur start ++ private int spacebarCharge = 0; ++ private int prevSpacebarCharge = 0; ++ private int powerToggleDelay = 0; ++ private boolean exploding = false; ++ // Purpur end + + public Creeper(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.creeperRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.creeperRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.creeperControllable; ++ } ++ ++ @Override ++ protected void customServerAiStep() { ++ if (powerToggleDelay > 0) { ++ powerToggleDelay--; ++ } ++ if (getRider() != null && this.isControllable()) { ++ if (getRider().getForwardMot() != 0 || getRider().getStrafeMot() != 0) { ++ spacebarCharge = 0; ++ setIgnited(false); ++ setSwellDir(-1); ++ } ++ if (spacebarCharge == prevSpacebarCharge) { ++ spacebarCharge = 0; ++ } ++ prevSpacebarCharge = spacebarCharge; ++ } ++ super.customServerAiStep(); ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setIgnited(false); ++ setSwellDir(-1); ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (powerToggleDelay > 0) { ++ return true; // just toggled power, do not jump or ignite ++ } ++ spacebarCharge++; ++ if (spacebarCharge > maxSwell - 2) { ++ spacebarCharge = 0; ++ if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.powered.creeper")) { ++ powerToggleDelay = 20; ++ setPowered(!isPowered()); ++ setIgnited(false); ++ setSwellDir(-1); ++ return true; ++ } ++ } ++ if (!isIgnited()) { ++ if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0 && ++ getRider().getBukkitEntity().hasPermission("allow.special.creeper")) { ++ setIgnited(true); ++ setSwellDir(1); ++ return true; ++ } ++ } ++ return getForwardMot() == 0 && getStrafeMot() == 0; // do not jump if standing still ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.creeperMaxHealth); ++ } ++ ++ public net.minecraft.world.entity.SpawnGroupData finalizeSpawn(net.minecraft.world.level.ServerLevelAccessor world, net.minecraft.world.DifficultyInstance difficulty, net.minecraft.world.entity.MobSpawnType spawnReason, @Nullable net.minecraft.world.entity.SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { ++ double chance = world.getLevel().purpurConfig.creeperChargedChance; ++ if (chance > 0D && random.nextDouble() <= chance) { ++ setPowered(true); ++ } ++ return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.creeperTakeDamageFromWater; ++ } ++ ++ @Override ++ protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(DamageSource damagesource) { ++ if (!exploding && this.level.purpurConfig.creeperExplodeWhenKilled && damagesource.getEntity() instanceof net.minecraft.server.level.ServerPlayer) { ++ this.explodeCreeper(); ++ } ++ return super.dropAllDeathLoot(damagesource); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.creeperAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); + this.goalSelector.addGoal(2, new SwellGoal(this)); ++ this.goalSelector.addGoal(3, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Ocelot.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Cat.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); + } +@@ -265,15 +374,17 @@ public class Creeper extends Monster implements PowerableMob { + } + + public void explodeCreeper() { ++ this.exploding = true; // Purpur + if (!this.level.isClientSide) { + float f = this.isPowered() ? 2.0F : 1.0F; ++ float multiplier = this.level.purpurConfig.creeperHealthRadius ? this.getHealth() / this.getMaxHealth() : 1; // Purpur + + // CraftBukkit start +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), this.explosionRadius * f, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), (this.explosionRadius * f) * multiplier, false); // Purpur + this.level.getCraftServer().getPluginManager().callEvent(event); + if (!event.isCancelled()) { + this.dead = true; +- this.level.explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); ++ this.level.explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), this.level.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING) && level.purpurConfig.creeperAllowGriefing ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); // Purpur + this.discard(); + this.spawnLingeringCloud(); + } else { +@@ -282,7 +393,7 @@ public class Creeper extends Monster implements PowerableMob { + } + // CraftBukkit end + } +- ++ this.exploding = false; // Purpur + } + + private void spawnLingeringCloud() { +@@ -324,6 +435,7 @@ public class Creeper extends Monster implements PowerableMob { + com.destroystokyo.paper.event.entity.CreeperIgniteEvent event = new com.destroystokyo.paper.event.entity.CreeperIgniteEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ignited); + if (event.callEvent()) { + this.entityData.set(Creeper.DATA_IS_IGNITED, event.isIgnited()); ++ if (!event.isIgnited()) setSwellDir(-1); // Purpur + } + } + // Paper end +diff --git a/src/main/java/net/minecraft/world/entity/monster/Drowned.java b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +index 1b1305f5eaf5710b72c57ab4c3953e703a23f1e0..68e31cf561f3d76bce6fa4324a75594c776f8964 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Drowned.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +@@ -29,6 +29,7 @@ import net.minecraft.world.entity.ai.goal.MoveToBlockGoal; + import net.minecraft.world.entity.ai.goal.RandomStrollGoal; + import net.minecraft.world.entity.ai.goal.RangedAttackGoal; + import net.minecraft.world.entity.ai.goal.ZombieAttackGoal; ++import net.minecraft.world.entity.ai.goal.MoveThroughVillageGoal; + import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; + import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; + import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; +@@ -68,6 +69,58 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.groundNavigation = new GroundPathNavigation(this, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.drownedRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.drownedRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.drownedControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.drownedMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.drownedSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.drownedJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.drownedJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.drownedJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.drownedTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.drownedAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void addBehaviourGoals() { + this.goalSelector.addGoal(1, new Drowned.DrownedGoToWaterGoal(this, 1.0D)); +@@ -75,10 +128,23 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.goalSelector.addGoal(2, new Drowned.DrownedAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(5, new Drowned.DrownedGoToBeachGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new Drowned.DrownedSwimUpGoal(this, 1.0D, this.level.getSeaLevel())); ++ if (level.purpurConfig.drownedBreakDoors) this.goalSelector.addGoal(6, new MoveThroughVillageGoal(this, 1.0D, true, 4, this::canBreakDoors)); + this.goalSelector.addGoal(7, new RandomStrollGoal(this, 1.0D)); + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Drowned.class})).setAlertOthers(ZombifiedPiglin.class)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::okTarget)); +- if (this.level.spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Paper ++ // Purpur start ++ if ( level.spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal(this, AbstractVillager.class, false) { // Spigot ++ @Override ++ public boolean canUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canUse(); ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canContinueToUse(); ++ } ++ }); ++ // Purpur end + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Axolotl.class, true, false)); + this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); +@@ -112,7 +178,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + + @Override + public boolean supportsBreakDoorGoal() { +- return false; ++ return level.purpurConfig.drownedBreakDoors ? true : false; + } + + @Override +@@ -254,8 +320,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.searchingForLand = targetingUnderwater; + } + +- private static class DrownedMoveControl extends MoveControl { +- ++ private static class DrownedMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + private final Drowned drowned; + + public DrownedMoveControl(Drowned drowned) { +@@ -264,7 +329,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + LivingEntity entityliving = this.drowned.getTarget(); + + if (this.drowned.wantsToSwim() && this.drowned.isInWater()) { +@@ -287,7 +352,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + + this.drowned.setYRot(this.rotlerp(this.drowned.getYRot(), f, 90.0F)); + this.drowned.yBodyRot = this.drowned.getYRot(); +- float f1 = (float) (this.speedModifier * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + float f2 = Mth.lerp(0.125F, this.drowned.getSpeed(), f1); + + this.drowned.setSpeed(f2); +@@ -297,7 +362,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.drowned.setDeltaMovement(this.drowned.getDeltaMovement().add(0.0D, -0.008D, 0.0D)); + } + +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java +index d02286d553c600fe7e75f48e278e380d21c5b868..3533414fcb112b75df7226d32b220bfcd6bd869f 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java +@@ -33,6 +33,38 @@ public class ElderGuardian extends Guardian { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.elderGuardianRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.elderGuardianControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.elderGuardianMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.elderGuardianTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.elderGuardianAlwaysDropExp; ++ } ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Guardian.createAttributes().add(Attributes.MOVEMENT_SPEED, 0.30000001192092896D).add(Attributes.ATTACK_DAMAGE, 8.0D).add(Attributes.MAX_HEALTH, 80.0D); + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +index ff0e09a7387e7dc9ca136d3e48e640b9e9cb4bf3..c029d6eb7893d41432e3de15fbf94768ef595d3f 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java ++++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +@@ -89,12 +89,40 @@ public class EnderMan extends Monster implements NeutralMob { + public EnderMan(EntityType type, Level world) { + super(type, world); + this.maxUpStep = 1.0F; +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.endermanRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.endermanRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.endermanControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.endermanMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.endermanAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F)); +@@ -102,9 +130,10 @@ public class EnderMan extends Monster implements NeutralMob { + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this)); + this.goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); +- this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, true, false)); ++ this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, 10, true, false, (entityliving) -> entityliving.level.purpurConfig.endermanAggroEndermites && entityliving instanceof Endermite endermite && (!entityliving.level.purpurConfig.endermanAggroEndermitesOnlyIfPlayerSpawned || endermite.isPlayerSpawned()))); // Purpur + this.targetSelector.addGoal(4, new ResetUniversalAngerTargetGoal<>(this, false)); + } + +@@ -241,7 +270,7 @@ public class EnderMan extends Monster implements NeutralMob { + // Paper end + ItemStack itemstack = (ItemStack) player.getInventory().armor.get(3); + +- if (itemstack.is(Blocks.CARVED_PUMPKIN.asItem())) { ++ if (this.level.purpurConfig.endermanDisableStareAggro || itemstack.is(Blocks.CARVED_PUMPKIN.asItem()) || (this.level.purpurConfig.endermanIgnorePlayerDragonHead && itemstack.is(net.minecraft.world.item.Items.DRAGON_HEAD))) { // Purpur + return false; + } else { + Vec3 vec3d = player.getViewVector(1.0F).normalize(); +@@ -278,12 +307,12 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.endermanTakeDamageFromWater; // Purpur + } + + @Override + protected void customServerAiStep() { +- if (this.level.isDay() && this.tickCount >= this.targetChangeTime + 600) { ++ if ((getRider() == null || !this.isControllable()) && this.level.isDay() && this.tickCount >= this.targetChangeTime + 600) { // Purpur - no random teleporting + float f = this.getLightLevelDependentMagicValue(); + + if (f > 0.5F && this.level.canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.RUNAWAY)) { // Paper +@@ -404,7 +433,9 @@ public class EnderMan extends Monster implements NeutralMob { + public boolean hurt(DamageSource source, float amount) { + if (this.isInvulnerableTo(source)) { + return false; +- } else if (source instanceof IndirectEntityDamageSource) { ++ } else if (getRider() != null && this.isControllable()) { return super.hurt(source, amount); // Purpur - no teleporting on damage ++ } else if (org.purpurmc.purpur.PurpurConfig.endermanShortHeight && source == DamageSource.IN_WALL) { return false; // Purpur - no suffocation damage if short height ++ } else if (source instanceof IndirectEntityDamageSource && !(this.level.purpurConfig.endermanIgnoreProjectiles && source.getDirectEntity() instanceof net.minecraft.world.entity.projectile.Projectile)) { // Purpur + Entity entity = source.getDirectEntity(); + boolean flag; + +@@ -467,7 +498,7 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean requiresCustomPersistence() { +- return super.requiresCustomPersistence() || this.getCarriedBlock() != null; ++ return super.requiresCustomPersistence() || (!this.level.purpurConfig.endermanDespawnEvenWithBlock && this.getCarriedBlock() != null); // Purpur + } + + private static class EndermanFreezeWhenLookedAt extends Goal { +@@ -514,7 +545,16 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean canUse() { +- return this.enderman.getCarriedBlock() == null ? false : (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0); ++ if (!enderman.level.purpurConfig.endermanAllowGriefing) return false; // Purpur ++ // Purpur start ++ if (this.enderman.getCarriedBlock() == null) { ++ return false; ++ } ++ if (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !this.enderman.level.purpurConfig.endermanBypassMobGriefing) { ++ return false; ++ } ++ return this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0; ++ // Purpur end + } + + @Override +@@ -561,7 +601,16 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean canUse() { +- return this.enderman.getCarriedBlock() != null ? false : (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0); ++ if (!enderman.level.purpurConfig.endermanAllowGriefing) return false; // Purpur ++ // Purpur start ++ if (this.enderman.getCarriedBlock() != null) { ++ return false; ++ } ++ if (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !this.enderman.level.purpurConfig.endermanBypassMobGriefing) { ++ return false; ++ } ++ return this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0; ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Endermite.java b/src/main/java/net/minecraft/world/entity/monster/Endermite.java +index e8c3972b889fd6b348a5b0d18444d28faa813879..c8696832f16e6c4a106befde471ef032bc40c891 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Endermite.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Endermite.java +@@ -31,20 +31,63 @@ import net.minecraft.world.level.block.state.BlockState; + public class Endermite extends Monster { + private static final int MAX_LIFE = 2400; + public int life; ++ private boolean isPlayerSpawned; // Purpur + + public Endermite(EntityType type, Level world) { + super(type, world); + this.xpReward = 3; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.endermiteRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.endermiteRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.endermiteControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.endermiteMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.endermiteTakeDamageFromWater; ++ } ++ ++ public boolean isPlayerSpawned() { ++ return this.isPlayerSpawned; ++ } ++ ++ public void setPlayerSpawned(boolean playerSpawned) { ++ this.isPlayerSpawned = playerSpawned; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.endermiteAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new ClimbOnTopOfPowderSnowGoal(this, this.level)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(3, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } +@@ -87,12 +130,14 @@ public class Endermite extends Monster { + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.life = nbt.getInt("Lifetime"); ++ this.isPlayerSpawned = nbt.getBoolean("PlayerSpawned"); // Purpur + } + + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("Lifetime", this.life); ++ nbt.putBoolean("PlayerSpawned", this.isPlayerSpawned); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Evoker.java b/src/main/java/net/minecraft/world/entity/monster/Evoker.java +index d16988e854c327e3c33b4c7c0d3546468526cfd0..9326096e2459abba9db19988b4d02c99779dd882 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Evoker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Evoker.java +@@ -48,10 +48,43 @@ public class Evoker extends SpellcasterIllager { + this.xpReward = 10; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.evokerRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.evokerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.evokerControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.evokerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.evokerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.evokerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Evoker.EvokerCastingSpellGoal()); + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 0.6D, 1.0D)); + this.goalSelector.addGoal(4, new Evoker.EvokerSummonSpellGoal()); +@@ -60,6 +93,7 @@ public class Evoker extends SpellcasterIllager { + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300)); + this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300)); +@@ -317,7 +351,7 @@ public class Evoker extends SpellcasterIllager { + return false; + } else if (Evoker.this.tickCount < this.nextAttackTickCount) { + return false; +- } else if (!Evoker.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ } else if (!Evoker.this.level.purpurConfig.evokerBypassMobGriefing && !Evoker.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } else { + List list = Evoker.this.level.getNearbyEntities(Sheep.class, this.wololoTargeting, Evoker.this, Evoker.this.getBoundingBox().inflate(16.0D, 4.0D, 16.0D)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Ghast.java b/src/main/java/net/minecraft/world/entity/monster/Ghast.java +index bb2cb17e4e5ce142eeec18951c8948e3d6b3209c..225a4e549c2cbf64beaba52d26b196af5b868433 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Ghast.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Ghast.java +@@ -44,11 +44,62 @@ public class Ghast extends FlyingMob implements Enemy { + this.moveControl = new Ghast.GhastMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.ghastRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.ghastRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.ghastControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.ghastMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.ghastMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.ghastTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.ghastAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(5, new Ghast.RandomFloatAroundGoal(this)); + this.goalSelector.addGoal(7, new Ghast.GhastLookGoal(this)); + this.goalSelector.addGoal(7, new Ghast.GhastShootFireballGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> { + return Math.abs(entityliving.getY() - this.getY()) <= 4.0D; + })); +@@ -103,7 +154,7 @@ public class Ghast extends FlyingMob implements Enemy { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + @Override +@@ -160,7 +211,7 @@ public class Ghast extends FlyingMob implements Enemy { + return 2.6F; + } + +- private static class GhastMoveControl extends MoveControl { ++ private static class GhastMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + private final Ghast ghast; + private int floatDuration; +@@ -171,7 +222,7 @@ public class Ghast extends FlyingMob implements Enemy { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO) { + if (this.floatDuration-- <= 0) { + this.floatDuration += this.ghast.getRandom().nextInt(5) + 2; +diff --git a/src/main/java/net/minecraft/world/entity/monster/Giant.java b/src/main/java/net/minecraft/world/entity/monster/Giant.java +index 41004c28edb748e12c4f868aa07b4672891197c1..2511ca42039fa91483a316ae13bb7da54f312f13 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Giant.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Giant.java +@@ -1,18 +1,123 @@ + package net.minecraft.world.entity.monster; + + import net.minecraft.core.BlockPos; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.Difficulty; ++import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.entity.EntityDimensions; + import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.EquipmentSlot; ++import net.minecraft.world.entity.MobSpawnType; + import net.minecraft.world.entity.Pose; ++import net.minecraft.world.entity.SpawnGroupData; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.ai.goal.FloatGoal; ++import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; ++import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; ++import net.minecraft.world.entity.ai.goal.MoveTowardsRestrictionGoal; ++import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; ++import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; ++import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; ++import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; ++import net.minecraft.world.entity.animal.IronGolem; ++import net.minecraft.world.entity.animal.Turtle; ++import net.minecraft.world.entity.npc.Villager; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelReader; ++import net.minecraft.world.level.ServerLevelAccessor; ++ ++import javax.annotation.Nullable; + + public class Giant extends Monster { + public Giant(EntityType type, Level world) { + super(type, world); ++ this.safeFallDistance = 10.0F; // Purpur ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.giantRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.giantRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.giantControllable; ++ } ++ ++ @Override ++ protected void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.giantMaxHealth); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level.purpurConfig.giantMovementSpeed); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(this.level.purpurConfig.giantAttackDamage); ++ } ++ ++ @Override ++ protected void registerGoals() { ++ if (level.purpurConfig.giantHaveAI) { ++ this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); ++ this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 16.0F)); ++ this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0D)); ++ if (level.purpurConfig.giantHaveHostileAI) { ++ this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers(ZombifiedPiglin.class)); ++ this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); ++ this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Villager.class, false)); ++ this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); ++ this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, true)); ++ } ++ } ++ } ++ ++ @Override ++ public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { ++ SpawnGroupData groupData = super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); ++ if (groupData == null) { ++ populateDefaultEquipmentSlots(this.random, difficulty); ++ populateDefaultEquipmentEnchantments(this.random, difficulty); ++ } ++ return groupData; ++ } ++ ++ @Override ++ protected void populateDefaultEquipmentSlots(net.minecraft.util.RandomSource random, DifficultyInstance difficulty) { ++ super.populateDefaultEquipmentSlots(this.random, difficulty); ++ // TODO make configurable ++ if (random.nextFloat() < (level.getDifficulty() == Difficulty.HARD ? 0.1F : 0.05F)) { ++ this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_SWORD)); ++ } ++ } ++ ++ @Override ++ public float getJumpPower() { ++ // make giants jump as high as everything else relative to their size ++ // 1.0 makes bottom of feet about as high as their waist when they jump ++ return level.purpurConfig.giantJumpHeight; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.giantTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.giantAlwaysDropExp; + } ++ // Purpur end + + @Override + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { +@@ -25,6 +130,6 @@ public class Giant extends Monster { + + @Override + public float getWalkTargetValue(BlockPos pos, LevelReader world) { +- return world.getPathfindingCostFromLightLevels(pos); ++ return super.getWalkTargetValue(pos, world); // Purpur - fix light requirements for natural spawns + } + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Guardian.java b/src/main/java/net/minecraft/world/entity/monster/Guardian.java +index 2f43700c01a0f0a4749f56d3f6294cf648b10f51..ca0696c9237e71d366aac399f335304ad768d03d 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Guardian.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Guardian.java +@@ -65,14 +65,55 @@ public class Guardian extends Monster { + this.xpReward = 10; + this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F); + this.moveControl = new Guardian.GuardianMoveControl(this); ++ // Purpur start ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void setYawPitch(float yaw, float pitch) { ++ super.setYawPitch(yaw, pitch * 0.35F); ++ } ++ }; ++ // Purpur end + this.clientSideTailAnimation = this.random.nextFloat(); + this.clientSideTailAnimationO = this.clientSideTailAnimation; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.guardianRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return true; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.guardianControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.guardianMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.guardianTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.guardianAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + MoveTowardsRestrictionGoal moveTowardsRestrictionGoal = new MoveTowardsRestrictionGoal(this, 1.0D); + this.randomStrollGoal = new RandomStrollGoal(this, 1.0D, 80); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Guardian.GuardianAttackGoal(this)); + this.goalSelector.addGoal(5, moveTowardsRestrictionGoal); + this.goalSelector.addGoal(7, this.randomStrollGoal); +@@ -81,6 +122,7 @@ public class Guardian extends Monster { + this.goalSelector.addGoal(9, new RandomLookAroundGoal(this)); + this.randomStrollGoal.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK)); + moveTowardsRestrictionGoal.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 10, true, false, new Guardian.GuardianAttackSelector(this))); + } + +@@ -330,7 +372,7 @@ public class Guardian extends Monster { + @Override + public void travel(Vec3 movementInput) { + if (this.isEffectiveAi() && this.isInWater()) { +- this.moveRelative(0.1F, movementInput); ++ this.moveRelative(getRider() != null && this.isControllable() ? getSpeed() : 0.1F, movementInput); // Purpur + this.move(MoverType.SELF, this.getDeltaMovement()); + this.setDeltaMovement(this.getDeltaMovement().scale(0.9D)); + if (!this.isMoving() && this.getTarget() == null) { +@@ -437,7 +479,7 @@ public class Guardian extends Monster { + } + } + +- static class GuardianMoveControl extends MoveControl { ++ static class GuardianMoveControl extends org.purpurmc.purpur.controller.WaterMoveControllerWASD { // Purpur + private final Guardian guardian; + + public GuardianMoveControl(Guardian guardian) { +@@ -445,8 +487,17 @@ public class Guardian extends Monster { + this.guardian = guardian; + } + ++ // Purpur start + @Override +- public void tick() { ++ public void purpurTick(Player rider) { ++ super.purpurTick(rider); ++ guardian.setDeltaMovement(guardian.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ guardian.setMoving(guardian.getForwardMot() > 0.0F); // control tail speed ++ } ++ // Purpur end ++ ++ @Override ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO && !this.guardian.getNavigation().isDone()) { + Vec3 vec3 = new Vec3(this.wantedX - this.guardian.getX(), this.wantedY - this.guardian.getY(), this.wantedZ - this.guardian.getZ()); + double d = vec3.length(); +@@ -456,7 +507,7 @@ public class Guardian extends Monster { + float h = (float)(Mth.atan2(vec3.z, vec3.x) * (double)(180F / (float)Math.PI)) - 90.0F; + this.guardian.setYRot(this.rotlerp(this.guardian.getYRot(), h, 90.0F)); + this.guardian.yBodyRot = this.guardian.getYRot(); +- float i = (float)(this.speedModifier * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float i = (float)(this.getSpeedModifier() * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + float j = Mth.lerp(0.125F, this.guardian.getSpeed(), i); + this.guardian.setSpeed(j); + double k = Math.sin((double)(this.guardian.tickCount + this.guardian.getId()) * 0.5D) * 0.05D; +diff --git a/src/main/java/net/minecraft/world/entity/monster/Husk.java b/src/main/java/net/minecraft/world/entity/monster/Husk.java +index 4996347c6dde85a2dc9aa37fdf495160093fac64..c865717f915f1bf27a07e09215322bdc6df7e909 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Husk.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Husk.java +@@ -20,15 +20,68 @@ public class Husk extends Zombie { + + public Husk(EntityType type, Level world) { + super(type, world); ++ this.setShouldBurnInDay(false); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.huskRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.huskRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.huskControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.huskMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.huskSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.huskJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.huskJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.huskJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.huskTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.huskAlwaysDropExp; ++ } ++ // Purpur end ++ + public static boolean checkHuskSpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + return checkMonsterSpawnRules(type, world, spawnReason, pos, random) && (spawnReason == MobSpawnType.SPAWNER || world.canSeeSky(pos)); + } + + @Override + public boolean isSunSensitive() { +- return false; ++ return this.shouldBurnInDay; // Purpur - moved to LivingEntity - keep methods for ABI compatibility + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java +index 86f7fdd42461db151221d2c0d5cff6953392fa80..4d50e9d2b9b06cae0fe135cc91a90919e82a26cb 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java +@@ -59,10 +59,45 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.illusionerRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.illusionerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.illusionerControllable; ++ } ++ ++ @Override ++ protected void initAttributes() { ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level.purpurConfig.illusionerMovementSpeed); ++ this.getAttribute(Attributes.FOLLOW_RANGE).setBaseValue(this.level.purpurConfig.illusionerFollowRange); ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.illusionerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.illusionerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.illusionerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new SpellcasterIllager.SpellcasterCastingSpellGoal()); + this.goalSelector.addGoal(4, new Illusioner.IllusionerMirrorSpellGoal()); + this.goalSelector.addGoal(5, new Illusioner.IllusionerBlindnessSpellGoal()); +@@ -70,6 +105,7 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob { + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300)); + this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java +index ea4fa9eba301e462c159cdb970079f6d87d25f4d..2111a99d23d86f5f2e2ce8101dbbf292671a5c47 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java ++++ b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java +@@ -25,6 +25,58 @@ public class MagmaCube extends Slime { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.magmaCubeRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.magmaCubeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.magmaCubeControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return 0.42F * this.getBlockJumpFactor(); // from EntityLiving ++ } ++ ++ @Override ++ protected String getMaxHealthEquation() { ++ return level.purpurConfig.magmaCubeMaxHealth; ++ } ++ ++ @Override ++ protected String getAttackDamageEquation() { ++ return level.purpurConfig.magmaCubeAttackDamage; ++ } ++ ++ @Override ++ protected java.util.Map getMaxHealthCache() { ++ return level.purpurConfig.magmaCubeMaxHealthCache; ++ } ++ ++ @Override ++ protected java.util.Map getAttackDamageCache() { ++ return level.purpurConfig.magmaCubeAttackDamageCache; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.magmaCubeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.magmaCubeAlwaysDropExp; ++ } ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, (double)0.2F); + } +@@ -70,10 +122,11 @@ public class MagmaCube extends Slime { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3 = this.getDeltaMovement(); + this.setDeltaMovement(vec3.x, (double)(this.getJumpPower() + (float)this.getSize() * 0.1F), vec3.z); + this.hasImpulse = true; ++ this.actualJump = false; // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Monster.java b/src/main/java/net/minecraft/world/entity/monster/Monster.java +index 55c245d0dfa369dc6de2197ae37335fba4fae4ae..c9b40515f4c2ff1eedfc9510930c3baebc078ebd 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Monster.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Monster.java +@@ -89,6 +89,14 @@ public abstract class Monster extends PathfinderMob implements Enemy { + } + + public static boolean isDarkEnoughToSpawn(ServerLevelAccessor world, BlockPos pos, RandomSource random) { ++ // Purpur start ++ if (!world.getMinecraftWorld().purpurConfig.mobsSpawnOnPackedIce || !world.getMinecraftWorld().purpurConfig.mobsSpawnOnBlueIce) { ++ net.minecraft.world.level.block.state.BlockState spawnBlock = world.getBlockState(pos.below()); ++ if ((!world.getMinecraftWorld().purpurConfig.mobsSpawnOnPackedIce && spawnBlock.is(net.minecraft.world.level.block.Blocks.PACKED_ICE)) || (!world.getMinecraftWorld().purpurConfig.mobsSpawnOnBlueIce && spawnBlock.is(net.minecraft.world.level.block.Blocks.BLUE_ICE))) { ++ return false; ++ } ++ } ++ // Purpur end + if (world.getBrightness(LightLayer.SKY, pos) > random.nextInt(32)) { + return false; + } else { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Phantom.java b/src/main/java/net/minecraft/world/entity/monster/Phantom.java +index aa8734856ec7b90036afad13bfda46c02e548812..cf6f7c6c5754ab712e06a7bfb8c1ef8e719879de 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Phantom.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Phantom.java +@@ -49,6 +49,8 @@ public class Phantom extends FlyingMob implements Enemy { + Vec3 moveTargetPoint; + public BlockPos anchorPoint; + Phantom.AttackPhase attackPhase; ++ Vec3 crystalPosition; // Purpur ++ private static final net.minecraft.world.item.crafting.Ingredient TORCH = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.item.Items.TORCH, net.minecraft.world.item.Items.SOUL_TORCH); // Purpur + + public Phantom(EntityType type, Level world) { + super(type, world); +@@ -58,8 +60,110 @@ public class Phantom extends FlyingMob implements Enemy { + this.xpReward = 5; + this.moveControl = new Phantom.PhantomMoveControl(this); + this.lookControl = new Phantom.PhantomLookControl(this); ++ this.setShouldBurnInDay(true); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.phantomRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.phantomRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.phantomControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.phantomMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, speed, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() { ++ return Monster.createMonsterAttributes().add(Attributes.FLYING_SPEED, 3.0D); ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.special.phantom")) { ++ shoot(); ++ } ++ return false; ++ } ++ ++ public boolean shoot() { ++ org.bukkit.Location loc = ((org.bukkit.entity.LivingEntity) getBukkitEntity()).getEyeLocation(); ++ loc.setPitch(-loc.getPitch()); ++ org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(100).add(loc.toVector()); ++ ++ org.purpurmc.purpur.entity.PhantomFlames flames = new org.purpurmc.purpur.entity.PhantomFlames(level, this); ++ flames.canGrief = level.purpurConfig.phantomAllowGriefing; ++ flames.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), 1.0F, 5.0F); ++ level.addFreshEntity(flames); ++ return true; ++ } ++ ++ private double getFromCache(java.util.function.Supplier equation, java.util.function.Supplier> cache, java.util.function.Supplier defaultValue) { ++ int size = getPhantomSize(); ++ Double value = cache.get().get(size); ++ if (value == null) { ++ try { ++ scriptEngine.eval("size = " + size); ++ value = (double) scriptEngine.eval(equation.get()); ++ } catch (Exception e) { ++ value = defaultValue.get(); ++ } ++ cache.get().put(size, value); ++ } ++ return value; ++ } ++ ++ @Override ++ protected net.minecraft.world.level.storage.loot.LootContext.Builder createLootContext(boolean causedByPlayer, DamageSource source) { ++ boolean dropped = false; ++ if (lastHurtByPlayer == null && source.getEntity() instanceof net.minecraft.world.entity.boss.enderdragon.EndCrystal) { ++ if (random.nextInt(5) < 1) { ++ dropped = spawnAtLocation(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.PHANTOM_MEMBRANE)) != null; ++ } ++ } ++ if (!dropped) { ++ return super.createLootContext(causedByPlayer, source); ++ } ++ return new net.minecraft.world.level.storage.loot.LootContext.Builder((net.minecraft.server.level.ServerLevel) level); ++ } ++ ++ public boolean isCirclingCrystal() { ++ return crystalPosition != null; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.phantomTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.phantomAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return (this.getUniqueFlapTickOffset() + this.tickCount) % Phantom.TICKS_PER_FLAP == 0; +@@ -72,9 +176,17 @@ public class Phantom extends FlyingMob implements Enemy { + + @Override + protected void registerGoals() { +- this.goalSelector.addGoal(1, new Phantom.PhantomAttackStrategyGoal()); +- this.goalSelector.addGoal(2, new Phantom.PhantomSweepAttackGoal()); +- this.goalSelector.addGoal(3, new Phantom.PhantomCircleAroundAnchorGoal()); ++ // Purpur start ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ if (level.purpurConfig.phantomOrbitCrystalRadius > 0) { ++ this.goalSelector.addGoal(1, new FindCrystalGoal(this)); ++ this.goalSelector.addGoal(2, new OrbitCrystalGoal(this)); ++ } ++ this.goalSelector.addGoal(3, new Phantom.PhantomAttackStrategyGoal()); ++ this.goalSelector.addGoal(4, new Phantom.PhantomSweepAttackGoal()); ++ this.goalSelector.addGoal(5, new Phantom.PhantomCircleAroundAnchorGoal()); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ // Purpur end + this.targetSelector.addGoal(1, new Phantom.PhantomAttackPlayerTargetGoal()); + } + +@@ -90,7 +202,10 @@ public class Phantom extends FlyingMob implements Enemy { + + private void updatePhantomSizeInfo() { + this.refreshDimensions(); +- this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue((double) (6 + this.getPhantomSize())); ++ // Purpur start ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(getFromCache(() -> this.level.purpurConfig.phantomMaxHealth, () -> this.level.purpurConfig.phantomMaxHealthCache, () -> 20.0D)); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(getFromCache(() -> this.level.purpurConfig.phantomAttackDamage, () -> this.level.purpurConfig.phantomAttackDamageCache, () -> (double) 6 + this.getPhantomSize())); ++ // Purpur end + } + + public int getPhantomSize() { +@@ -140,14 +255,12 @@ public class Phantom extends FlyingMob implements Enemy { + this.level.addParticle(ParticleTypes.MYCELIUM, this.getX() - (double) f2, this.getY() + (double) f4, this.getZ() - (double) f3, 0.0D, 0.0D, 0.0D); + } + ++ if (level.purpurConfig.phantomFlamesOnSwoop && attackPhase == AttackPhase.SWOOP) shoot(); // Purpur + } + + @Override + public void aiStep() { +- if (this.isAlive() && shouldBurnInDay && this.isSunBurnTick()) { // Paper - Configurable Burning +- this.setSecondsOnFire(8); +- } +- ++ // Purpur - moved down to shouldBurnInDay() + super.aiStep(); + } + +@@ -159,7 +272,11 @@ public class Phantom extends FlyingMob implements Enemy { + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { + this.anchorPoint = this.blockPosition().above(5); +- this.setPhantomSize(0); ++ // Purpur start ++ int min = world.getLevel().purpurConfig.phantomMinSize; ++ int max = world.getLevel().purpurConfig.phantomMaxSize; ++ this.setPhantomSize(min == max ? min : world.getRandom().nextInt(max + 1 - min) + min); ++ // Purpur end + return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); + } + +@@ -175,7 +292,7 @@ public class Phantom extends FlyingMob implements Enemy { + if (nbt.hasUUID("Paper.SpawningEntity")) { + this.spawningEntity = nbt.getUUID("Paper.SpawningEntity"); + } +- if (nbt.contains("Paper.ShouldBurnInDay")) { ++ if (false && nbt.contains("Paper.ShouldBurnInDay")) { // Purpur - implemented in LivingEntity + this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); + } + // Paper end +@@ -192,7 +309,7 @@ public class Phantom extends FlyingMob implements Enemy { + if (this.spawningEntity != null) { + nbt.putUUID("Paper.SpawningEntity", this.spawningEntity); + } +- nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay); ++ // nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay); // Purpur - implemented in LivingEntity + // Paper end + } + +@@ -253,8 +370,14 @@ public class Phantom extends FlyingMob implements Enemy { + } + public void setSpawningEntity(java.util.UUID entity) { this.spawningEntity = entity; } + +- private boolean shouldBurnInDay = true; +- public boolean shouldBurnInDay() { return shouldBurnInDay; } ++ // private boolean shouldBurnInDay = true; // Purpur - moved to LivingEntity - keep methods for ABI compatibility ++ // Purpur start ++ public boolean shouldBurnInDay() { ++ boolean burnFromDaylight = this.shouldBurnInDay && this.level.purpurConfig.phantomBurnInDaylight; ++ boolean burnFromLightSource = this.level.purpurConfig.phantomBurnInLight > 0 && this.level.getMaxLocalRawBrightness(blockPosition()) >= this.level.purpurConfig.phantomBurnInLight; ++ return burnFromDaylight || burnFromLightSource; ++ } ++ // Purpur End + public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } + // Paper end + private static enum AttackPhase { +@@ -264,7 +387,125 @@ public class Phantom extends FlyingMob implements Enemy { + private AttackPhase() {} + } + +- private class PhantomMoveControl extends MoveControl { ++ // Purpur start ++ class FindCrystalGoal extends Goal { ++ private final Phantom phantom; ++ private net.minecraft.world.entity.boss.enderdragon.EndCrystal crystal; ++ private Comparator comparator; ++ ++ FindCrystalGoal(Phantom phantom) { ++ this.phantom = phantom; ++ this.comparator = Comparator.comparingDouble(phantom::distanceToSqr); ++ this.setFlags(EnumSet.of(Flag.LOOK)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ double range = maxTargetRange(); ++ List crystals = level.getEntitiesOfClass(net.minecraft.world.entity.boss.enderdragon.EndCrystal.class, phantom.getBoundingBox().inflate(range)); ++ if (crystals.isEmpty()) { ++ return false; ++ } ++ crystals.sort(comparator); ++ crystal = crystals.get(0); ++ if (phantom.distanceToSqr(crystal) > range * range) { ++ crystal = null; ++ return false; ++ } ++ return true; ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ if (crystal == null || !crystal.isAlive()) { ++ return false; ++ } ++ double range = maxTargetRange(); ++ return phantom.distanceToSqr(crystal) <= (range * range) * 2; ++ } ++ ++ @Override ++ public void start() { ++ phantom.crystalPosition = new Vec3(crystal.getX(), crystal.getY() + (phantom.random.nextInt(10) + 10), crystal.getZ()); ++ } ++ ++ @Override ++ public void stop() { ++ crystal = null; ++ phantom.crystalPosition = null; ++ super.stop(); ++ } ++ ++ private double maxTargetRange() { ++ return phantom.level.purpurConfig.phantomOrbitCrystalRadius; ++ } ++ } ++ ++ class OrbitCrystalGoal extends Goal { ++ private final Phantom phantom; ++ private float offset; ++ private float radius; ++ private float verticalChange; ++ private float direction; ++ ++ OrbitCrystalGoal(Phantom phantom) { ++ this.phantom = phantom; ++ this.setFlags(EnumSet.of(Flag.MOVE)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ return phantom.isCirclingCrystal(); ++ } ++ ++ @Override ++ public void start() { ++ this.radius = 5.0F + phantom.random.nextFloat() * 10.0F; ++ this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F; ++ this.direction = phantom.random.nextBoolean() ? 1.0F : -1.0F; ++ updateOffset(); ++ } ++ ++ @Override ++ public void tick() { ++ if (phantom.random.nextInt(350) == 0) { ++ this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F; ++ } ++ if (phantom.random.nextInt(250) == 0) { ++ ++this.radius; ++ if (this.radius > 15.0F) { ++ this.radius = 5.0F; ++ this.direction = -this.direction; ++ } ++ } ++ if (phantom.random.nextInt(450) == 0) { ++ this.offset = phantom.random.nextFloat() * 2.0F * 3.1415927F; ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.distanceToSqr(phantom.getX(), phantom.getY(), phantom.getZ()) < 4.0D) { ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.y < phantom.getY() && !phantom.level.isEmptyBlock(new BlockPos(phantom).below(1))) { ++ this.verticalChange = Math.max(1.0F, this.verticalChange); ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.y > phantom.getY() && !phantom.level.isEmptyBlock(new BlockPos(phantom).above(1))) { ++ this.verticalChange = Math.min(-1.0F, this.verticalChange); ++ updateOffset(); ++ } ++ } ++ ++ private void updateOffset() { ++ this.offset += this.direction * 15.0F * 0.017453292F; ++ phantom.moveTargetPoint = phantom.crystalPosition.add( ++ this.radius * Mth.cos(this.offset), ++ -4.0F + this.verticalChange, ++ this.radius * Mth.sin(this.offset)); ++ } ++ } ++ // Purpur end ++ ++ private class PhantomMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + private float speed = 0.1F; + +@@ -272,8 +513,19 @@ public class Phantom extends FlyingMob implements Enemy { + super(entity); + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ if (!Phantom.this.onGround) { ++ // phantom is always in motion when flying ++ // TODO - FIX THIS ++ // rider.setForward(1.0F); ++ } ++ super.purpurTick(rider); ++ } ++ // Purpur end ++ + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (Phantom.this.horizontalCollision) { + Phantom.this.setYRot(Phantom.this.getYRot() + 180.0F); + this.speed = 0.1F; +@@ -319,14 +571,20 @@ public class Phantom extends FlyingMob implements Enemy { + } + } + +- private class PhantomLookControl extends LookControl { ++ private class PhantomLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + public PhantomLookControl(Mob entity) { + super(entity); + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot(), -rider.xRotO * 0.75F); ++ } ++ // Purpur end ++ + @Override +- public void tick() {} ++ public void vanillaTick() {} // Purpur + } + + private class PhantomBodyRotationControl extends BodyRotationControl { +@@ -413,6 +671,12 @@ public class Phantom extends FlyingMob implements Enemy { + return false; + } else if (!entityliving.isAlive()) { + return false; ++ // Purpur start ++ } else if (level.purpurConfig.phantomBurnInLight > 0 && level.getLightEmission(new BlockPos(Phantom.this)) >= level.purpurConfig.phantomBurnInLight) { ++ return false; ++ } else if (level.purpurConfig.phantomIgnorePlayersWithTorch && (TORCH.test(entityliving.getItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND)) || TORCH.test(entityliving.getItemInHand(net.minecraft.world.InteractionHand.OFF_HAND)))) { ++ return false; ++ // Purpur end + } else { + if (entityliving instanceof Player) { + Player entityhuman = (Player) entityliving; +@@ -558,6 +822,7 @@ public class Phantom extends FlyingMob implements Enemy { + this.nextScanTick = reducedTickDelay(60); + List list = Phantom.this.level.getNearbyPlayers(this.attackTargeting, Phantom.this, Phantom.this.getBoundingBox().inflate(16.0D, 64.0D, 16.0D)); + ++ if (level.purpurConfig.phantomIgnorePlayersWithTorch) list.removeIf(human -> TORCH.test(human.getItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND)) || TORCH.test(human.getItemInHand(net.minecraft.world.InteractionHand.OFF_HAND)));// Purpur + if (!list.isEmpty()) { + list.sort(Comparator.comparing((Entity e) -> { return e.getY(); }).reversed()); // CraftBukkit - decompile error + Iterator iterator = list.iterator(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Pillager.java b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +index cec545c3baa6599d47b9cf1a4b97de8771062a22..1d4fed01ee94678e04962df0f086f53edf3f43a4 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Pillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +@@ -62,15 +62,49 @@ public class Pillager extends AbstractIllager implements CrossbowAttackMob, Inve + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.pillagerRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.pillagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.pillagerControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.pillagerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.pillagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.pillagerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new Raider.HoldGroundAttackGoal(this, 10.0F)); + this.goalSelector.addGoal(3, new RangedCrossbowAttackGoal<>(this, 1.0D, 8.0F)); + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 15.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 15.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, Raider.class)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Ravager.java b/src/main/java/net/minecraft/world/entity/monster/Ravager.java +index 40443f7d0c9f5697f529bfbbd16695c00bbd7322..67815f8c68044b7dc05a6efd5c60dbf05bbcacbb 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Ravager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Ravager.java +@@ -69,14 +69,54 @@ public class Ravager extends Raider { + this.xpReward = 20; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.ravagerRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.ravagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.ravagerControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ getNavigation().stop(); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.ravagerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.ravagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.ravagerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Ravager.RavagerMeleeAttackGoal()); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.4D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(2, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true, (entityliving) -> { +@@ -151,7 +191,7 @@ public class Ravager extends Raider { + @Override + public void aiStep() { + super.aiStep(); +- if (this.isAlive()) { ++ if (this.isAlive() && (getRider() == null || !this.isControllable())) { // Purpur + if (this.isImmobile()) { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.0D); + } else { +@@ -161,7 +201,7 @@ public class Ravager extends Raider { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(Mth.lerp(0.1D, d1, d0)); + } + +- if (this.horizontalCollision && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.horizontalCollision && (this.level.purpurConfig.ravagerBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // Purpur + boolean flag = false; + AABB axisalignedbb = this.getBoundingBox().inflate(0.2D); + Iterator iterator = BlockPos.betweenClosed(Mth.floor(axisalignedbb.minX), Mth.floor(axisalignedbb.minY), Mth.floor(axisalignedbb.minZ), Mth.floor(axisalignedbb.maxX), Mth.floor(axisalignedbb.maxY), Mth.floor(axisalignedbb.maxZ)).iterator(); +@@ -171,7 +211,7 @@ public class Ravager extends Raider { + BlockState iblockdata = this.level.getBlockState(blockposition); + Block block = iblockdata.getBlock(); + +- if (block instanceof LeavesBlock && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock()).isCancelled()) { // CraftBukkit // Paper ++ if (this.level.purpurConfig.ravagerGriefableBlocks.contains(block) && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock()).isCancelled()) { // CraftBukkit // Paper + flag = this.level.destroyBlock(blockposition, true, this) || flag; + } + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +index 9dedb3599d5388c1bcc34558e8e5cb4a8668646f..a31ce1f7ba5150c11cee58599d92241194f1bef2 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +@@ -21,6 +21,8 @@ import net.minecraft.sounds.SoundSource; + import net.minecraft.util.Mth; + import net.minecraft.world.Difficulty; + import net.minecraft.world.DifficultyInstance; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntityDimensions; +@@ -49,6 +51,8 @@ import net.minecraft.world.entity.projectile.AbstractArrow; + import net.minecraft.world.entity.projectile.ShulkerBullet; + import net.minecraft.world.entity.vehicle.Boat; + import net.minecraft.world.item.DyeColor; ++import net.minecraft.world.item.DyeItem; ++import net.minecraft.world.item.ItemStack; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.ServerLevelAccessor; + import net.minecraft.world.level.block.Blocks; +@@ -94,12 +98,59 @@ public class Shulker extends AbstractGolem implements VariantHolder= f) { ++ if ((!this.level.purpurConfig.shulkerSpawnFromBulletRequireOpenLid || !this.isClosed()) && this.teleportSomewhere()) { ++ // Purpur start ++ float chance = this.level.purpurConfig.shulkerSpawnFromBulletBaseChance; ++ if (!this.level.purpurConfig.shulkerSpawnFromBulletNearbyEquation.isBlank()) { ++ int nearby = this.level.getEntities((EntityTypeTest) EntityType.SHULKER, axisalignedbb.inflate(this.level.purpurConfig.shulkerSpawnFromBulletNearbyRange), Entity::isAlive).size(); ++ try { ++ scriptEngine.eval("nearby = " + nearby); ++ chance -= (float) scriptEngine.eval(this.level.purpurConfig.shulkerSpawnFromBulletNearbyEquation); ++ } catch (Exception ignore) { ++ chance -= (float) (nearby - 1) / 5.0F; ++ } ++ } ++ if (this.level.random.nextFloat() <= chance) { ++ // Purpur end + Shulker entityshulker = (Shulker) EntityType.SHULKER.create(this.level); + + if (entityshulker != null) { +@@ -597,7 +657,7 @@ public class Shulker extends AbstractGolem implements VariantHolder getVariant() { +- return Optional.ofNullable(this.getColor()); ++ return Optional.ofNullable(this.level.purpurConfig.shulkerSpawnFromBulletRandomColor ? DyeColor.random(this.level.random) : this.getColor()); // Purpur + } + + @Nullable +@@ -607,7 +667,7 @@ public class Shulker extends AbstractGolem implements VariantHolder(this, Player.class, true)); + } +@@ -184,7 +218,7 @@ public class Silverfish extends Monster { + continue; + } + // CraftBukkit end +- if (world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (world.purpurConfig.silverfishBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + world.destroyBlock(blockposition1, true, this.silverfish); + } else { + world.setBlock(blockposition1, ((InfestedBlock) block).hostStateByInfested(world.getBlockState(blockposition1)), 3); +@@ -222,7 +256,7 @@ public class Silverfish extends Monster { + } else { + RandomSource randomsource = this.mob.getRandom(); + +- if (this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && randomsource.nextInt(reducedTickDelay(10)) == 0) { ++ if ((this.mob.level.purpurConfig.silverfishBypassMobGriefing || this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && randomsource.nextInt(reducedTickDelay(10)) == 0) { // Purpur + this.selectedDirection = Direction.getRandom(randomsource); + BlockPos blockposition = (new BlockPos(this.mob.getX(), this.mob.getY() + 0.5D, this.mob.getZ())).relative(this.selectedDirection); + BlockState iblockdata = this.mob.level.getBlockState(blockposition); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java +index badde621357a567965f0ef203e402e21bed09059..e0352f073c95f8caf47d6789c0bd10e5a8c329c8 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java +@@ -14,6 +14,16 @@ import net.minecraft.world.item.Items; + import net.minecraft.world.level.ItemLike; + import net.minecraft.world.level.Level; + ++// Purpur start ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.block.Blocks; ++import org.bukkit.craftbukkit.event.CraftEventFactory; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.core.particles.ParticleTypes; ++// Purpur end ++ + public class Skeleton extends AbstractSkeleton { + + private static final int TOTAL_CONVERSION_TIME = 300; +@@ -26,6 +36,38 @@ public class Skeleton extends AbstractSkeleton { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.skeletonRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.skeletonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.skeletonControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.skeletonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.skeletonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.skeletonAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -142,4 +184,67 @@ public class Skeleton extends AbstractSkeleton { + } + + } ++ ++ // Purpur start ++ private int witherRosesFed = 0; ++ ++ @Override ++ public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ ItemStack stack = player.getItemInHand(hand); ++ ++ if (level.purpurConfig.skeletonFeedWitherRoses > 0 && this.getType() != EntityType.WITHER_SKELETON && stack.getItem() == Blocks.WITHER_ROSE.asItem()) { ++ return this.feedWitherRose(player, stack); ++ } ++ ++ return super.mobInteract(player, hand); ++ } ++ ++ private InteractionResult feedWitherRose(Player player, ItemStack stack) { ++ if (++witherRosesFed < level.purpurConfig.skeletonFeedWitherRoses) { ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ return InteractionResult.CONSUME; ++ } ++ ++ WitherSkeleton skeleton = EntityType.WITHER_SKELETON.create(level); ++ if (skeleton == null) { ++ return InteractionResult.PASS; ++ } ++ ++ skeleton.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); ++ skeleton.setHealth(this.getHealth()); ++ skeleton.setAggressive(this.isAggressive()); ++ skeleton.copyPosition(this); ++ skeleton.setYBodyRot(this.yBodyRot); ++ skeleton.setYHeadRot(this.getYHeadRot()); ++ skeleton.yRotO = this.yRotO; ++ skeleton.xRotO = this.xRotO; ++ ++ if (this.hasCustomName()) { ++ skeleton.setCustomName(this.getCustomName()); ++ } ++ ++ if (CraftEventFactory.callEntityTransformEvent(this, skeleton, org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION).isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ ++ if (!new com.destroystokyo.paper.event.entity.EntityTransformedEvent(this.getBukkitEntity(), skeleton.getBukkitEntity(), com.destroystokyo.paper.event.entity.EntityTransformedEvent.TransformedReason.INFECTED).callEvent()) { ++ return InteractionResult.PASS; ++ } ++ ++ this.level.addFreshEntity(skeleton); ++ this.remove(RemovalReason.DISCARDED); ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ ++ for (int i = 0; i < 15; ++i) { ++ ((ServerLevel) level).sendParticles(((ServerLevel) level).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 2), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Slime.java b/src/main/java/net/minecraft/world/entity/monster/Slime.java +index 87c2e50c6f817d1a77e0cfd64366765b265f9ba0..360ad0ffd25c5d42d8d50060be40cab304c8fb32 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Slime.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Slime.java +@@ -64,6 +64,7 @@ public class Slime extends Mob implements Enemy { + public float squish; + public float oSquish; + private boolean wasOnGround; ++ protected boolean actualJump; // Purpur + + public Slime(EntityType type, Level world) { + super(type, world); +@@ -71,12 +72,89 @@ public class Slime extends Mob implements Enemy { + this.moveControl = new Slime.SlimeMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.slimeRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.slimeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.slimeControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ float height = super.getJumpPower(); ++ return getRider() != null && this.isControllable() && actualJump ? height * 1.5F : height; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (onGround && getRider() != null && this.isControllable()) { ++ actualJump = true; ++ if (getRider().getForwardMot() == 0 || getRider().getStrafeMot() == 0) { ++ jumpFromGround(); // jump() here if not moving ++ } ++ } ++ return true; // do not jump() in wasd controller, let vanilla controller handle ++ } ++ ++ protected String getMaxHealthEquation() { ++ return level.purpurConfig.slimeMaxHealth; ++ } ++ ++ protected String getAttackDamageEquation() { ++ return level.purpurConfig.slimeAttackDamage; ++ } ++ ++ protected java.util.Map getMaxHealthCache() { ++ return level.purpurConfig.slimeMaxHealthCache; ++ } ++ ++ protected java.util.Map getAttackDamageCache() { ++ return level.purpurConfig.slimeAttackDamageCache; ++ } ++ ++ protected double getFromCache(java.util.function.Supplier equation, java.util.function.Supplier> cache, java.util.function.Supplier defaultValue) { ++ int size = getSize(); ++ Double value = cache.get().get(size); ++ if (value == null) { ++ try { ++ scriptEngine.eval("size = " + size); ++ value = (double) scriptEngine.eval(equation.get()); ++ } catch (Exception e) { ++ value = defaultValue.get(); ++ } ++ cache.get().put(size, value); ++ } ++ return value; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.slimeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.slimeAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Slime.SlimeFloatGoal(this)); + this.goalSelector.addGoal(2, new Slime.SlimeAttackGoal(this)); + this.goalSelector.addGoal(3, new Slime.SlimeRandomDirectionGoal(this)); + this.goalSelector.addGoal(5, new Slime.SlimeKeepOnJumpingGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> { + return Math.abs(entityliving.getY() - this.getY()) <= 4.0D; + })); +@@ -96,9 +174,9 @@ public class Slime extends Mob implements Enemy { + this.entityData.set(Slime.ID_SIZE, j); + this.reapplyPosition(); + this.refreshDimensions(); +- this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double) (j * j)); ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(getFromCache(this::getMaxHealthEquation, this::getMaxHealthCache, () -> (double) size * size)); // Purpur + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue((double) (0.2F + 0.1F * (float) j)); +- this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue((double) j); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(getFromCache(this::getAttackDamageEquation, this::getAttackDamageCache, () -> (double) j)); // Purpur + if (heal) { + this.setHealth(this.getMaxHealth()); + } +@@ -368,11 +446,12 @@ public class Slime extends Mob implements Enemy { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(vec3d.x, (double) this.getJumpPower(), vec3d.z); + this.hasImpulse = true; ++ this.actualJump = false; // Purpur + } + + @Nullable +@@ -406,7 +485,7 @@ public class Slime extends Mob implements Enemy { + return super.getDimensions(pose).scale(0.255F * (float) this.getSize()); + } + +- private static class SlimeMoveControl extends MoveControl { ++ private static class SlimeMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private float yRot; + private int jumpDelay; +@@ -425,21 +504,33 @@ public class Slime extends Mob implements Enemy { + } + + public void setWantedMovement(double speed) { +- this.speedModifier = speed; ++ this.setSpeedModifier(speed); // Purpur + this.operation = MoveControl.Operation.MOVE_TO; + } + + @Override + public void tick() { ++ // Purpur start ++ if (slime.getRider() != null && slime.isControllable()) { ++ purpurTick(slime.getRider()); ++ if (slime.getForwardMot() != 0 || slime.getStrafeMot() != 0) { ++ if (jumpDelay > 10) { ++ jumpDelay = 6; ++ } ++ } else { ++ jumpDelay = 20; ++ } ++ } else { ++ // Purpur end + this.mob.setYRot(this.rotlerp(this.mob.getYRot(), this.yRot, 90.0F)); + this.mob.yHeadRot = this.mob.getYRot(); + this.mob.yBodyRot = this.mob.getYRot(); +- if (this.operation != MoveControl.Operation.MOVE_TO) { ++ } if ((slime.getRider() == null || !slime.isControllable()) && this.operation != MoveControl.Operation.MOVE_TO) { // Purpur + this.mob.setZza(0.0F); + } else { + this.operation = MoveControl.Operation.WAIT; + if (this.mob.isOnGround()) { +- this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); ++ this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (slime.getRider() != null && slime.isControllable() && (slime.getRider().getForwardMot() != 0 || slime.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur + if (this.jumpDelay-- <= 0) { + this.jumpDelay = this.slime.getJumpDelay(); + if (this.isAggressive) { +@@ -456,7 +547,7 @@ public class Slime extends Mob implements Enemy { + this.mob.setSpeed(0.0F); + } + } else { +- this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); ++ this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (slime.getRider() != null && slime.isControllable() && (slime.getRider().getForwardMot() != 0 || slime.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java +index d786b8b8c9d478504f74e65c3bc7ed3e9884d003..1f0ab16f718f2d499187949c5a25819120fe86f5 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java +@@ -51,14 +51,48 @@ public class Spider extends Monster { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.spiderRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.spiderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.spiderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.spiderMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.spiderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.spiderAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(3, new LeapAtTargetGoal(this, 0.4F)); + this.goalSelector.addGoal(4, new Spider.SpiderAttackGoal(this)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new Spider.SpiderTargetGoal<>(this, Player.class)); + this.targetSelector.addGoal(3, new Spider.SpiderTargetGoal<>(this, IronGolem.class)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Stray.java b/src/main/java/net/minecraft/world/entity/monster/Stray.java +index 118b636a44e4b062e812e433f603b039276337da..c1d36eb62c52c3fd8055e3b6c7d504c83fe3042e 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Stray.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Stray.java +@@ -21,6 +21,38 @@ public class Stray extends AbstractSkeleton { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.strayRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.strayRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.strayControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.strayMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.strayTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.strayAlwaysDropExp; ++ } ++ // Purpur end ++ + public static boolean checkStraySpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + BlockPos blockPos = pos; + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Strider.java b/src/main/java/net/minecraft/world/entity/monster/Strider.java +index 6a511394bb01e025d5e3cf3963618920eee74445..4aafc7d56eb669f6454e3c88189ec765f674e795 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Strider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Strider.java +@@ -90,12 +90,44 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + super(type, world); + this.steering = new ItemBasedSteering(this.entityData, Strider.DATA_BOOST_TIME, Strider.DATA_SADDLE_ID); + this.blocksBuilding = true; +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + this.setPathfindingMalus(BlockPathTypes.LAVA, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.striderRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.striderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.striderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.striderMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.striderBreedingTicks; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.striderAlwaysDropExp; ++ } ++ // Purpur end ++ + public static boolean checkStriderSpawnRules(EntityType type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable(); + +@@ -157,6 +189,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + @Override + protected void registerGoals() { + this.panicGoal = new PanicGoal(this, 1.65D); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, this.panicGoal); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.temptGoal = new TemptGoal(this, 1.4D, Strider.TEMPT_ITEMS, false); +@@ -386,7 +419,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.striderTakeDamageFromWater; // Purpur + } + + @Override +@@ -428,6 +461,19 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + public InteractionResult mobInteract(Player player, InteractionHand hand) { + boolean flag = this.isFood(player.getItemInHand(hand)); + ++ // Purpur start ++ if (level.purpurConfig.striderGiveSaddleBack && player.isSecondaryUseActive() && !flag && isSaddled() && !isVehicle()) { ++ this.steering.setSaddle(false); ++ if (!player.getAbilities().instabuild) { ++ ItemStack saddle = new ItemStack(Items.SADDLE); ++ if (!player.getInventory().add(saddle)) { ++ player.drop(saddle, false); ++ } ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end ++ + if (!flag && this.isSaddled() && !this.isVehicle() && !player.isSecondaryUseActive()) { + if (!this.level.isClientSide) { + player.startRiding(this); +@@ -440,7 +486,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + if (!enuminteractionresult.consumesAction()) { + ItemStack itemstack = player.getItemInHand(hand); + +- return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, this, hand) : InteractionResult.PASS; ++ return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, this, hand) : tryRide(player, hand); // Purpur + } else { + if (flag && !this.isSilent()) { + this.level.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.STRIDER_EAT, this.getSoundSource(), 1.0F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java +index 8804771d73c2521d6dff284de3464fa5788e5ffc..b368d20b6ce18b5cb9af054e1cd518c2a413fbf1 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vex.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java +@@ -62,6 +62,65 @@ public class Vex extends Monster { + this.xpReward = 3; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.vexRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.vexRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.vexControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.vexMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable()) { ++ float speed; ++ if (onGround) { ++ speed = (float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F; ++ } else { ++ speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ } ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { ++ return false; // no fall damage please ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.vexMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.vexTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.vexAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { + return dimensions.height - 0.28125F; +@@ -80,7 +139,7 @@ public class Vex extends Monster { + + @Override + public void tick() { +- this.noPhysics = true; ++ this.noPhysics = getRider() == null || !this.isControllable(); // Purpur + super.tick(); + this.noPhysics = false; + this.setNoGravity(true); +@@ -95,17 +154,19 @@ public class Vex extends Monster { + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Vex.VexChargeAttackGoal()); + this.goalSelector.addGoal(8, new Vex.VexRandomMoveGoal()); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, new Vex.VexCopyOwnerTargetGoal(this)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + public static AttributeSupplier.Builder createAttributes() { +- return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D); ++ return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur; + } + + @Override +@@ -233,14 +294,14 @@ public class Vex extends Monster { + return 0.4D; + } + +- private class VexMoveControl extends MoveControl { ++ private class VexMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + public VexMoveControl(Vex entityvex) { + super(entityvex); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO) { + Vec3 vec3d = new Vec3(this.wantedX - Vex.this.getX(), this.wantedY - Vex.this.getY(), this.wantedZ - Vex.this.getZ()); + double d0 = vec3d.length(); +@@ -249,7 +310,7 @@ public class Vex extends Monster { + this.operation = MoveControl.Operation.WAIT; + Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().scale(0.5D)); + } else { +- Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.speedModifier * 0.05D / d0))); ++ Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.getSpeedModifier() * 0.05D / d0))); // Purpur + if (Vex.this.getTarget() == null) { + Vec3 vec3d1 = Vex.this.getDeltaMovement(); + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java +index a9e75a16a7dc0ff5d4f0faa92ebc444559a39325..efbfe0a151686f00051026113c4d1f4d9c9eb241 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java +@@ -58,14 +58,48 @@ public class Vindicator extends AbstractIllager { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.vindicatorRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.vindicatorRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.vindicatorControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.vindicatorMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.vindicatorTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.vindicatorAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Vindicator.VindicatorBreakDoorGoal(this)); + this.goalSelector.addGoal(2, new AbstractIllager.RaiderOpenDoorGoal(this)); + this.goalSelector.addGoal(3, new Raider.HoldGroundAttackGoal(this, 10.0F)); + this.goalSelector.addGoal(4, new Vindicator.VindicatorMeleeAttackGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, Raider.class)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true)); +@@ -130,6 +164,12 @@ public class Vindicator extends AbstractIllager { + RandomSource randomSource = world.getRandom(); + this.populateDefaultEquipmentSlots(randomSource, difficulty); + this.populateDefaultEquipmentEnchantments(randomSource, difficulty); ++ // Purpur start ++ Level level = world.getMinecraftWorld(); ++ if (level.purpurConfig.vindicatorJohnnySpawnChance > 0D && random.nextDouble() <= level.purpurConfig.vindicatorJohnnySpawnChance) { ++ setCustomName(Component.translatable("Johnny")); ++ } ++ // Purpur end + return spawnGroupData; + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Witch.java b/src/main/java/net/minecraft/world/entity/monster/Witch.java +index b7bc64818387288955d0723cd071d4203bd2f121..d308c2bed67977bd6fd2a4509f9a13ae2af9025f 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Witch.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Witch.java +@@ -56,6 +56,38 @@ public class Witch extends Raider implements RangedAttackMob { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.witchRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.witchRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.witchControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.witchMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.witchTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.witchAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); +@@ -64,10 +96,12 @@ public class Witch extends Raider implements RangedAttackMob { + }); + this.attackPlayersGoal = new NearestAttackableWitchTargetGoal<>(this, Player.class, 10, true, false, (Predicate) null); + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 60, 10.0F)); + this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(3, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[]{Raider.class})); + this.targetSelector.addGoal(2, this.healRaidersGoal); + this.targetSelector.addGoal(3, this.attackPlayersGoal); +diff --git a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java +index 6449213d717271bcc516e393a78dfe1e5c762d68..56f1c52afe32ce71edd44c7bc3ff1ac1f09457a2 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java +@@ -35,6 +35,38 @@ public class WitherSkeleton extends AbstractSkeleton { + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.witherSkeletonRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.witherSkeletonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.witherSkeletonControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.witherSkeletonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.witherSkeletonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.witherSkeletonAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractPiglin.class, true)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java +index 5956a7759964f5e4939f062e93714fba64f53141..70d891d85748039b517a87b2438b04a9010d8af4 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java +@@ -67,6 +67,38 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zoglinRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.zoglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zoglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zoglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zoglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zoglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -182,7 +214,7 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + + @Override + public Brain getBrain() { +- return super.getBrain(); ++ return (Brain) super.getBrain(); // Purpur - decompile error + } + + protected void updateActivity() { +@@ -198,9 +230,10 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("zoglinBrain"); ++ //this.level.getProfiler().push("zoglinBrain"); // Purpur ++ if (getRider() == null || !this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.updateActivity(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +index 9976205537cfe228735687f1e9c52c74ac025690..ef8cca70661cedecf08a787011342c402eb59a79 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +@@ -95,22 +95,69 @@ public class Zombie extends Monster { + private int inWaterTime; + public int conversionTime; + private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field +- private boolean shouldBurnInDay = true; // Paper ++ // private boolean shouldBurnInDay = true; // Paper // Purpur - implemented in LivingEntity + + public Zombie(EntityType type, Level world) { + super(type, world); + this.breakDoorGoal = new BreakDoorGoal(this, com.google.common.base.Predicates.in(world.paperConfig().entities.behavior.doorBreakingDifficulty.getOrDefault(type, world.paperConfig().entities.behavior.doorBreakingDifficulty.get(EntityType.ZOMBIE)))); // Paper ++ this.setShouldBurnInDay(true); // Purpur + } + + public Zombie(Level world) { + this(EntityType.ZOMBIE, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zombieRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.zombieRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zombieControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zombieMaxHealth); ++ } ++ ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.zombieJockeyOnlyBaby; ++ } ++ ++ public double jockeyChance() { ++ return level.purpurConfig.zombieJockeyChance; ++ } ++ ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.zombieJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zombieTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zombieAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + if (level.paperConfig().entities.behavior.zombiesTargetTurtleEggs) this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0D, 3)); // Paper + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.addBehaviourGoals(); + } + +@@ -120,7 +167,19 @@ public class Zombie extends Monster { + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers(ZombifiedPiglin.class)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); +- if ( level.spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Spigot ++ // Purpur start ++ if ( level.spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal(this, AbstractVillager.class, false) { // Spigot ++ @Override ++ public boolean canUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canUse(); ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canContinueToUse(); ++ } ++ }); ++ // Purpur end + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); + this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -242,30 +301,7 @@ public class Zombie extends Monster { + + @Override + public void aiStep() { +- if (this.isAlive()) { +- boolean flag = this.isSunSensitive() && this.isSunBurnTick(); +- +- if (flag) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); +- +- if (!itemstack.isEmpty()) { +- if (itemstack.isDamageableItem()) { +- itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); +- if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { +- this.broadcastBreakEvent(EquipmentSlot.HEAD); +- this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); +- } +- } +- +- flag = false; +- } +- +- if (flag) { +- this.setSecondsOnFire(8); +- } +- } +- } +- ++ // Purpur - implemented in LivingEntity + super.aiStep(); + } + +@@ -303,6 +339,7 @@ public class Zombie extends Monster { + + } + ++ public boolean shouldBurnInDay() { return isSunSensitive(); } // Purpur - for ABI compatibility + public boolean isSunSensitive() { + return this.shouldBurnInDay; // Paper - use api value instead + } +@@ -432,7 +469,7 @@ public class Zombie extends Monster { + nbt.putBoolean("CanBreakDoors", this.canBreakDoors()); + nbt.putInt("InWaterTime", this.isInWater() ? this.inWaterTime : -1); + nbt.putInt("DrownedConversionTime", this.isUnderWaterConverting() ? this.conversionTime : -1); +- nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper ++ // nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper // Purpur - implemented in LivingEntity + } + + @Override +@@ -446,7 +483,7 @@ public class Zombie extends Monster { + } + // Paper start + if (nbt.contains("Paper.ShouldBurnInDay")) { +- this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); ++ // this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); // Purpur - implemented in LivingEntity + } + // Paper end + +@@ -527,19 +564,20 @@ public class Zombie extends Monster { + if (object instanceof Zombie.ZombieGroupData) { + Zombie.ZombieGroupData entityzombie_groupdatazombie = (Zombie.ZombieGroupData) object; + +- if (entityzombie_groupdatazombie.isBaby) { +- this.setBaby(true); ++ // Purpur start ++ if (!jockeyOnlyBaby() || entityzombie_groupdatazombie.isBaby) { ++ this.setBaby(entityzombie_groupdatazombie.isBaby); + if (entityzombie_groupdatazombie.canSpawnJockey) { +- if ((double) randomsource.nextFloat() < 0.05D) { +- List list = world.getEntitiesOfClass(Chicken.class, this.getBoundingBox().inflate(5.0D, 3.0D, 5.0D), EntitySelector.ENTITY_NOT_BEING_RIDDEN); ++ if ((double) randomsource.nextFloat() < jockeyChance()) { ++ List list = jockeyTryExistingChickens() ? world.getEntitiesOfClass(Chicken.class, this.getBoundingBox().inflate(5.0D, 3.0D, 5.0D), EntitySelector.ENTITY_NOT_BEING_RIDDEN) : java.util.Collections.emptyList(); ++ // Purpur end + + if (!list.isEmpty()) { + Chicken entitychicken = (Chicken) list.get(0); + + entitychicken.setChickenJockey(true); + this.startRiding(entitychicken); +- } +- } else if ((double) randomsource.nextFloat() < 0.05D) { ++ } else { // Purpur + Chicken entitychicken1 = (Chicken) EntityType.CHICKEN.create(this.level); + + if (entitychicken1 != null) { +@@ -549,6 +587,7 @@ public class Zombie extends Monster { + this.startRiding(entitychicken1); + world.addFreshEntity(entitychicken1, CreatureSpawnEvent.SpawnReason.MOUNT); // CraftBukkit + } ++ } // Purpur + } + } + } +@@ -559,11 +598,7 @@ public class Zombie extends Monster { + } + + if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) { +- LocalDate localdate = LocalDate.now(); +- int i = localdate.get(ChronoField.DAY_OF_MONTH); +- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- +- if (j == 10 && i == 31 && randomsource.nextFloat() < 0.25F) { ++ if (net.minecraft.world.entity.ambient.Bat.isHalloweenSeason(world.getMinecraftWorld()) && this.random.nextFloat() < this.level.purpurConfig.chanceHeadHalloweenOnEntity) { // Purpur + this.setItemSlot(EquipmentSlot.HEAD, new ItemStack(randomsource.nextFloat() < 0.1F ? Blocks.JACK_O_LANTERN : Blocks.CARVED_PUMPKIN)); + this.armorDropChances[EquipmentSlot.HEAD.getIndex()] = 0.0F; + } +@@ -595,7 +630,7 @@ public class Zombie extends Monster { + } + + protected void randomizeReinforcementsChance() { +- this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * 0.10000000149011612D); ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.zombieSpawnReinforcements); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +index 71a36cf9b976443cca9ab63cd0eb23253f638562..0fdfcb3a26698f26caf163828f2cf89e2a28054a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +@@ -79,6 +79,58 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + }); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zombieVillagerRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.zombieVillagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zombieVillagerControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zombieVillagerMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.zombieVillagerSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.zombieVillagerJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.zombieVillagerJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.zombieVillagerJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zombieVillagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zombieVillagerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -165,13 +217,13 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + ItemStack itemstack = player.getItemInHand(hand); + + if (itemstack.is(Items.GOLDEN_APPLE)) { +- if (this.hasEffect(MobEffects.WEAKNESS)) { ++ if (this.hasEffect(MobEffects.WEAKNESS) && level.purpurConfig.zombieVillagerCureEnabled) { // Purpur + if (!player.getAbilities().instabuild) { + itemstack.shrink(1); + } + + if (!this.level.isClientSide) { +- this.startConverting(player.getUUID(), this.random.nextInt(2401) + 3600); ++ this.startConverting(player.getUUID(), this.random.nextInt(level.purpurConfig.zombieVillagerCuringTimeMax - level.purpurConfig.zombieVillagerCuringTimeMin + 1) + level.purpurConfig.zombieVillagerCuringTimeMin); // Purpur + } + + return InteractionResult.SUCCESS; +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +index b75945807b425609394c343da56c316a769f0a29..ab33a30995d741898cd034fe0fad99eff3529707 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +@@ -63,6 +63,53 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zombifiedPiglinRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.zombifiedPiglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zombifiedPiglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zombifiedPiglinMaxHealth); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.zombifiedPiglinJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.zombifiedPiglinJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.zombifiedPiglinJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zombifiedPiglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zombifiedPiglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public void setPersistentAngerTarget(@Nullable UUID angryAt) { + this.persistentAngerTarget = angryAt; +@@ -115,7 +162,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.maybeAlertOthers(); + } + +- if (this.isAngry()) { ++ if (this.isAngry() && this.level.purpurConfig.zombifiedPiglinCountAsPlayerKillWhenAngry) { // Purpur + this.lastHurtByPlayerTime = this.tickCount; + } + +@@ -170,7 +217,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.ticksUntilNextAlert = ZombifiedPiglin.ALERT_INTERVAL.sample(this.random); + } + +- if (entityliving instanceof Player) { ++ if (entityliving instanceof Player && this.level.purpurConfig.zombifiedPiglinCountAsPlayerKillWhenAngry) { // Purpur + this.setLastHurtByPlayer((Player) entityliving); + } + +@@ -250,7 +297,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + + @Override + protected void randomizeReinforcementsChance() { +- this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(0.0D); ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.zombifiedPiglinSpawnReinforcements); // Purpur + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +index daa2224b021c966751eb39f269ffbfe6e7f3d426..aba0633361d4d933a3c18049595f3deb1d39da1a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +@@ -67,6 +67,43 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.hoglinRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.hoglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.hoglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.hoglinMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.hoglinBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.hoglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.hoglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean canBeLeashed(Player player) { + return !this.isLeashed(); +@@ -129,10 +166,10 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("hoglinBrain"); +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ //this.level.getProfiler().push("hoglinBrain"); // Purpur ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + HoglinAi.updateActivity(this); + if (this.isConverting()) { + ++this.timeInOverworld; +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +index b401fb4f276ca81b4bb18426ee56abed8a9f7a7b..d606a351210486fc8656c0bfd224347150af7faf 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +@@ -97,6 +97,38 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.piglinRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.piglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.piglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.piglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.piglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.piglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +@@ -311,10 +343,10 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("piglinBrain"); +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ //this.level.getProfiler().push("piglinBrain"); // Purpur ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + PiglinAi.updateActivity(this); + super.customServerAiStep(); + } +@@ -410,7 +442,7 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + + @Override + public boolean wantsToPickUp(ItemStack stack) { +- return this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && this.canPickUpLoot() && PiglinAi.wantsToPickup(this, stack); ++ return (this.level.purpurConfig.piglinBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && this.canPickUpLoot() && PiglinAi.wantsToPickup(this, stack); + } + + protected boolean canReplaceCurrentItem(ItemStack stack) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java +index ac75c54e897565e340b66823caeed92ba1d1641a..bc6572b1025d74a7590d7e1cc49132f95af0560a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java +@@ -41,6 +41,38 @@ public class PiglinBrute extends AbstractPiglin { + this.xpReward = 20; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.piglinBruteRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.piglinBruteRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.piglinBruteControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.piglinBruteMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.piglinBruteTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.piglinBruteAlwaysDropExp; ++ } ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 50.0D).add(Attributes.MOVEMENT_SPEED, (double)0.35F).add(Attributes.ATTACK_DAMAGE, 7.0D); + } +@@ -70,7 +102,7 @@ public class PiglinBrute extends AbstractPiglin { + + @Override + public Brain getBrain() { +- return super.getBrain(); ++ return (Brain) super.getBrain(); // Purpur - decompile error + } + + @Override +@@ -85,9 +117,10 @@ public class PiglinBrute extends AbstractPiglin { + + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("piglinBruteBrain"); ++ //this.level.getProfiler().push("piglinBruteBrain"); // Purpur ++ if (getRider() == null || this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + PiglinBruteAi.updateActivity(this); + PiglinBruteAi.maybePlayActivitySound(this); + super.customServerAiStep(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +index 904826ea563bd2eb469f403df459def62cc1b5e6..17df2b09542f67cdd1d83f795d9b2aad9ccd4e05 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java ++++ b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +@@ -120,8 +120,32 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F); ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this, 0.5F); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.wardenRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.wardenRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.wardenControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + public Packet getAddEntityPacket() { + return new ClientboundAddEntityPacket(this, this.hasPose(Pose.EMERGING) ? 1 : 0); +@@ -275,10 +299,10 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + protected void customServerAiStep() { + ServerLevel worldserver = (ServerLevel) this.level; + +- worldserver.getProfiler().push("wardenBrain"); ++ //worldserver.getProfiler().push("wardenBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick(worldserver, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + if ((this.tickCount + this.getId()) % 120 == 0) { + Warden.applyDarknessAround(worldserver, this.position(), this, 20); +@@ -405,19 +429,16 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + + @Contract("null->false") + public boolean canTargetEntity(@Nullable Entity entity) { +- boolean flag; +- ++ if (getRider() != null && isControllable()) return false; // Purpur + if (entity instanceof LivingEntity) { + LivingEntity entityliving = (LivingEntity) entity; + + if (this.level == entity.level && EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(entity) && !this.isAlliedTo(entity) && entityliving.getType() != EntityType.ARMOR_STAND && entityliving.getType() != EntityType.WARDEN && !entityliving.isInvulnerable() && !entityliving.isDeadOrDying() && this.level.getWorldBorder().isWithinBounds(entityliving.getBoundingBox())) { +- flag = true; +- return flag; ++ return true; // Purpur - wtf + } + } + +- flag = false; +- return flag; ++ return false; // Purpur - wtf + } + + public static void applyDarknessAround(ServerLevel world, Vec3 pos, @Nullable Entity entity, int range) { +diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +index ca96b893e22de3ae7c11d5cded51edf70bdcb6f2..d4ab27de59e9c533789f062e74ceb453483e2e39 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -43,6 +43,7 @@ import org.bukkit.event.entity.VillagerAcquireTradeEvent; + // CraftBukkit end + + public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant { ++ static final net.minecraft.world.item.crafting.Ingredient TEMPT_ITEMS = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.level.block.Blocks.EMERALD_BLOCK.asItem()); // Purpur + + // CraftBukkit start + private CraftMerchant craftMerchant; +diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +index 5f407535298a31a34cfe114dd863fd6a9b977707..29c7e33fe961020e5a0007287fe9b6631689f1b8 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +@@ -30,7 +30,7 @@ public class CatSpawner implements CustomSpawner { + if (this.nextTick > 0) { + return 0; + } else { +- this.nextTick = 1200; ++ this.nextTick = world.purpurConfig.catSpawnDelay; // Purpur + Player player = world.getRandomPlayer(); + if (player == null) { + return 0; +@@ -63,11 +63,15 @@ public class CatSpawner implements CustomSpawner { + } + + private int spawnInVillage(ServerLevel world, BlockPos pos) { +- int i = 48; ++ // Purpur start ++ int range = world.purpurConfig.catSpawnVillageScanRange; ++ if (range <= 0) return 0; ++ + if (world.getPoiManager().getCountInRange((entry) -> { + return entry.is(PoiTypes.HOME); +- }, pos, 48, PoiManager.Occupancy.IS_OCCUPIED) > 4L) { +- List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(48.0D, 8.0D, 48.0D)); ++ }, pos, range, PoiManager.Occupancy.IS_OCCUPIED) > 4L) { ++ List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(range, 8.0D, range)); ++ // Purpur end + if (list.size() < 5) { + return this.spawnCat(pos, world); + } +@@ -77,8 +81,11 @@ public class CatSpawner implements CustomSpawner { + } + + private int spawnInHut(ServerLevel world, BlockPos pos) { +- int i = 16; +- List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(16.0D, 8.0D, 16.0D)); ++ // Purpur start ++ int range = world.purpurConfig.catSpawnSwampHutScanRange; ++ if (range <= 0) return 0; ++ List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(range, 8.0D, range)); ++ // Purpur end + return list.size() < 1 ? this.spawnCat(pos, world) : 0; + } + +diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java +index 76a9da8209d557b913c49ccd281bf147b9ac4fa4..aed1d9ccffe471b6c2a1d52d2d3d097f6431318b 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -139,6 +139,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + }, MemoryModuleType.MEETING_POINT, (entityvillager, holder) -> { + return holder.is(PoiTypes.MEETING); + }); ++ private boolean isLobotomized = false; public boolean isLobotomized() { return this.isLobotomized; } // Purpur ++ private int notLobotomizedCount = 0; // Purpur + + public long nextGolemPanic = -1; // Pufferfish + +@@ -155,6 +157,90 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + this.setVillagerData(this.getVillagerData().setType(type).setProfession(VillagerProfession.NONE)); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.villagerRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.villagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.villagerControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ if (level.purpurConfig.villagerFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.villagerMaxHealth); ++ } ++ ++ @Override ++ public boolean canBeLeashed(Player player) { ++ return level.purpurConfig.villagerCanBeLeashed && !this.isLeashed(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.villagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.villagerAlwaysDropExp; ++ } ++ ++ private boolean checkLobotomized() { ++ int interval = this.level.purpurConfig.villagerLobotomizeCheckInterval; ++ if (this.notLobotomizedCount > 3) { ++ // check half as often if not lobotomized for the last 3+ consecutive checks ++ interval *= 2; ++ } ++ if (this.level.getGameTime() % interval == 0) { ++ // offset Y for short blocks like dirt_path/farmland ++ this.isLobotomized = !canTravelFrom(new BlockPos(getX(), getY() + 0.0625D, getZ())); ++ ++ if (this.isLobotomized) { ++ this.notLobotomizedCount = 0; ++ } else { ++ this.notLobotomizedCount++; ++ } ++ } ++ return this.isLobotomized; ++ } ++ ++ private boolean canTravelFrom(BlockPos pos) { ++ return canTravelTo(pos.east()) || canTravelTo(pos.west()) || canTravelTo(pos.north()) || canTravelTo(pos.south()); ++ } ++ ++ private boolean canTravelTo(BlockPos pos) { ++ net.minecraft.world.level.block.state.BlockState state = this.level.getBlockStateIfLoaded(pos); ++ if (state == null) { ++ // chunk not loaded ++ return false; ++ } ++ net.minecraft.world.level.block.Block bottom = state.getBlock(); ++ if (bottom instanceof net.minecraft.world.level.block.FenceBlock || ++ bottom instanceof net.minecraft.world.level.block.FenceGateBlock || ++ bottom instanceof net.minecraft.world.level.block.WallBlock) { ++ // bottom block is too tall to get over ++ return false; ++ } ++ net.minecraft.world.level.block.Block top = level.getBlockState(pos.above()).getBlock(); ++ // only if both blocks have no collision ++ return !bottom.hasCollision && !top.hasCollision; ++ } ++ // Purpur end ++ + @Override + public Brain getBrain() { + return (Brain) super.getBrain(); // CraftBukkit - decompile error +@@ -189,7 +275,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + brain.addActivity(Activity.PLAY, VillagerGoalPackages.getPlayPackage(0.5F)); + } else { + brain.setSchedule(Schedule.VILLAGER_DEFAULT); +- brain.addActivityWithConditions(Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerprofession, 0.5F), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT))); ++ brain.addActivityWithConditions(Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerprofession, 0.5F, this.level.purpurConfig.villagerClericsFarmWarts), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT))); // Purpur + } + + brain.addActivity(Activity.CORE, VillagerGoalPackages.getCorePackage(villagerprofession, 0.5F)); +@@ -249,14 +335,29 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + @Override + protected void customServerAiStep() { mobTick(false); } + protected void mobTick(boolean inactive) { +- this.level.getProfiler().push("villagerBrain"); ++ //this.level.getProfiler().push("villagerBrain"); // Purpur ++ // Purpur start ++ if (this.level.purpurConfig.villagerLobotomizeEnabled) { ++ // treat as inactive if lobotomized ++ inactive = inactive || checkLobotomized(); ++ } else { ++ // clean up state for API ++ this.isLobotomized = false; ++ } ++ // Purpur end + // Pufferfish start + if (!inactive) { +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level, this); // Paper + } + // Pufferfish end +- this.level.getProfiler().pop(); ++ // Purpur start ++ else if (this.isLobotomized && shouldRestock()) { ++ // make sure we restock if needed when lobotomized ++ restock(); ++ } ++ // Purpur end ++ //this.level.getProfiler().pop(); // Purpur + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; + } +@@ -312,7 +413,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + if (!itemstack.is(Items.VILLAGER_SPAWN_EGG) && this.isAlive() && !this.isTrading() && !this.isSleeping()) { + if (this.isBaby()) { + this.setUnhappy(); +- return InteractionResult.sidedSuccess(this.level.isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level.isClientSide)); // Purpur + } else { + boolean flag = this.getOffers().isEmpty(); + +@@ -325,9 +426,10 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + if (flag) { +- return InteractionResult.sidedSuccess(this.level.isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level.isClientSide)); // Purpur + } else { +- if (!this.level.isClientSide && !this.offers.isEmpty()) { ++ if (level.purpurConfig.villagerRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur ++ if (this.level.purpurConfig.villagerAllowTrading && !this.offers.isEmpty()) { // Purpur + this.startTrading(player); + } + +@@ -484,7 +586,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + while (iterator.hasNext()) { + MerchantOffer merchantrecipe = (MerchantOffer) iterator.next(); + +- merchantrecipe.updateDemand(); ++ merchantrecipe.updateDemand(this.level.purpurConfig.villagerMinimumDemand); // Purpur + } + + } +@@ -734,7 +836,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + @Override + public boolean canBreed() { +- return this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; ++ return this.level.purpurConfig.villagerCanBreed && this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; // Purpur + } + + private boolean hungry() { +@@ -926,6 +1028,11 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + public boolean hasFarmSeeds() { ++ // Purpur start ++ if (this.level.purpurConfig.villagerClericsFarmWarts && this.getVillagerData().getProfession() == VillagerProfession.CLERIC) { ++ return this.getInventory().hasAnyOf(ImmutableSet.of(Items.NETHER_WART)); ++ } ++ // Purpur end + return this.getInventory().hasAnyOf(ImmutableSet.of(Items.WHEAT_SEEDS, Items.POTATO, Items.CARROT, Items.BEETROOT_SEEDS)); + } + +@@ -974,6 +1081,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + public void spawnGolemIfNeeded(ServerLevel world, long time, int requiredCount) { ++ if (world.purpurConfig.villagerSpawnIronGolemRadius > 0 && world.getEntitiesOfClass(net.minecraft.world.entity.animal.IronGolem.class, getBoundingBox().inflate(world.purpurConfig.villagerSpawnIronGolemRadius)).size() > world.purpurConfig.villagerSpawnIronGolemLimit) return; // Purpur + if (this.wantsToSpawnGolem(time)) { + AABB axisalignedbb = this.getBoundingBox().inflate(10.0D, 10.0D, 10.0D); + List list = world.getEntitiesOfClass(Villager.class, axisalignedbb); +@@ -1047,6 +1155,12 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + @Override + public void startSleeping(BlockPos pos) { ++ // Purpur start ++ if (level.purpurConfig.bedExplodeOnVillagerSleep && this.level.getBlockState(pos).getBlock() instanceof net.minecraft.world.level.block.BedBlock) { ++ this.level.explode(null, DamageSource.explosion(this, null), null, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (float) this.level.purpurConfig.bedExplosionPower, this.level.purpurConfig.bedExplosionFire, this.level.purpurConfig.bedExplosionEffect); ++ return; ++ } ++ // Purpur end + super.startSleeping(pos); + this.brain.setMemory(MemoryModuleType.LAST_SLEPT, this.level.getGameTime()); // CraftBukkit - decompile error + this.brain.eraseMemory(MemoryModuleType.WALK_TARGET); +diff --git a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +index ac70c2c03241e73943bd517a8c69dd05e0873634..0318663a824d2a9515f867a075d148c3fcb1a907 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java ++++ b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +@@ -26,7 +26,7 @@ public record VillagerProfession(String name, Predicate> heldJob + public static final VillagerProfession ARMORER = register("armorer", PoiTypes.ARMORER, SoundEvents.VILLAGER_WORK_ARMORER); + public static final VillagerProfession BUTCHER = register("butcher", PoiTypes.BUTCHER, SoundEvents.VILLAGER_WORK_BUTCHER); + public static final VillagerProfession CARTOGRAPHER = register("cartographer", PoiTypes.CARTOGRAPHER, SoundEvents.VILLAGER_WORK_CARTOGRAPHER); +- public static final VillagerProfession CLERIC = register("cleric", PoiTypes.CLERIC, SoundEvents.VILLAGER_WORK_CLERIC); ++ public static final VillagerProfession CLERIC = register("cleric", PoiTypes.CLERIC, ImmutableSet.of(Items.NETHER_WART), ImmutableSet.of(Blocks.SOUL_SAND), SoundEvents.VILLAGER_WORK_CLERIC); // Purpur + public static final VillagerProfession FARMER = register("farmer", PoiTypes.FARMER, ImmutableSet.of(Items.WHEAT, Items.WHEAT_SEEDS, Items.BEETROOT_SEEDS, Items.BONE_MEAL), ImmutableSet.of(Blocks.FARMLAND), SoundEvents.VILLAGER_WORK_FARMER); + public static final VillagerProfession FISHERMAN = register("fisherman", PoiTypes.FISHERMAN, SoundEvents.VILLAGER_WORK_FISHERMAN); + public static final VillagerProfession FLETCHER = register("fletcher", PoiTypes.FLETCHER, SoundEvents.VILLAGER_WORK_FLETCHER); +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +index e92e6fb4cf97f4d5406b5b5d5786bfa5fb55f536..a2ad029160065baa395cfe20fa40881d8252fcb3 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +@@ -66,6 +66,43 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + this.setDespawnDelay(48000); // CraftBukkit - set default from MobSpawnerTrader + } + ++ // Purpur - start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.wanderingTraderRidable; ++ } ++ ++ @Override ++ public boolean rideableUnderWater() { ++ return level.purpurConfig.wanderingTraderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.wanderingTraderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.wanderingTraderMaxHealth); ++ } ++ ++ @Override ++ public boolean canBeLeashed(Player player) { ++ return level.purpurConfig.wanderingTraderCanBeLeashed && !this.isLeashed(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.wanderingTraderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.wanderingTraderAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); +@@ -73,7 +110,7 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + return this.canDrinkPotion && this.level.isNight() && !entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API + })); + this.goalSelector.addGoal(0, new UseItemGoal<>(this, new ItemStack(Items.MILK_BUCKET), SoundEvents.WANDERING_TRADER_REAPPEARED, (entityvillagertrader) -> { +- return canDrinkMilk && this.level.isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API ++ return level.purpurConfig.milkClearsBeneficialEffects && canDrinkMilk && this.level.isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API // Purpur + })); + this.goalSelector.addGoal(1, new TradeWithPlayerGoal(this)); + this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this, Zombie.class, 8.0F, 0.5D, 0.5D)); +@@ -86,6 +123,7 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + this.goalSelector.addGoal(1, new PanicGoal(this, 0.5D)); + this.goalSelector.addGoal(1, new LookAtTradingPlayerGoal(this)); + this.goalSelector.addGoal(2, new WanderingTrader.WanderToPositionGoal(this, 2.0D, 0.35D)); ++ if (level.purpurConfig.wanderingTraderFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); // Purpur + this.goalSelector.addGoal(4, new MoveTowardsRestrictionGoal(this, 0.35D)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 0.35D)); + this.goalSelector.addGoal(9, new InteractGoal(this, Player.class, 3.0F, 1.0F)); +@@ -113,9 +151,10 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + } + + if (this.getOffers().isEmpty()) { +- return InteractionResult.sidedSuccess(this.level.isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level.isClientSide)); // Purpur + } else { +- if (!this.level.isClientSide) { ++ if (level.purpurConfig.wanderingTraderRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur ++ if (this.level.purpurConfig.wanderingTraderAllowTrading) { // Purpur + this.setTradingPlayer(player); + this.openTradingScreen(player, this.getDisplayName(), 1); + } +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +index 0ae8e9134a3671cdf2a480cd4dd6598653e261ab..7d34bd0d727eeb0afbc1869b076ee2078a67e4d0 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +@@ -159,7 +159,17 @@ public class WanderingTraderSpawner implements CustomSpawner { + int k = pos.getX() + this.random.nextInt(range * 2) - range; + int l = pos.getZ() + this.random.nextInt(range * 2) - range; + int i1 = world.getHeight(Heightmap.Types.WORLD_SURFACE, k, l); +- BlockPos blockposition2 = new BlockPos(k, i1, l); ++ // Purpur start - allow traders to spawn below nether roof ++ BlockPos.MutableBlockPos blockposition2 = new BlockPos.MutableBlockPos(k, i1, l); ++ if (world.dimensionType().hasCeiling()) { ++ do { ++ blockposition2.relative(net.minecraft.core.Direction.DOWN); ++ } while (!world.getBlockState(blockposition2).isAir()); ++ do { ++ blockposition2.relative(net.minecraft.core.Direction.DOWN); ++ } while (world.getBlockState(blockposition2).isAir() && blockposition2.getY() > 0); ++ } ++ // Purpur end + + if (NaturalSpawner.isSpawnPositionOk(SpawnPlacements.Type.ON_GROUND, world, blockposition2, EntityType.WANDERING_TRADER)) { + blockposition1 = blockposition2; +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 1116116e4ba6c5ecec400cd371b70b9e14efd92b..0c632cd0cb932a4cfaf2288c9d109325d5287537 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -183,6 +183,8 @@ public abstract class Player extends LivingEntity { + public boolean affectsSpawning = true; + public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; + // Paper end ++ public int sixRowEnderchestSlotCount = -1; // Purpur ++ public boolean canPortalInstant = false; // Purpur + + // CraftBukkit start + public boolean fauxSleeping; +@@ -194,6 +196,28 @@ public abstract class Player extends LivingEntity { + } + // CraftBukkit end + ++ // Purpur start ++ public int burpDelay = 0; ++ ++ public abstract void resetLastActionTime(); ++ ++ public void setAfk(boolean afk) { ++ } ++ ++ public boolean isAfk() { ++ return false; ++ } ++ ++ @Override ++ public boolean processClick(InteractionHand hand) { ++ Entity vehicle = getRootVehicle(); ++ if (vehicle != null && vehicle.getRider() == this) { ++ return vehicle.onClick(hand); ++ } ++ return false; ++ } ++ // Purpur end ++ + public Player(Level world, BlockPos pos, float yaw, GameProfile gameProfile) { + super(EntityType.PLAYER, world); + this.lastItemInMainHand = ItemStack.EMPTY; +@@ -238,6 +262,12 @@ public abstract class Player extends LivingEntity { + + @Override + public void tick() { ++ // Purpur start ++ if (this.burpDelay > 0 && --this.burpDelay == 0) { ++ this.level.playSound(null, getX(), getY(), getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 1.0F, this.level.random.nextFloat() * 0.1F + 0.9F); ++ } ++ // Purpur end ++ + this.noPhysics = this.isSpectator(); + if (this.isSpectator()) { + this.onGround = false; +@@ -341,6 +371,16 @@ public abstract class Player extends LivingEntity { + this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.TURTLE_HELMET); // CraftBukkit + } + ++ // Purpur start ++ if (this.level.purpurConfig.playerNetheriteFireResistanceDuration > 0 && this.level.getGameTime() % 20 == 0) { ++ if (itemstack.is(Items.NETHERITE_HELMET) ++ && this.getItemBySlot(EquipmentSlot.CHEST).is(Items.NETHERITE_CHESTPLATE) ++ && this.getItemBySlot(EquipmentSlot.LEGS).is(Items.NETHERITE_LEGGINGS) ++ && this.getItemBySlot(EquipmentSlot.FEET).is(Items.NETHERITE_BOOTS)) { ++ this.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, this.level.purpurConfig.playerNetheriteFireResistanceDuration, this.level.purpurConfig.playerNetheriteFireResistanceAmplifier, this.level.purpurConfig.playerNetheriteFireResistanceAmbient, this.level.purpurConfig.playerNetheriteFireResistanceShowParticles, this.level.purpurConfig.playerNetheriteFireResistanceShowIcon), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.NETHERITE_ARMOR); ++ } ++ } ++ // Purpur end + } + + protected ItemCooldowns createItemCooldowns() { +@@ -427,7 +467,7 @@ public abstract class Player extends LivingEntity { + + @Override + public int getPortalWaitTime() { +- return this.abilities.invulnerable ? 1 : 80; ++ return this.abilities.invulnerable || canPortalInstant ? 1 : 80; // Purpur + } + + @Override +@@ -586,7 +626,7 @@ public abstract class Player extends LivingEntity { + for (int i = 0; i < list.size(); ++i) { + Entity entity = (Entity) list.get(i); + +- if (entity.getType() == EntityType.EXPERIENCE_ORB) { ++ if (entity.getType() == EntityType.EXPERIENCE_ORB && entity.level.purpurConfig.playerExpPickupDelay >= 0) { // Purpur + list1.add(entity); + } else if (!entity.isRemoved()) { + this.touch(entity); +@@ -1276,7 +1316,7 @@ public abstract class Player extends LivingEntity { + flag2 = flag2 && !level.paperConfig().entities.behavior.disablePlayerCrits; // Paper + flag2 = flag2 && !this.isSprinting(); + if (flag2) { +- f *= 1.5F; ++ f *= this.level.purpurConfig.playerCriticalDamageMultiplier; // Purpur + } + + f += f1; +@@ -1949,9 +1989,18 @@ public abstract class Player extends LivingEntity { + @Override + public int getExperienceReward() { + if (!this.level.getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) && !this.isSpectator()) { +- int i = this.experienceLevel * 7; +- +- return i > 100 ? 100 : i; ++ // Purpur start ++ int toDrop; ++ try { ++ scriptEngine.eval("expLevel = " + experienceLevel); ++ scriptEngine.eval("expTotal = " + totalExperience); ++ scriptEngine.eval("exp = " + experienceProgress); ++ toDrop = (int) Math.round((Double) scriptEngine.eval(level.purpurConfig.playerDeathExpDropEquation)); ++ } catch (Exception ignore) { ++ toDrop = experienceLevel * 7; ++ } ++ return Math.min(toDrop, level.purpurConfig.playerDeathExpDropMax); ++ // Purpur end + } else { + return 0; + } +@@ -2027,6 +2076,11 @@ public abstract class Player extends LivingEntity { + return this.inventory.armor; + } + ++ @Override ++ public boolean rideableUnderWater() { ++ return this.level.purpurConfig.playerRidableInWater; ++ } ++ + public boolean setEntityOnShoulder(CompoundTag entityNbt) { + if (!this.isPassenger() && this.onGround && !this.isInWater() && !this.isInPowderSnow) { + if (this.getShoulderEntityLeft().isEmpty()) { +@@ -2305,7 +2359,7 @@ public abstract class Player extends LivingEntity { + public ItemStack eat(Level world, ItemStack stack) { + this.getFoodData().eat(stack.getItem(), stack); + this.awardStat(Stats.ITEM_USED.get(stack.getItem())); +- world.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); ++ // world.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); // Purpur - moved to tick() + if (this instanceof ServerPlayer) { + CriteriaTriggers.CONSUME_ITEM.trigger((ServerPlayer) this, stack); + } +diff --git a/src/main/java/net/minecraft/world/entity/player/StackedContents.java b/src/main/java/net/minecraft/world/entity/player/StackedContents.java +index 574ebb3a2fcd0e4e426a8a7ee88d722ed3b9c3f5..842b921799111789b37a34b76644c9217bc85794 100644 +--- a/src/main/java/net/minecraft/world/entity/player/StackedContents.java ++++ b/src/main/java/net/minecraft/world/entity/player/StackedContents.java +@@ -37,8 +37,62 @@ public class StackedContents { + int i = getStackingIndex(stack); + int j = Math.min(maxCount, stack.getCount()); + this.put(i, j); ++ // PaperPR start ++ if (stack.hasTag()) { ++ this.put(getExactStackingIndex(stack), j); ++ } ++ } ++ ++ } ++ private static final net.minecraft.core.IdMap EXACT_MATCHES_ID_MAP = new net.minecraft.core.IdMap<>() { ++ private final java.util.concurrent.atomic.AtomicInteger idCounter = new java.util.concurrent.atomic.AtomicInteger(BuiltInRegistries.ITEM.size()); ++ private final it.unimi.dsi.fastutil.objects.Object2IntMap itemstackToId = new it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap<>(new it.unimi.dsi.fastutil.Hash.Strategy<>() { ++ @Override ++ public int hashCode(ItemStack o) { ++ return java.util.Objects.hash(o.getItem(), o.getTag()); ++ } ++ ++ @Override ++ public boolean equals(@Nullable ItemStack a, @Nullable ItemStack b) { ++ if (a == null || b == null) { ++ return false; ++ } ++ return ItemStack.tagMatches(a, b); ++ } ++ }); ++ private final it.unimi.dsi.fastutil.ints.Int2ObjectMap idToItemstack = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); ++ ++ @Override ++ public int getId(ItemStack value) { ++ if (!this.itemstackToId.containsKey(value)) { ++ final int id = this.idCounter.incrementAndGet(); ++ final ItemStack copy = value.copy(); ++ this.itemstackToId.put(copy, id); ++ this.idToItemstack.put(id, copy); ++ return id; ++ } ++ return this.itemstackToId.getInt(value); ++ } ++ ++ @Override ++ public @Nullable ItemStack byId(int index) { ++ return this.idToItemstack.get(index); ++ } ++ ++ @Override ++ public int size() { ++ return this.itemstackToId.size(); ++ } ++ ++ @Override ++ public java.util.Iterator iterator() { ++ return this.idToItemstack.values().iterator(); + } ++ }; + ++ public static int getExactStackingIndex(ItemStack stack) { ++ return EXACT_MATCHES_ID_MAP.getId(stack); ++ // PaperPR end + } + + public static int getStackingIndex(ItemStack stack) { +@@ -80,6 +134,12 @@ public class StackedContents { + } + + public static ItemStack fromStackingIndex(int itemId) { ++ // PaperPR start ++ if (itemId > BuiltInRegistries.ITEM.size()) { ++ final ItemStack stack = EXACT_MATCHES_ID_MAP.byId(itemId); ++ return stack == null ? ItemStack.EMPTY : stack.copy(); ++ } ++ // PaperPR end + return itemId == 0 ? ItemStack.EMPTY : new ItemStack(Item.byId(itemId)); + } + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +index c56bc341ebb1592af9285d5e044951e7ae2ae0b2..ed6a097aa1319e492ff56974fd259c227995523e 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +@@ -72,6 +72,7 @@ public abstract class AbstractArrow extends Projectile { + private IntOpenHashSet piercingIgnoreEntityIds; + @Nullable + private List piercedAndKilledEntities; ++ public int lootingLevel; // Purpur + + // Spigot Start + @Override +@@ -312,7 +313,7 @@ public abstract class AbstractArrow extends Projectile { + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(vec3d.multiply((double) (this.random.nextFloat() * 0.2F), (double) (this.random.nextFloat() * 0.2F), (double) (this.random.nextFloat() * 0.2F))); +- this.life = 0; ++ if (this.level.purpurConfig.arrowMovementResetsDespawnCounter) this.life = 0; // Purpur - do not reset despawn counter + } + + @Override +@@ -612,6 +613,12 @@ public abstract class AbstractArrow extends Projectile { + this.knockback = punch; + } + ++ // Purpur start ++ public void setLootingLevel(int looting) { ++ this.lootingLevel = looting; ++ } ++ // Purpur end ++ + public int getKnockback() { + return this.knockback; + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java b/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java +index 3a4b95288d390e72c0d97671ecc2e2ef2f976de1..4a3ba1f44379290b1e89366fa82c4028d888a260 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java +@@ -17,20 +17,20 @@ public class LargeFireball extends Fireball { + + public LargeFireball(EntityType type, Level world) { + super(type, world); +- isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit ++ isIncendiary = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit // Purpur + } + + public LargeFireball(Level world, LivingEntity owner, double velocityX, double velocityY, double velocityZ, int explosionPower) { + super(EntityType.FIREBALL, owner, velocityX, velocityY, velocityZ, world); + this.explosionPower = explosionPower; +- isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit ++ isIncendiary = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit // Purpur + } + + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); + if (!this.level.isClientSide) { +- boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ boolean flag = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Purpur + + // CraftBukkit start - fire ExplosionPrimeEvent + ExplosionPrimeEvent event = new ExplosionPrimeEvent((org.bukkit.entity.Explosive) this.getBukkitEntity()); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +index 4132c1113f5437a776e5e3c1cb306904775aed88..1a945a32c3d3705a318ebca72a365931a8c001b7 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +@@ -27,6 +27,12 @@ public class LlamaSpit extends Projectile { + this.setPos(owner.getX() - (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(owner.yBodyRot * 0.017453292F), owner.getEyeY() - 0.10000000149011612D, owner.getZ() + (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(owner.yBodyRot * 0.017453292F)); + } + ++ // Purpur start ++ public void super_tick() { ++ super.tick(); ++ } ++ // Purpur end ++ + @Override + public void tick() { + super.tick(); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index a000834c4ea8645a2fcd697e6396f797c42c8fa3..7e620e7ea23943a8639a437d7937da2784b59f60 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -303,6 +303,6 @@ public abstract class Projectile extends Entity { + public boolean mayInteract(Level world, BlockPos pos) { + Entity entity = this.getOwner(); + +- return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.purpurConfig.projectilesBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +index 00ac1cdc4734cc57f15433c5c6e7a3a545739d33..f1601114647b62b0b10064ed43058cb867740a63 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +@@ -24,7 +24,7 @@ public class SmallFireball extends Fireball { + super(EntityType.SMALL_FIREBALL, owner, velocityX, velocityY, velocityZ, world); + // CraftBukkit start + if (this.getOwner() != null && this.getOwner() instanceof Mob) { +- isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ isIncendiary = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Purpur + } + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +index a725851060f13e734dbd2fbf8c83c9e1af57a8b7..c7c10c89871a3ee6d21da4bb19407a68759b3ade 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +@@ -54,10 +54,40 @@ public class Snowball extends ThrowableItemProjectile { + protected void onHitEntity(EntityHitResult entityHitResult) { + super.onHitEntity(entityHitResult); + Entity entity = entityHitResult.getEntity(); +- int i = entity instanceof Blaze ? 3 : 0; ++ int i = entity.level.purpurConfig.snowballDamage >= 0 ? entity.level.purpurConfig.snowballDamage : entity instanceof Blaze ? 3 : 0; // Purpur + entity.hurt(DamageSource.thrown(this, this.getOwner()), (float)i); + } + ++ // Purpur start - borrowed and modified code from ThrownPotion#onHitBlock and ThrownPotion#dowseFire ++ @Override ++ protected void onHitBlock(net.minecraft.world.phys.BlockHitResult blockHitResult) { ++ super.onHitBlock(blockHitResult); ++ ++ if (!this.level.isClientSide) { ++ net.minecraft.core.BlockPos blockposition = blockHitResult.getBlockPos(); ++ net.minecraft.core.BlockPos blockposition1 = blockposition.relative(blockHitResult.getDirection()); ++ ++ net.minecraft.world.level.block.state.BlockState iblockdata = this.level.getBlockState(blockposition); ++ ++ if (this.level.purpurConfig.snowballExtinguishesFire && this.level.getBlockState(blockposition1).is(net.minecraft.world.level.block.Blocks.FIRE)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition1, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState()).isCancelled()) { ++ this.level.removeBlock(blockposition1, false); ++ } ++ } else if (this.level.purpurConfig.snowballExtinguishesCandles && net.minecraft.world.level.block.AbstractCandleBlock.isLit(iblockdata)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.setValue(net.minecraft.world.level.block.AbstractCandleBlock.LIT, false)).isCancelled()) { ++ net.minecraft.world.level.block.AbstractCandleBlock.extinguish(null, iblockdata, this.level, blockposition); ++ } ++ } else if (this.level.purpurConfig.snowballExtinguishesCampfires && net.minecraft.world.level.block.CampfireBlock.isLitCampfire(iblockdata)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.setValue(net.minecraft.world.level.block.CampfireBlock.LIT, false)).isCancelled()) { ++ this.level.levelEvent(null, 1009, blockposition, 0); ++ net.minecraft.world.level.block.CampfireBlock.dowse(this.getOwner(), this.level, blockposition, iblockdata); ++ this.level.setBlockAndUpdate(blockposition, iblockdata.setValue(net.minecraft.world.level.block.CampfireBlock.LIT, false)); ++ } ++ } ++ } ++ } ++ // Purpur end ++ + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index f224ebbc0efefddede43d87f0300c014077b9931..26991a8259e99c73ac79368642b1812264216c47 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -69,10 +69,11 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + Bukkit.getPluginManager().callEvent(teleEvent); + + if (!teleEvent.isCancelled() && !entityplayer.connection.isDisconnected()) { +- if (this.random.nextFloat() < 0.05F && this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { ++ if (this.random.nextFloat() < this.level.purpurConfig.enderPearlEndermiteChance && this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { // Purpur + Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(this.level); + + if (entityendermite != null) { ++ entityendermite.setPlayerSpawned(true); // Purpur + entityendermite.moveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot()); + this.level.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); + } +@@ -85,7 +86,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + entityplayer.connection.teleport(teleEvent.getTo()); + entity.resetFallDistance(); + CraftEventFactory.entityDamage = this; +- entity.hurt(DamageSource.FALL, 5.0F); ++ entity.hurt(DamageSource.FALL, this.level.purpurConfig.enderPearlDamage); // Purpur + CraftEventFactory.entityDamage = null; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java +index fb6e590e4613f59aaa8278932134aa0ca3d9da8f..8e41faaa9610a31415c7c89c266c22f26f7e0bc6 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java +@@ -60,7 +60,7 @@ public class ThrownTrident extends AbstractArrow { + Entity entity = this.getOwner(); + byte b0 = (Byte) this.entityData.get(ThrownTrident.ID_LOYALTY); + +- if (b0 > 0 && (this.dealtDamage || this.isNoPhysics()) && entity != null) { ++ if (b0 > 0 && (this.dealtDamage || this.isNoPhysics() || (level.purpurConfig.tridentLoyaltyVoidReturnHeight < 0.0D && getY() < level.purpurConfig.tridentLoyaltyVoidReturnHeight)) && entity != null) { // Purpur + if (!this.isAcceptibleReturnOwner()) { + if (!this.level.isClientSide && this.pickup == AbstractArrow.Pickup.ALLOWED) { + this.spawnAtLocation(this.getPickupItem(), 0.1F); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +index 9665095240a370983878350aed41badacfb6f261..623b90b263257dd633af330a63e4bb9d4d507493 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +@@ -94,7 +94,7 @@ public class WitherSkull extends AbstractHurtingProjectile { + if (!this.level.isClientSide) { + // CraftBukkit start + // this.level.explode(this, this.getX(), this.getY(), this.getZ(), 1.0F, false, World.a.MOB); +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 1.0F, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), this.level.purpurConfig.witherExplosionRadius, false); // Purpur + this.level.getCraftServer().getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java +index e5ccbaf72f29731f1d1aa939b9297b644a408cd4..6c77c67dc6a378b87cd73c6c55b6d41d1542f6f3 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raider.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java +@@ -319,7 +319,7 @@ public abstract class Raider extends PatrollingMonster { + + @Override + public boolean canUse() { +- if (!this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items ++ if ((!this.mob.level.purpurConfig.pillagerBypassMobGriefing && !this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items // Purpur + Raid raid = this.mob.getCurrentRaid(); + + if (this.mob.hasActiveRaid() && !this.mob.getCurrentRaid().isOver() && this.mob.canBeLeader() && !ItemStack.matches(this.mob.getItemBySlot(EquipmentSlot.HEAD), Raid.getLeaderBannerInstance())) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java +index feb89eb69994bdd1d2f95d2b9992e69251b2bee7..0670775c2de33e69c75644e5d2ff19db08444040 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raids.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java +@@ -28,6 +28,7 @@ import net.minecraft.world.phys.Vec3; + public class Raids extends SavedData { + + private static final String RAID_FILE_ID = "raids"; ++ public final Map playerCooldowns = Maps.newHashMap(); + public final Map raidMap = Maps.newHashMap(); + private final ServerLevel level; + private int nextAvailableID; +@@ -45,6 +46,17 @@ public class Raids extends SavedData { + + public void tick() { + ++this.tick; ++ // Purpur start ++ if (level.purpurConfig.raidCooldownSeconds != 0 && this.tick % 20 == 0) { ++ com.google.common.collect.ImmutableMap.copyOf(playerCooldowns).forEach((uuid, i) -> { ++ if (i < 1) { ++ playerCooldowns.remove(uuid); ++ } else { ++ playerCooldowns.put(uuid, i - 1); ++ } ++ }); ++ } ++ // Purpur end + Iterator iterator = this.raidMap.values().iterator(); + + while (iterator.hasNext()) { +@@ -129,11 +141,13 @@ public class Raids extends SavedData { + } + + if (flag) { ++ if (level.purpurConfig.raidCooldownSeconds != 0 && playerCooldowns.containsKey(player.getUUID())) return null; // Purpur + // CraftBukkit start + if (!org.bukkit.craftbukkit.event.CraftEventFactory.callRaidTriggerEvent(raid, player)) { + player.removeEffect(MobEffects.BAD_OMEN); + return null; + } ++ if (level.purpurConfig.raidCooldownSeconds != 0) playerCooldowns.put(player.getUUID(), level.purpurConfig.raidCooldownSeconds); // Purpur + + if (!this.raidMap.containsKey(raid.getId())) { + this.raidMap.put(raid.getId(), raid); +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +index eec7d7a5b558830111831792c42665724613af23..36da85fe7ba6c6931e96bf4cf77cbe5f079299af 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +@@ -106,11 +106,13 @@ public abstract class AbstractMinecart extends Entity { + private double flyingY = 0.949999988079071D; // Paper - restore vanilla precision + private double flyingZ = 0.949999988079071D; // Paper - restore vanilla precision + public double maxSpeed = 0.4D; ++ public double storedMaxSpeed; // Purpur + // CraftBukkit end + + protected AbstractMinecart(EntityType type, Level world) { + super(type, world); + this.blocksBuilding = true; ++ if (world != null) maxSpeed = storedMaxSpeed = world.purpurConfig.minecartMaxSpeed; // Purpur + } + + protected AbstractMinecart(EntityType type, Level world, double x, double y, double z) { +@@ -333,6 +335,12 @@ public abstract class AbstractMinecart extends Entity { + + @Override + public void tick() { ++ // Purpur start ++ if (storedMaxSpeed != level.purpurConfig.minecartMaxSpeed) { ++ maxSpeed = storedMaxSpeed = level.purpurConfig.minecartMaxSpeed; ++ } ++ // Purpur end ++ + // CraftBukkit start + double prevX = this.getX(); + double prevY = this.getY(); +@@ -496,16 +504,63 @@ public abstract class AbstractMinecart extends Entity { + + public void activateMinecart(int x, int y, int z, boolean powered) {} + ++ // Purpur start ++ private Double lastSpeed; ++ ++ public double getControllableSpeed() { ++ BlockPos pos = new BlockPos(this); ++ Block block = level.getBlockState(pos).getBlock(); ++ if (!block.material.isSolid()) { ++ block = level.getBlockState(pos.relative(Direction.DOWN)).getBlock(); ++ } ++ Double speed = level.purpurConfig.minecartControllableBlockSpeeds.get(block); ++ if (!block.material.isSolid()) { ++ speed = lastSpeed; ++ } ++ if (speed == null) { ++ speed = level.purpurConfig.minecartControllableBaseSpeed; ++ } ++ return lastSpeed = speed; ++ } ++ // Purpur end ++ + protected void comeOffTrack() { + double d0 = this.getMaxSpeed(); + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(Mth.clamp(vec3d.x, -d0, d0), vec3d.y, Mth.clamp(vec3d.z, -d0, d0)); ++ ++ // Purpur start ++ if (level.purpurConfig.minecartControllable && !isInWater() && !isInLava() && !passengers.isEmpty()) { ++ Entity passenger = passengers.get(0); ++ if (passenger instanceof Player) { ++ Player player = (Player) passenger; ++ if (player.jumping && this.onGround) { ++ setDeltaMovement(new Vec3(getDeltaMovement().x, level.purpurConfig.minecartControllableHopBoost, getDeltaMovement().z)); ++ } ++ if (player.zza != 0.0F) { ++ Vector velocity = player.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(getControllableSpeed()); ++ if (player.zza < 0.0) { ++ velocity.multiply(-0.5); ++ } ++ setDeltaMovement(new Vec3(velocity.getX(), getDeltaMovement().y, velocity.getZ())); ++ } ++ this.setYRot(passenger.getYRot() - 90); ++ maxUpStep = level.purpurConfig.minecartControllableStepHeight; ++ } else { ++ maxUpStep = 0.0F; ++ } ++ } else { ++ maxUpStep = 0.0F; ++ } ++ // Purpur end ++ + if (this.onGround) { + // CraftBukkit start - replace magic numbers with our variables + this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.derailedX, this.getDeltaMovement().y * this.derailedY, this.getDeltaMovement().z * this.derailedZ)); + // CraftBukkit end + } ++ else if (level.purpurConfig.minecartControllable) setDeltaMovement(new Vec3(getDeltaMovement().x * derailedX, getDeltaMovement().y, getDeltaMovement().z * derailedZ)); // Purpur + + this.move(MoverType.SELF, this.getDeltaMovement()); + if (!this.onGround) { +@@ -667,7 +722,7 @@ public abstract class AbstractMinecart extends Entity { + if (d18 > 0.01D) { + double d20 = 0.06D; + +- this.setDeltaMovement(vec3d4.add(vec3d4.x / d18 * 0.06D, 0.0D, vec3d4.z / d18 * 0.06D)); ++ this.setDeltaMovement(vec3d4.add(vec3d4.x / d18 * this.level.purpurConfig.poweredRailBoostModifier, 0.0D, vec3d4.z / d18 * this.level.purpurConfig.poweredRailBoostModifier)); // Purpur + } else { + Vec3 vec3d5 = this.getDeltaMovement(); + double d21 = vec3d5.x; +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +index 85e1892866cd2ee0cec1552b8541c1f800bdf68c..231c71939982ba4ce9305bc8eb6174ed45102c9f 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +@@ -222,7 +222,13 @@ public class Boat extends Entity implements VariantHolder { + } + + protected void destroy(DamageSource source) { +- this.spawnAtLocation((ItemLike) this.getDropItem()); ++ // Purpur start ++ final ItemStack boat = new ItemStack(this.getDropItem()); ++ if (this.level.purpurConfig.persistentDroppableEntityDisplayNames && this.hasCustomName()) { ++ boat.setHoverName(this.getCustomName()); ++ } ++ this.spawnAtLocation(boat); ++ // Purpur end + } + + @Override +@@ -537,6 +543,7 @@ public class Boat extends Entity implements VariantHolder { + + if (f > 0.0F) { + this.landFriction = f; ++ if (level.purpurConfig.boatEjectPlayersOnLand) ejectPassengers(); // Purpur + return Boat.Status.ON_LAND; + } else { + return Boat.Status.IN_AIR; +diff --git a/src/main/java/net/minecraft/world/food/FoodData.java b/src/main/java/net/minecraft/world/food/FoodData.java +index 2934b6de1f1fb914a532ee20184df99d1acd8e65..0e753dd68d9506a2a4e5ad74e7f4d04cd4d00494 100644 +--- a/src/main/java/net/minecraft/world/food/FoodData.java ++++ b/src/main/java/net/minecraft/world/food/FoodData.java +@@ -34,8 +34,10 @@ public class FoodData { + // CraftBukkit end + + public void eat(int food, float saturationModifier) { ++ int oldValue = this.foodLevel; // Purpur + this.foodLevel = Math.min(food + this.foodLevel, 20); + this.saturationLevel = Math.min(this.saturationLevel + (float) food * saturationModifier * 2.0F, (float) this.foodLevel); ++ if (this.entityhuman.level.purpurConfig.playerBurpWhenFull && this.foodLevel == 20 && oldValue < 20) this.entityhuman.burpDelay = this.entityhuman.level.purpurConfig.playerBurpDelay; // Purpur + } + + public void eat(Item item, ItemStack stack) { +@@ -101,7 +103,7 @@ public class FoodData { + ++this.tickTimer; + if (this.tickTimer >= this.starvationRate) { // CraftBukkit - add regen rate manipulation + if (player.getHealth() > 10.0F || enumdifficulty == Difficulty.HARD || player.getHealth() > 1.0F && enumdifficulty == Difficulty.NORMAL) { +- player.hurt(DamageSource.STARVE, 1.0F); ++ player.hurt(DamageSource.STARVE, player.level.purpurConfig.hungerStarvationDamage); // Purpur + } + + this.tickTimer = 0; +diff --git a/src/main/java/net/minecraft/world/food/FoodProperties.java b/src/main/java/net/minecraft/world/food/FoodProperties.java +index 9967ba762567631f2bdb1e4f8fe16a13ea927b46..6c945ae8fe8b1517e312c688f829fab41f12d9f4 100644 +--- a/src/main/java/net/minecraft/world/food/FoodProperties.java ++++ b/src/main/java/net/minecraft/world/food/FoodProperties.java +@@ -2,15 +2,22 @@ package net.minecraft.world.food; + + import com.google.common.collect.Lists; + import com.mojang.datafixers.util.Pair; ++ ++import java.util.ArrayList; + import java.util.List; + import net.minecraft.world.effect.MobEffectInstance; + + public class FoodProperties { +- private final int nutrition; +- private final float saturationModifier; +- private final boolean isMeat; +- private final boolean canAlwaysEat; +- private final boolean fastFood; ++ // Purpur start ++ private int nutrition; public void setNutrition(int nutrition) { this.nutrition = nutrition; } ++ private float saturationModifier; public void setSaturationModifier(float saturation) { this.saturationModifier = saturation; } ++ private boolean isMeat; public void setIsMeat(boolean isMeat) { this.isMeat = isMeat; } ++ private boolean canAlwaysEat; public void setCanAlwaysEat(boolean canAlwaysEat) { this.canAlwaysEat = canAlwaysEat; } ++ private boolean fastFood; public void setFastFood(boolean isFastFood) { this.fastFood = isFastFood; } ++ public FoodProperties copy() { ++ return new FoodProperties(this.nutrition, this.saturationModifier, this.isMeat, this.canAlwaysEat, this.fastFood, new ArrayList<>(this.effects)); ++ } ++ // Purpur end + private final List> effects; + + FoodProperties(int hunger, float saturationModifier, boolean meat, boolean alwaysEdible, boolean snack, List> statusEffects) { +diff --git a/src/main/java/net/minecraft/world/food/Foods.java b/src/main/java/net/minecraft/world/food/Foods.java +index b16d9e2eaa589f19c563ee70b1a56d67dbcdecb0..71beab673f04cd051c46ea37f8c847316885d38d 100644 +--- a/src/main/java/net/minecraft/world/food/Foods.java ++++ b/src/main/java/net/minecraft/world/food/Foods.java +@@ -4,6 +4,9 @@ import net.minecraft.world.effect.MobEffectInstance; + import net.minecraft.world.effect.MobEffects; + + public class Foods { ++ public static final java.util.Map ALL_PROPERTIES = new java.util.HashMap<>(); // Purpur ++ public static final java.util.Map DEFAULT_PROPERTIES = new java.util.HashMap<>(); // Purpur ++ + public static final FoodProperties APPLE = (new FoodProperties.Builder()).nutrition(4).saturationMod(0.3F).build(); + public static final FoodProperties BAKED_POTATO = (new FoodProperties.Builder()).nutrition(5).saturationMod(0.6F).build(); + public static final FoodProperties BEEF = (new FoodProperties.Builder()).nutrition(3).saturationMod(0.3F).meat().build(); +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +index 143977055717c2fe27df76231da304e2863b8f1f..cbec8dbc06dfc05150c345246bfd63c8001071d0 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -76,6 +76,7 @@ public abstract class AbstractContainerMenu { + @Nullable + private ContainerSynchronizer synchronizer; + private boolean suppressRemoteUpdates; ++ @javax.annotation.Nullable protected ItemStack activeQuickItem = null; // Purpur + + // CraftBukkit start + public boolean checkReachable = true; +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java +index fa0d55946680f1a913493d8a36abe266ace8be52..d662d362d36356a44dd672c25997837a8a66096e 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java +@@ -145,7 +145,13 @@ public abstract class AbstractFurnaceMenu extends RecipeBookMenu { + } else if (slot != 1 && slot != 0) { + if (this.canSmelt(itemstack1)) { + if (!this.moveItemStackTo(itemstack1, 0, 1, false)) { +- return ItemStack.EMPTY; ++ // Purpur start - fix #625 ++ if (this.isFuel(itemstack1)) { ++ if (!this.moveItemStackTo(itemstack1, 1, 2, false)) { ++ return ItemStack.EMPTY; ++ } ++ } ++ // Purpur end + } + } else if (this.isFuel(itemstack1)) { + if (!this.moveItemStackTo(itemstack1, 1, 2, false)) { +diff --git a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +index 506d758efbf16da9467f120321d2359a8832e477..05a8a295fe7970255c07efad8b4ab7e9a358bf83 100644 +--- a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +@@ -21,6 +21,13 @@ import org.slf4j.Logger; + import org.bukkit.craftbukkit.inventory.CraftInventoryView; + // CraftBukkit end + ++// Purpur start ++import net.minecraft.nbt.IntTag; ++import net.minecraft.network.protocol.game.ClientboundContainerSetDataPacket; ++import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; ++import net.minecraft.server.level.ServerPlayer; ++// Purpur end ++ + public class AnvilMenu extends ItemCombinerMenu { + + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -41,6 +48,8 @@ public class AnvilMenu extends ItemCombinerMenu { + public int maximumRepairCost = 40; + private CraftInventoryView bukkitEntity; + // CraftBukkit end ++ public boolean bypassCost = false; // Purpur ++ public boolean canDoUnsafeEnchants = false; // Purpur + + public AnvilMenu(int syncId, Inventory inventory) { + this(syncId, inventory, ContainerLevelAccess.NULL); +@@ -59,12 +68,15 @@ public class AnvilMenu extends ItemCombinerMenu { + + @Override + protected boolean mayPickup(Player player, boolean present) { +- return (player.getAbilities().instabuild || player.experienceLevel >= this.cost.get()) && this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST && present; // CraftBukkit - allow cost 0 like a free item ++ return (player.getAbilities().instabuild || player.experienceLevel >= this.cost.get()) && (bypassCost || this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST) && present; // CraftBukkit - allow cost 0 like a free item // Purpur + } + + @Override + protected void onTake(Player player, ItemStack stack) { ++ ItemStack itemstack = activeQuickItem == null ? stack : activeQuickItem; // Purpur ++ if (org.purpurmc.purpur.event.inventory.AnvilTakeResultEvent.getHandlerList().getRegisteredListeners().length > 0) new org.purpurmc.purpur.event.inventory.AnvilTakeResultEvent(player.getBukkitEntity(), getBukkitView(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)).callEvent(); // Purpur + if (!player.getAbilities().instabuild) { ++ if (bypassCost) ((ServerPlayer) player).lastSentExp = -1; else // Purpur + player.giveExperienceLevels(-this.cost.get()); + } + +@@ -115,6 +127,12 @@ public class AnvilMenu extends ItemCombinerMenu { + + @Override + public void createResult() { ++ // Purpur start ++ bypassCost = false; ++ canDoUnsafeEnchants = false; ++ if (org.purpurmc.purpur.event.inventory.AnvilUpdateResultEvent.getHandlerList().getRegisteredListeners().length > 0) new org.purpurmc.purpur.event.inventory.AnvilUpdateResultEvent(getBukkitView()).callEvent(); ++ // Purpur end ++ + ItemStack itemstack = this.inputSlots.getItem(0); + + this.cost.set(1); +@@ -191,7 +209,8 @@ public class AnvilMenu extends ItemCombinerMenu { + int i2 = (Integer) map1.get(enchantment); + + i2 = l1 == i2 ? i2 + 1 : Math.max(i2, l1); +- boolean flag3 = enchantment.canEnchant(itemstack); ++ boolean flag3 = canDoUnsafeEnchants || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants && org.purpurmc.purpur.PurpurConfig.allowInapplicableEnchants) || enchantment.canEnchant(itemstack); // Purpur ++ boolean flag4 = true; // Purpur + + if (this.player.getAbilities().instabuild || itemstack.is(Items.ENCHANTED_BOOK)) { + flag3 = true; +@@ -203,16 +222,16 @@ public class AnvilMenu extends ItemCombinerMenu { + Enchantment enchantment1 = (Enchantment) iterator1.next(); + + if (enchantment1 != enchantment && !enchantment.isCompatibleWith(enchantment1)) { +- flag3 = false; ++ flag4 = canDoUnsafeEnchants || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants && org.purpurmc.purpur.PurpurConfig.allowIncompatibleEnchants); // Purpur flag3 -> flag4 + ++i; + } + } + +- if (!flag3) { ++ if (!flag3 || !flag4) { // Purpur + flag2 = true; + } else { + flag1 = true; +- if (i2 > enchantment.getMaxLevel()) { ++ if ((!org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants || !org.purpurmc.purpur.PurpurConfig.allowHigherEnchantsLevels) && i2 > enchantment.getMaxLevel()) { // Purpur + i2 = enchantment.getMaxLevel(); + } + +@@ -262,6 +281,54 @@ public class AnvilMenu extends ItemCombinerMenu { + } else if (!this.itemName.equals(itemstack.getHoverName().getString())) { + b1 = 1; + i += b1; ++ // Purpur start ++ if (this.player != null) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity player = this.player.getBukkitEntity(); ++ String name = this.itemName; ++ boolean removeItalics = false; ++ if (player.hasPermission("purpur.anvil.remove_italics")) { ++ if (name.startsWith("&r")) { ++ name = name.substring(2); ++ removeItalics = true; ++ } else if (name.startsWith("")) { ++ name = name.substring(3); ++ removeItalics = true; ++ } else if (name.startsWith("")) { ++ name = name.substring(7); ++ removeItalics = true; ++ } ++ } ++ if (this.player.level.purpurConfig.anvilAllowColors) { ++ if (player.hasPermission("purpur.anvil.color")) { ++ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(?i)&([0-9a-fr])").matcher(name); ++ while (matcher.find()) { ++ String match = matcher.group(1); ++ name = name.replace("&" + match, "\u00a7" + match.toLowerCase(java.util.Locale.ROOT)); ++ } ++ //name = name.replaceAll("(?i)&([0-9a-fr])", "\u00a7$1"); ++ } ++ if (player.hasPermission("purpur.anvil.format")) { ++ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(?i)&([k-or])").matcher(name); ++ while (matcher.find()) { ++ String match = matcher.group(1); ++ name = name.replace("&" + match, "\u00a7" + match.toLowerCase(java.util.Locale.ROOT)); ++ } ++ //name = name.replaceAll("(?i)&([l-or])", "\u00a7$1"); ++ } ++ } ++ net.kyori.adventure.text.Component component; ++ if (this.player.level.purpurConfig.anvilColorsUseMiniMessage && player.hasPermission("purpur.anvil.minimessage")) { ++ component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.bukkit.ChatColor.stripColor(name)); ++ } else { ++ component = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(name); ++ } ++ if (removeItalics) { ++ component = component.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ itemstack1.setHoverName(io.papermc.paper.adventure.PaperAdventure.asVanilla(component)); ++ } ++ else ++ // Purpur end + itemstack1.setHoverName(Component.literal(this.itemName)); + } + +@@ -274,6 +341,13 @@ public class AnvilMenu extends ItemCombinerMenu { + this.cost.set(this.maximumRepairCost - 1); // CraftBukkit + } + ++ // Purpur start ++ if (bypassCost && cost.get() >= maximumRepairCost) { ++ itemstack.addTagElement("Purpur.realCost", IntTag.valueOf(cost.get())); ++ cost.set(maximumRepairCost - 1); ++ } ++ // Purpur end ++ + if (this.cost.get() >= this.maximumRepairCost && !this.player.getAbilities().instabuild) { // CraftBukkit + itemstack1 = ItemStack.EMPTY; + } +@@ -296,11 +370,17 @@ public class AnvilMenu extends ItemCombinerMenu { + org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), itemstack1); // CraftBukkit + sendAllDataToRemote(); // CraftBukkit - SPIGOT-6686: Always send completed inventory to stay in sync with client + this.broadcastChanges(); ++ // Purpur start ++ if ((canDoUnsafeEnchants || org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants) && itemstack1 != ItemStack.EMPTY) { ++ ((ServerPlayer) player).connection.send(new ClientboundContainerSetSlotPacket(containerId, incrementStateId(), 2, itemstack1)); ++ ((ServerPlayer) player).connection.send(new ClientboundContainerSetDataPacket(containerId, 0, cost.get())); ++ } ++ // Purpur end + } + } + + public static int calculateIncreasedRepairCost(int cost) { +- return cost * 2 + 1; ++ return org.purpurmc.purpur.PurpurConfig.anvilCumulativeCost ? cost * 2 + 1 : 0; + } + + public void setItemName(String newItemName) { +diff --git a/src/main/java/net/minecraft/world/inventory/ChestMenu.java b/src/main/java/net/minecraft/world/inventory/ChestMenu.java +index 82331715e91c6e9a13c0626164368ae16e754126..15d115917e2c9f7d5669cabe2721df9fcdb4ec7b 100644 +--- a/src/main/java/net/minecraft/world/inventory/ChestMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/ChestMenu.java +@@ -67,10 +67,30 @@ public class ChestMenu extends AbstractContainerMenu { + return new ChestMenu(MenuType.GENERIC_9x6, syncId, playerInventory, 6); + } + ++ // Purpur start ++ public static ChestMenu oneRow(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x1, syncId, playerInventory, inventory, 1); ++ } ++ ++ public static ChestMenu twoRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x2, syncId, playerInventory, inventory, 2); ++ } ++ // Purpur end ++ + public static ChestMenu threeRows(int syncId, Inventory playerInventory, Container inventory) { + return new ChestMenu(MenuType.GENERIC_9x3, syncId, playerInventory, inventory, 3); + } + ++ // Purpur start ++ public static ChestMenu fourRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x4, syncId, playerInventory, inventory, 4); ++ } ++ ++ public static ChestMenu fiveRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x5, syncId, playerInventory, inventory, 5); ++ } ++ // Purpur end ++ + public static ChestMenu sixRows(int syncId, Inventory playerInventory, Container inventory) { + return new ChestMenu(MenuType.GENERIC_9x6, syncId, playerInventory, inventory, 6); + } +diff --git a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +index 0d464958c63d4bb460bb65cfa5c227b21101a069..02f5dd214cdb59685cb4129bf143904d4cb83011 100644 +--- a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +@@ -38,6 +38,12 @@ import org.bukkit.event.enchantment.PrepareItemEnchantEvent; + import org.bukkit.entity.Player; + // CraftBukkit end + ++// Purpur start ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.EnchantmentTableBlockEntity; ++import org.bukkit.craftbukkit.entity.CraftHumanEntity; ++// Purpur end ++ + public class EnchantmentMenu extends AbstractContainerMenu { + + private final Container enchantSlots; +@@ -71,6 +77,22 @@ public class EnchantmentMenu extends AbstractContainerMenu { + return context.getLocation(); + } + // CraftBukkit end ++ ++ // Purpur start ++ @Override ++ public void onClose(CraftHumanEntity who) { ++ super.onClose(who); ++ ++ if (who.getHandle().getLevel().purpurConfig.enchantmentTableLapisPersists) { ++ access.execute((level, pos) -> { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof EnchantmentTableBlockEntity enchantmentTable) { ++ enchantmentTable.setLapis(this.getItem(1).getCount()); ++ } ++ }); ++ } ++ } ++ // Purpur end + }; + this.random = RandomSource.create(); + this.enchantmentSeed = DataSlot.standalone(); +@@ -96,6 +118,17 @@ public class EnchantmentMenu extends AbstractContainerMenu { + } + }); + ++ // Purpur start ++ access.execute((level, pos) -> { ++ if (level.purpurConfig.enchantmentTableLapisPersists) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof EnchantmentTableBlockEntity enchantmentTable) { ++ this.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI, enchantmentTable.getLapis())); ++ } ++ } ++ }); ++ // Purpur end ++ + int j; + + for (j = 0; j < 3; ++j) { +@@ -338,6 +371,7 @@ public class EnchantmentMenu extends AbstractContainerMenu { + public void removed(net.minecraft.world.entity.player.Player player) { + super.removed(player); + this.access.execute((world, blockposition) -> { ++ if (world.purpurConfig.enchantmentTableLapisPersists) this.getSlot(1).set(ItemStack.EMPTY); // Purpur + this.clearContainer(player, this.enchantSlots); + }); + } +diff --git a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java +index 233e8626280a8b93dcb8621a1405e8c308c6836b..e74114fed79810e13319a70006e28aa56fd93f18 100644 +--- a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java +@@ -95,9 +95,11 @@ public class GrindstoneMenu extends AbstractContainerMenu { + + @Override + public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) { ++ ItemStack itemstack = activeQuickItem == null ? stack : activeQuickItem; // Purpur + context.execute((world, blockposition) -> { ++ org.purpurmc.purpur.event.inventory.GrindstoneTakeResultEvent grindstoneTakeResultEvent = new org.purpurmc.purpur.event.inventory.GrindstoneTakeResultEvent(player.getBukkitEntity(), getBukkitView(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), this.getExperienceAmount(world)); grindstoneTakeResultEvent.callEvent(); // Purpur + if (world instanceof ServerLevel) { +- ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), this.getExperienceAmount(world), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player); // Paper ++ ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), grindstoneTakeResultEvent.getExperienceAmount(), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player); // Paper // Purpur + } + + world.levelEvent(1042, blockposition, 0); +@@ -130,7 +132,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + Enchantment enchantment = (Enchantment) entry.getKey(); + Integer integer = (Integer) entry.getValue(); + +- if (!enchantment.isCurse()) { ++ if (!org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(enchantment)) { // Purpur + j += enchantment.getMinCost(integer); + } + } +@@ -230,7 +232,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + Entry entry = (Entry) iterator.next(); + Enchantment enchantment = (Enchantment) entry.getKey(); + +- if (!enchantment.isCurse() || EnchantmentHelper.getItemEnchantmentLevel(enchantment, itemstack2) == 0) { ++ if (!org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(enchantment) || EnchantmentHelper.getItemEnchantmentLevel(enchantment, itemstack2) == 0) { // Purpur + itemstack2.enchant(enchantment, (Integer) entry.getValue()); + } + } +@@ -251,7 +253,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + + itemstack1.setCount(amount); + Map map = (Map) EnchantmentHelper.getEnchantments(item).entrySet().stream().filter((entry) -> { +- return ((Enchantment) entry.getKey()).isCurse(); ++ return org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains((Enchantment) entry.getKey()); // Purpur + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + EnchantmentHelper.setEnchantments(map, itemstack1); +@@ -267,6 +269,20 @@ public class GrindstoneMenu extends AbstractContainerMenu { + itemstack1.setRepairCost(AnvilMenu.calculateIncreasedRepairCost(itemstack1.getBaseRepairCost())); + } + ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.grindstoneRemoveAttributes && itemstack1.getTag() != null) { ++ for (String key : itemstack1.getTag().getAllKeys()) { ++ if (!key.equals("display")) { ++ itemstack1.getTag().remove(key); ++ } ++ } ++ } ++ ++ if (org.purpurmc.purpur.PurpurConfig.grindstoneRemoveDisplay && itemstack1.getTag() != null) { ++ itemstack1.getTag().remove("display"); ++ } ++ // Purpur end ++ + return itemstack1; + } + +@@ -328,7 +344,9 @@ public class GrindstoneMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ this.activeQuickItem = itemstack; // Purpur + slot1.onTake(player, itemstack1); ++ this.activeQuickItem = null; // Purpur + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/InventoryMenu.java b/src/main/java/net/minecraft/world/inventory/InventoryMenu.java +index 150701f965006f1c7dc9d801ca0ab0add927d143..4b8669f0b881e524c0cbf570c442ca8a1044d68e 100644 +--- a/src/main/java/net/minecraft/world/inventory/InventoryMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/InventoryMenu.java +@@ -4,6 +4,7 @@ import com.mojang.datafixers.util.Pair; + import net.minecraft.network.chat.Component; + import net.minecraft.resources.ResourceLocation; + import net.minecraft.world.Container; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.EquipmentSlot; + import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.player.Inventory; +@@ -96,7 +97,7 @@ public class InventoryMenu extends RecipeBookMenu { + public boolean mayPickup(Player playerEntity) { + ItemStack itemstack = this.getItem(); + +- return !itemstack.isEmpty() && !playerEntity.isCreative() && EnchantmentHelper.hasBindingCurse(itemstack) ? false : super.mayPickup(playerEntity); ++ return !itemstack.isEmpty() && !playerEntity.isCreative() && EnchantmentHelper.hasBindingCurse(itemstack) ? playerEntity.level.purpurConfig.playerRemoveBindingWithWeakness && playerEntity.hasEffect(MobEffects.WEAKNESS) : super.mayPickup(playerEntity); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java b/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java +index c34a66310969c3c837d09693159b827c1edddd3b..25885eb3b7312bd317fc519ad420109ff6531c7d 100644 +--- a/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java +@@ -140,7 +140,9 @@ public abstract class ItemCombinerMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ this.activeQuickItem = itemstack; // Purpur + slot1.onTake(player, itemstack1); ++ this.activeQuickItem = null; // Purpur + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java b/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java +index 59acb1aab21e2dce0f046942f124b50ac1cb8d0f..5058b30994fe38d8db2336267121476eaf4f1ff6 100644 +--- a/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java +@@ -29,11 +29,18 @@ public class PlayerEnderChestContainer extends SimpleContainer { + } + + public PlayerEnderChestContainer(Player owner) { +- super(27); ++ super(org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? 54 : 27); // Purpur + this.owner = owner; + // CraftBukkit end + } + ++ // Purpur start ++ @Override ++ public int getContainerSize() { ++ return owner.sixRowEnderchestSlotCount < 0 ? super.getContainerSize() : owner.sixRowEnderchestSlotCount; ++ } ++ // Purpur end ++ + public void setActiveChest(EnderChestBlockEntity blockEntity) { + this.activeChest = blockEntity; + } +diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java +index 9c8604376228c02f8bbd9a15673fbdf5097e7cb2..26ed7bc8bc76b81b3cb47784de158febb5719a81 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorItem.java +@@ -55,7 +55,7 @@ public class ArmorItem extends Item implements Wearable { + return false; + } else { + LivingEntity entityliving = (LivingEntity) list.get(0); +- EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(armor); ++ EquipmentSlot enumitemslot = pointer.getLevel().purpurConfig.dispenserApplyCursedArmor ? Mob.getEquipmentSlotForItem(armor) : Mob.getSlotForDispenser(armor); if (enumitemslot == null) return false; // Purpur + ItemStack itemstack1 = armor.copyWithCount(1); // Paper - shrink below and single item in event + // CraftBukkit start + Level world = pointer.getLevel(); +@@ -147,7 +147,14 @@ public class ArmorItem extends Item implements Wearable { + } + + itemstack.setCount(0); +- return InteractionResultHolder.sidedSuccess(itemstack, world.isClientSide()); ++ // Purpur start ++ return InteractionResultHolder.success(world.purpurConfig.playerArmorSwappingCreativeMakesCopy ? itemstack : ItemStack.EMPTY); ++ } else if (world.purpurConfig.playerArmorSwapping && !net.minecraft.world.item.enchantment.EnchantmentHelper.hasBindingCurse(itemstack1)) { ++ user.setItemSlot(enumitemslot, itemstack); ++ user.awardStat(Stats.ITEM_USED.get(this)); ++ user.level.playSound(null, user.getX(), user.getY(), user.getZ(), itemstack.getEquipSound(), net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); // we have to force the sound, for whatever reason ++ return InteractionResultHolder.success(itemstack1); ++ // Purpur end + } else { + return InteractionResultHolder.fail(itemstack); + } +diff --git a/src/main/java/net/minecraft/world/item/ArmorStandItem.java b/src/main/java/net/minecraft/world/item/ArmorStandItem.java +index 07850a88f3b8f834669394b733b9dca3968dfabc..d21a6d62d1d8b7c208e72acafc42f975012f7107 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorStandItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorStandItem.java +@@ -62,6 +62,14 @@ public class ArmorStandItem extends Item { + return InteractionResult.FAIL; + } + // CraftBukkit end ++ // Purpur start ++ if (world.purpurConfig.persistentDroppableEntityDisplayNames && itemstack.hasCustomHoverName()) { ++ entityarmorstand.setCustomName(itemstack.getHoverName()); ++ if (world.purpurConfig.armorstandSetNameVisible) { ++ entityarmorstand.setCustomNameVisible(true); ++ } ++ } ++ // Purpur end + worldserver.addFreshEntityWithPassengers(entityarmorstand); + world.playSound((Player) null, entityarmorstand.getX(), entityarmorstand.getY(), entityarmorstand.getZ(), SoundEvents.ARMOR_STAND_PLACE, SoundSource.BLOCKS, 0.75F, 0.8F); + entityarmorstand.gameEvent(GameEvent.ENTITY_PLACE, context.getPlayer()); +diff --git a/src/main/java/net/minecraft/world/item/AxeItem.java b/src/main/java/net/minecraft/world/item/AxeItem.java +index 1c096338a90d740a3813274278056017fa1ec844..1075e9e60e3243134483429b40bad8c89705fc9b 100644 +--- a/src/main/java/net/minecraft/world/item/AxeItem.java ++++ b/src/main/java/net/minecraft/world/item/AxeItem.java +@@ -33,29 +33,32 @@ public class AxeItem extends DiggerItem { + BlockPos blockPos = context.getClickedPos(); + Player player = context.getPlayer(); + BlockState blockState = level.getBlockState(blockPos); +- Optional optional = this.getStripped(blockState); +- Optional optional2 = WeatheringCopper.getPrevious(blockState); +- Optional optional3 = Optional.ofNullable(HoneycombItem.WAX_OFF_BY_BLOCK.get().get(blockState.getBlock())).map((block) -> { +- return block.withPropertiesOf(blockState); +- }); ++ // Purpur start ++ Block clickedBlock = level.getBlockState(blockPos).getBlock(); ++ Optional optional = Optional.ofNullable(level.purpurConfig.axeStrippables.get(blockState.getBlock())); ++ Optional optional2 = Optional.ofNullable(level.purpurConfig.axeWeatherables.get(blockState.getBlock())); ++ Optional optional3 = Optional.ofNullable(level.purpurConfig.axeWaxables.get(blockState.getBlock())); ++ // Purpur end + ItemStack itemStack = context.getItemInHand(); +- Optional optional4 = Optional.empty(); ++ Optional optional4 = Optional.empty(); // Purpur + if (optional.isPresent()) { +- level.playSound(player, blockPos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!STRIPPABLES.containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + optional4 = optional; + } else if (optional2.isPresent()) { +- level.playSound(player, blockPos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!HoneycombItem.WAXABLES.get().containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + level.levelEvent(player, 3005, blockPos, 0); + optional4 = optional2; + } else if (optional3.isPresent()) { +- level.playSound(player, blockPos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!HoneycombItem.WAX_OFF_BY_BLOCK.get().containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + level.levelEvent(player, 3004, blockPos, 0); + optional4 = optional3; + } + + if (optional4.isPresent()) { ++ org.purpurmc.purpur.tool.Actionable actionable = optional4.get(); ++ BlockState state = actionable.into().withPropertiesOf(blockState); // Purpur + // Paper start - EntityChangeBlockEvent +- if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, optional4.get()).isCancelled()) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, state).isCancelled()) { // Purpur + return InteractionResult.PASS; + } + // Paper end +@@ -63,15 +66,22 @@ public class AxeItem extends DiggerItem { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, blockPos, itemStack); + } + +- level.setBlock(blockPos, optional4.get(), 11); +- level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, optional4.get())); ++ // Purpur start ++ level.setBlock(blockPos, state, 11); ++ actionable.drops().forEach((drop, chance) -> { ++ if (level.random.nextDouble() < chance) { ++ Block.popResourceFromFace(level, blockPos, context.getClickedFace(), new ItemStack(drop)); ++ } ++ }); ++ level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, state)); ++ // Purpur end + if (player != null) { + itemStack.hurtAndBreak(1, player, (p) -> { + p.broadcastBreakEvent(context.getHand()); + }); + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } else { + return InteractionResult.PASS; + } +diff --git a/src/main/java/net/minecraft/world/item/BlockItem.java b/src/main/java/net/minecraft/world/item/BlockItem.java +index b0204af850ee182773ad458208cccd946ad148d5..193ae502acb622da3a42d49dc0c69da9d22f6ede 100644 +--- a/src/main/java/net/minecraft/world/item/BlockItem.java ++++ b/src/main/java/net/minecraft/world/item/BlockItem.java +@@ -153,7 +153,24 @@ public class BlockItem extends Item { + } + + protected boolean updateCustomBlockEntityTag(BlockPos pos, Level world, @Nullable Player player, ItemStack stack, BlockState state) { +- return BlockItem.updateCustomBlockEntityTag(world, player, pos, stack); ++ // Purpur start ++ boolean handled = updateCustomBlockEntityTag(world, player, pos, stack); ++ if (world.purpurConfig.persistentTileEntityDisplayNames && stack.hasTag()) { ++ CompoundTag display = stack.getTagElement("display"); ++ if (display != null) { ++ BlockEntity blockEntity = world.getBlockEntity(pos); ++ if (blockEntity != null) { ++ if (display.contains("Name", 8)) { ++ blockEntity.setPersistentDisplayName(display.getString("Name")); ++ } ++ if (display.contains("Lore", 9)) { ++ blockEntity.setPersistentLore(display.getList("Lore", 8)); ++ } ++ } ++ } ++ } ++ return handled; ++ // Purpur end + } + + @Nullable +@@ -288,7 +305,7 @@ public class BlockItem extends Item { + + @Override + public void onDestroyed(ItemEntity entity) { +- if (this.block instanceof ShulkerBoxBlock) { ++ if (this.block instanceof ShulkerBoxBlock && entity.level.purpurConfig.shulkerBoxItemDropContentsWhenDestroyed) { + ItemStack itemstack = entity.getItem(); + CompoundTag nbttagcompound = BlockItem.getBlockEntityData(itemstack); + +diff --git a/src/main/java/net/minecraft/world/item/BoatItem.java b/src/main/java/net/minecraft/world/item/BoatItem.java +index 1a95ac11a2fbc811c89afa3adf38e0fc9eaab09b..91280f8c39ea191b90da2a9ff5c49f43c255bd9a 100644 +--- a/src/main/java/net/minecraft/world/item/BoatItem.java ++++ b/src/main/java/net/minecraft/world/item/BoatItem.java +@@ -69,6 +69,11 @@ public class BoatItem extends Item { + + entityboat.setVariant(this.type); + entityboat.setYRot(user.getYRot()); ++ // Purpur start ++ if (world.purpurConfig.persistentDroppableEntityDisplayNames && itemstack.hasCustomHoverName()) { ++ entityboat.setCustomName(itemstack.getHoverName()); ++ } ++ // Purpur end + if (!world.noCollision(entityboat, entityboat.getBoundingBox())) { + return InteractionResultHolder.fail(itemstack); + } else { +diff --git a/src/main/java/net/minecraft/world/item/BowItem.java b/src/main/java/net/minecraft/world/item/BowItem.java +index 08d597db1a5345a343777a01427655e6bf2c926b..d45a2f49c82d00801578c34e5f5277fc5e82be87 100644 +--- a/src/main/java/net/minecraft/world/item/BowItem.java ++++ b/src/main/java/net/minecraft/world/item/BowItem.java +@@ -38,13 +38,13 @@ public class BowItem extends ProjectileWeaponItem implements Vanishable { + float f = BowItem.getPowerForTime(j); + + if ((double) f >= 0.1D) { +- boolean flag1 = flag && itemstack1.is(Items.ARROW); ++ boolean flag1 = flag && ((itemstack1.is(Items.ARROW) && world.purpurConfig.infinityWorksWithNormalArrows) || (itemstack1.is(Items.TIPPED_ARROW) && world.purpurConfig.infinityWorksWithTippedArrows) || (itemstack1.is(Items.SPECTRAL_ARROW) && world.purpurConfig.infinityWorksWithSpectralArrows)); // Purpur if (!world.isClientSide) { + + if (!world.isClientSide) { + ArrowItem itemarrow = (ArrowItem) (itemstack1.getItem() instanceof ArrowItem ? itemstack1.getItem() : Items.ARROW); + AbstractArrow entityarrow = itemarrow.createArrow(world, itemstack1, entityhuman); + +- entityarrow.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, f * 3.0F, 1.0F); ++ entityarrow.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, f * 3.0F, (float) world.purpurConfig.bowProjectileOffset); // Purpur + if (f == 1.0F) { + entityarrow.setCritArrow(true); + } +@@ -64,6 +64,13 @@ public class BowItem extends ProjectileWeaponItem implements Vanishable { + if (EnchantmentHelper.getItemEnchantmentLevel(Enchantments.FLAMING_ARROWS, stack) > 0) { + entityarrow.setSecondsOnFire(100); + } ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MOB_LOOTING, stack); ++ ++ if (lootingLevel > 0) { ++ entityarrow.setLootingLevel(lootingLevel); ++ } ++ // Purpur end + // CraftBukkit start + org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(entityhuman, stack, itemstack1, entityarrow, entityhuman.getUsedItemHand(), f, !flag1); + if (event.isCancelled()) { +@@ -132,7 +139,7 @@ public class BowItem extends ProjectileWeaponItem implements Vanishable { + ItemStack itemstack = user.getItemInHand(hand); + boolean flag = !user.getProjectile(itemstack).isEmpty(); + +- if (!user.getAbilities().instabuild && !flag) { ++ if (!(world.purpurConfig.infinityWorksWithoutArrows && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, itemstack) > 0) && !user.getAbilities().instabuild && !flag) { // Purpur + return InteractionResultHolder.fail(itemstack); + } else { + user.startUsingItem(hand); +diff --git a/src/main/java/net/minecraft/world/item/BucketItem.java b/src/main/java/net/minecraft/world/item/BucketItem.java +index 5c6aa9c464784ad5ee366412d080c72d3d22a76f..c03abc9589bf5f37abc1b0d355ed9784bac31a93 100644 +--- a/src/main/java/net/minecraft/world/item/BucketItem.java ++++ b/src/main/java/net/minecraft/world/item/BucketItem.java +@@ -166,7 +166,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + // CraftBukkit end + if (!flag1) { + return movingobjectpositionblock != null && this.emptyContents(entityhuman, world, movingobjectpositionblock.getBlockPos().relative(movingobjectpositionblock.getDirection()), (BlockHitResult) null, enumdirection, clicked, itemstack, enumhand); // CraftBukkit +- } else if (world.dimensionType().ultraWarm() && this.content.is(FluidTags.WATER)) { ++ } else if ((world.dimensionType().ultraWarm() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) && this.content.is(FluidTags.WATER)) { // Purpur + int i = blockposition.getX(); + int j = blockposition.getY(); + int k = blockposition.getZ(); +@@ -174,7 +174,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + world.playSound(entityhuman, blockposition, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.5F, 2.6F + (world.random.nextFloat() - world.random.nextFloat()) * 0.8F); + + for (int l = 0; l < 8; ++l) { +- world.addParticle(ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 0.0D, 0.0D, 0.0D); ++ ((ServerLevel) world).sendParticles(null, ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 1, 0.0D, 0.0D, 0.0D, 0.0D, true); // Purpur + } + + return true; +diff --git a/src/main/java/net/minecraft/world/item/CrossbowItem.java b/src/main/java/net/minecraft/world/item/CrossbowItem.java +index caa5f5f5d58b8ddbca0910412b695cb810570623..c4942deb963064c61d4ab95c27ed341a6648dcc8 100644 +--- a/src/main/java/net/minecraft/world/item/CrossbowItem.java ++++ b/src/main/java/net/minecraft/world/item/CrossbowItem.java +@@ -63,7 +63,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + ItemStack itemstack = user.getItemInHand(hand); + + if (CrossbowItem.isCharged(itemstack)) { +- CrossbowItem.performShooting(world, user, hand, itemstack, CrossbowItem.getShootingPower(itemstack), 1.0F); ++ CrossbowItem.performShooting(world, user, hand, itemstack, CrossbowItem.getShootingPower(itemstack), (float) world.purpurConfig.crossbowProjectileOffset); // Purpur + CrossbowItem.setCharged(itemstack, false); + return InteractionResultHolder.consume(itemstack); + } else if (!user.getProjectile(itemstack).isEmpty()) { +@@ -112,7 +112,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + // Paper end + int i = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MULTISHOT, projectile); + int j = i == 0 ? 1 : 3; +- boolean flag = !consume || shooter instanceof Player && ((Player) shooter).getAbilities().instabuild; // Paper - add consume ++ boolean flag = !consume || shooter instanceof Player && ((Player) shooter).getAbilities().instabuild || (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, projectile) > 0); // Paper - add consume // Purpur + ItemStack itemstack1 = shooter.getProjectile(projectile); + ItemStack itemstack2 = itemstack1.copy(); + +@@ -293,6 +293,14 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + entityarrow.setPierceLevel((byte) i); + } + ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MOB_LOOTING, crossbow); ++ ++ if (lootingLevel > 0) { ++ entityarrow.setLootingLevel(lootingLevel); ++ } ++ // Purpur end ++ + return entityarrow; + } + +@@ -302,7 +310,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + + for (int i = 0; i < list.size(); ++i) { + ItemStack itemstack1 = (ItemStack) list.get(i); +- boolean flag = entity instanceof Player && ((Player) entity).getAbilities().instabuild; ++ boolean flag = entity instanceof Player && ((Player) entity).getAbilities().instabuild || (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, stack) > 0); // Purpur + + if (!itemstack1.isEmpty()) { + if (i == 0) { +diff --git a/src/main/java/net/minecraft/world/item/DyeColor.java b/src/main/java/net/minecraft/world/item/DyeColor.java +index 2170715ed0e81a3055e4ab546c8b294c5ef7f142..beae4e2b9f61df83215de860d64c4ce2d3482004 100644 +--- a/src/main/java/net/minecraft/world/item/DyeColor.java ++++ b/src/main/java/net/minecraft/world/item/DyeColor.java +@@ -103,4 +103,10 @@ public enum DyeColor implements StringRepresentable { + public String getSerializedName() { + return this.name; + } ++ ++ // Purpur start ++ public static DyeColor random(net.minecraft.util.RandomSource random) { ++ return values()[random.nextInt(values().length)]; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/item/EggItem.java b/src/main/java/net/minecraft/world/item/EggItem.java +index 58cb992c5defec2f092755cbde661ff10f38bf9d..52f48681407d23f0925f4c9c072d5f0a2a6b1778 100644 +--- a/src/main/java/net/minecraft/world/item/EggItem.java ++++ b/src/main/java/net/minecraft/world/item/EggItem.java +@@ -24,7 +24,7 @@ public class EggItem extends Item { + ThrownEgg entityegg = new ThrownEgg(world, user); + + entityegg.setItem(itemstack); +- entityegg.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entityegg.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.eggProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entityegg.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entityegg)) { +diff --git a/src/main/java/net/minecraft/world/item/ElytraItem.java b/src/main/java/net/minecraft/world/item/ElytraItem.java +index 42f79d418ec4e2dbeac9a217d9dc144cda2ef714..250c0e31825f772d3fee7a523f150cb25e8ae558 100644 +--- a/src/main/java/net/minecraft/world/item/ElytraItem.java ++++ b/src/main/java/net/minecraft/world/item/ElytraItem.java +@@ -39,7 +39,14 @@ public class ElytraItem extends Item implements Wearable { + } + + itemStack.setCount(0); +- return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide()); ++ // Purpur start ++ return InteractionResultHolder.success(world.purpurConfig.playerArmorSwappingCreativeMakesCopy ? itemStack : ItemStack.EMPTY); ++ } else if (world.purpurConfig.playerArmorSwapping) { ++ user.setItemSlot(equipmentSlot, itemStack); ++ user.awardStat(Stats.ITEM_USED.get(this)); ++ user.level.playSound(null, user.getX(), user.getY(), user.getZ(), itemStack.getEquipSound(), net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); // we have to force the sound, for whatever reason ++ return InteractionResultHolder.success(itemStack2); ++ // Purpur end + } else { + return InteractionResultHolder.fail(itemStack); + } +diff --git a/src/main/java/net/minecraft/world/item/EnderpearlItem.java b/src/main/java/net/minecraft/world/item/EnderpearlItem.java +index 749ab72edc0d2e9c6f1161415ab8d59d3d6ca976..6b27d98d06b163243bb0e1bb979aad03f48d7770 100644 +--- a/src/main/java/net/minecraft/world/item/EnderpearlItem.java ++++ b/src/main/java/net/minecraft/world/item/EnderpearlItem.java +@@ -24,7 +24,7 @@ public class EnderpearlItem extends Item { + ThrownEnderpearl entityenderpearl = new ThrownEnderpearl(world, user); + + entityenderpearl.setItem(itemstack); +- entityenderpearl.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entityenderpearl.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.enderPearlProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entityenderpearl.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entityenderpearl)) { +@@ -36,7 +36,7 @@ public class EnderpearlItem extends Item { + + world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENDER_PEARL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (net.minecraft.world.entity.Entity.SHARED_RANDOM.nextFloat() * 0.4F + 0.8F)); + user.awardStat(Stats.ITEM_USED.get(this)); +- user.getCooldowns().addCooldown(this, 20); ++ user.getCooldowns().addCooldown(this, user.getAbilities().instabuild ? world.purpurConfig.enderPearlCooldownCreative : world.purpurConfig.enderPearlCooldown); // Purpur + } else { + // Paper end + if (user instanceof net.minecraft.server.level.ServerPlayer) { +diff --git a/src/main/java/net/minecraft/world/item/FireworkRocketItem.java b/src/main/java/net/minecraft/world/item/FireworkRocketItem.java +index 783791cf501d6ed3975aa82b958d7437158909ba..eb093b151e2d04476e38e3e0666888236c1ab057 100644 +--- a/src/main/java/net/minecraft/world/item/FireworkRocketItem.java ++++ b/src/main/java/net/minecraft/world/item/FireworkRocketItem.java +@@ -14,6 +14,7 @@ import net.minecraft.util.ByIdMap; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResult; + import net.minecraft.world.InteractionResultHolder; ++import net.minecraft.world.entity.EquipmentSlot; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.entity.projectile.FireworkRocketEntity; + import net.minecraft.world.item.context.UseOnContext; +@@ -68,6 +69,14 @@ public class FireworkRocketItem extends Item { + com.destroystokyo.paper.event.player.PlayerElytraBoostEvent event = new com.destroystokyo.paper.event.player.PlayerElytraBoostEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Firework) fireworkRocketEntity.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(fireworkRocketEntity)) { + user.awardStat(Stats.ITEM_USED.get(this)); ++ // Purpur start ++ if (world.purpurConfig.elytraDamagePerFireworkBoost > 0) { ++ ItemStack chestItem = user.getItemBySlot(EquipmentSlot.CHEST); ++ if (chestItem.getItem() == Items.ELYTRA) { ++ chestItem.hurtAndBreak(world.purpurConfig.elytraDamagePerFireworkBoost, user, (entityliving) -> entityliving.broadcastBreakEvent(EquipmentSlot.CHEST)); ++ } ++ } ++ // Purpur end + if (event.shouldConsume() && !user.getAbilities().instabuild) { + itemStack.shrink(1); + } else ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory(); +diff --git a/src/main/java/net/minecraft/world/item/HangingEntityItem.java b/src/main/java/net/minecraft/world/item/HangingEntityItem.java +index 489558eb0126e7a41e2e379e352bddc034375b61..062152b258224f28e07f96d6135bbb7a9f8a3f9a 100644 +--- a/src/main/java/net/minecraft/world/item/HangingEntityItem.java ++++ b/src/main/java/net/minecraft/world/item/HangingEntityItem.java +@@ -41,7 +41,7 @@ public class HangingEntityItem extends Item { + return InteractionResult.FAIL; + } else { + Level world = context.getLevel(); +- Object object; ++ Entity object; // Purpur + + if (this.type == EntityType.PAINTING) { + Optional optional = Painting.create(world, blockposition1, enumdirection); +@@ -65,6 +65,11 @@ public class HangingEntityItem extends Item { + + if (nbttagcompound != null) { + EntityType.updateCustomEntityTag(world, entityhuman, (Entity) object, nbttagcompound); ++ // Purpur start ++ if (world.purpurConfig.persistentDroppableEntityDisplayNames && itemstack.hasCustomHoverName()) { ++ object.setCustomName(itemstack.getHoverName()); ++ } ++ // Purpur end + } + + if (((HangingEntity) object).survives()) { +diff --git a/src/main/java/net/minecraft/world/item/HoeItem.java b/src/main/java/net/minecraft/world/item/HoeItem.java +index 180aec596110309aade13d2080f8824d152b07cb..c4aec1e5135a79837918b692e75a7b55d5cffeb0 100644 +--- a/src/main/java/net/minecraft/world/item/HoeItem.java ++++ b/src/main/java/net/minecraft/world/item/HoeItem.java +@@ -34,15 +34,23 @@ public class HoeItem extends DiggerItem { + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos blockPos = context.getClickedPos(); +- Pair, Consumer> pair = TILLABLES.get(level.getBlockState(blockPos).getBlock()); +- if (pair == null) { +- return InteractionResult.PASS; +- } else { +- Predicate predicate = pair.getFirst(); +- Consumer consumer = pair.getSecond(); ++ // Purpur start ++ Block clickedBlock = level.getBlockState(blockPos).getBlock(); ++ var tillable = level.purpurConfig.hoeTillables.get(level.getBlockState(blockPos).getBlock()); ++ if (tillable == null) { return InteractionResult.PASS; } else { ++ Predicate predicate = tillable.condition().predicate(); ++ Consumer consumer = (ctx) -> { ++ level.setBlock(blockPos, tillable.into().defaultBlockState(), 11); ++ tillable.drops().forEach((drop, chance) -> { ++ if (level.random.nextDouble() < chance) { ++ Block.popResourceFromFace(level, blockPos, ctx.getClickedFace(), new ItemStack(drop)); ++ } ++ }); ++ }; ++ // Purpur end + if (predicate.test(context)) { + Player player = context.getPlayer(); +- level.playSound(player, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!TILLABLES.containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + if (!level.isClientSide) { + consumer.accept(context); + if (player != null) { +@@ -52,7 +60,7 @@ public class HoeItem extends DiggerItem { + } + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } else { + return InteractionResult.PASS; + } +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index 31eed67d07097c7eb1b06547a9f556bcc709d96c..e2b341ecddb946e9c1e546b9706d86dd030db865 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -108,6 +108,7 @@ import org.bukkit.event.world.StructureGrowEvent; + + public final class ItemStack { + ++ public boolean isExactRecipeIngredient = false; // PaperPR + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> { + return instance.group(BuiltInRegistries.ITEM.byNameCodec().fieldOf("id").forGetter((itemstack) -> { + return itemstack.item; +@@ -413,6 +414,7 @@ public final class ItemStack { + world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 + for (BlockState blockstate : blocks) { + blockstate.update(true, false); ++ ((CraftBlock) blockstate.getBlock()).getNMS().getBlock().forgetPlacer(); // Purpur + } + world.preventPoiUpdated = false; + +@@ -442,6 +444,7 @@ public final class ItemStack { + if (!(block.getBlock() instanceof BaseEntityBlock)) { // Containers get placed automatically + block.getBlock().onPlace(block, world, newblockposition, oldBlock, true, itemactioncontext); // Paper - pass itemactioncontext + } ++ block.getBlock().forgetPlacer(); // Purpur + + world.notifyAndUpdatePhysics(newblockposition, null, oldBlock, block, world.getBlockState(newblockposition), updateFlag, 512); // send null chunk as chunk.k() returns false by this point + } +@@ -544,6 +547,16 @@ public final class ItemStack { + return this.isDamageableItem() && this.getDamageValue() > 0; + } + ++ // Purpur start ++ public float getDamagePercent() { ++ if (isDamaged()) { ++ return (float) getDamageValue() / (float) getItem().getMaxDamage(); ++ } else { ++ return 0F; ++ } ++ } ++ // Purpur end ++ + public int getDamageValue() { + return this.tag == null ? 0 : this.tag.getInt("Damage"); + } +@@ -563,7 +576,7 @@ public final class ItemStack { + int j; + + if (amount > 0) { +- j = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, this); ++ j = (getItem() == Items.ELYTRA && player != null && player.level.purpurConfig.elytraIgnoreUnbreaking) ? 0 : EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, this); + int k = 0; + + for (int l = 0; j > 0 && l < amount; ++l) { +@@ -618,6 +631,12 @@ public final class ItemStack { + if (this.hurt(amount, entity.getRandom(), entity /*instanceof ServerPlayer ? (ServerPlayer) entity : null*/)) { // Paper - pass LivingEntity for EntityItemDamageEvent + breakCallback.accept(entity); + Item item = this.getItem(); ++ // Purpur start ++ if (item == Items.ELYTRA) { ++ setDamageValue(item.getMaxDamage() - 1); ++ return; ++ } ++ // Purpur end + // CraftBukkit start - Check for item breaking + if (this.count == 1 && entity instanceof net.minecraft.world.entity.player.Player) { + org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemBreakEvent((net.minecraft.world.entity.player.Player) entity, this); +@@ -1141,7 +1160,7 @@ public final class ItemStack { + + ListTag nbttaglist = this.tag.getList("Enchantments", 10); + +- nbttaglist.add(EnchantmentHelper.storeEnchantment(EnchantmentHelper.getEnchantmentId(enchantment), (byte) level)); ++ nbttaglist.add(EnchantmentHelper.storeEnchantment(EnchantmentHelper.getEnchantmentId(enchantment), (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels) ? (byte) level : (short) level)); // Purpur + processEnchantOrder(this.tag); // Paper + } + +@@ -1149,6 +1168,12 @@ public final class ItemStack { + return this.tag != null && this.tag.contains("Enchantments", 9) ? !this.tag.getList("Enchantments", 10).isEmpty() : false; + } + ++ // Purpur start ++ public boolean hasEnchantment(Enchantment enchantment) { ++ return isEnchanted() && EnchantmentHelper.deserializeEnchantments(getEnchantmentTags()).containsKey(enchantment); ++ } ++ // Purpur end ++ + public void addTagElement(String key, Tag element) { + this.getOrCreateTag().put(key, element); + } +diff --git a/src/main/java/net/minecraft/world/item/Items.java b/src/main/java/net/minecraft/world/item/Items.java +index 21b74b3473553162c0113b2d365605782080cdfc..803bb632c2fb525b32e9bddc01f1c9112ee0bfba 100644 +--- a/src/main/java/net/minecraft/world/item/Items.java ++++ b/src/main/java/net/minecraft/world/item/Items.java +@@ -280,7 +280,7 @@ public class Items { + public static final Item PURPUR_BLOCK = registerBlock(Blocks.PURPUR_BLOCK); + public static final Item PURPUR_PILLAR = registerBlock(Blocks.PURPUR_PILLAR); + public static final Item PURPUR_STAIRS = registerBlock(Blocks.PURPUR_STAIRS); +- public static final Item SPAWNER = registerBlock(Blocks.SPAWNER); ++ public static final Item SPAWNER = registerBlock(new org.purpurmc.purpur.item.SpawnerItem(Blocks.SPAWNER, new Item.Properties().rarity(Rarity.EPIC))); // Purpur + public static final Item CHEST = registerBlock(Blocks.CHEST); + public static final Item CRAFTING_TABLE = registerBlock(Blocks.CRAFTING_TABLE); + public static final Item FARMLAND = registerBlock(Blocks.FARMLAND); +@@ -1153,7 +1153,7 @@ public class Items { + public static final Item LANTERN = registerBlock(Blocks.LANTERN); + public static final Item SOUL_LANTERN = registerBlock(Blocks.SOUL_LANTERN); + public static final Item SWEET_BERRIES = registerItem("sweet_berries", new ItemNameBlockItem(Blocks.SWEET_BERRY_BUSH, (new Item.Properties()).food(Foods.SWEET_BERRIES))); +- public static final Item GLOW_BERRIES = registerItem("glow_berries", new ItemNameBlockItem(Blocks.CAVE_VINES, (new Item.Properties()).food(Foods.GLOW_BERRIES))); ++ public static final Item GLOW_BERRIES = registerItem("glow_berries", new org.purpurmc.purpur.item.GlowBerryItem(Blocks.CAVE_VINES, (new Item.Properties()).food(Foods.GLOW_BERRIES))); // Purpur + public static final Item CAMPFIRE = registerBlock(Blocks.CAMPFIRE); + public static final Item SOUL_CAMPFIRE = registerBlock(Blocks.SOUL_CAMPFIRE); + public static final Item SHROOMLIGHT = registerBlock(Blocks.SHROOMLIGHT); +@@ -1236,6 +1236,13 @@ public class Items { + ((BlockItem)item).registerBlocks(Item.BY_BLOCK, item); + } + ++ // Purpur start ++ if (item.getFoodProperties() != null) { ++ Foods.ALL_PROPERTIES.put(id.getPath(), item.getFoodProperties()); ++ Foods.DEFAULT_PROPERTIES.put(id.getPath(), item.getFoodProperties().copy()); ++ } ++ // Purpur end ++ + return Registry.register(BuiltInRegistries.ITEM, id, item); + } + } +diff --git a/src/main/java/net/minecraft/world/item/MilkBucketItem.java b/src/main/java/net/minecraft/world/item/MilkBucketItem.java +index f33977d95b6db473be4f95075ba99caf90ad0220..56dc04d8875971ee9a5d077a695509af74fe2473 100644 +--- a/src/main/java/net/minecraft/world/item/MilkBucketItem.java ++++ b/src/main/java/net/minecraft/world/item/MilkBucketItem.java +@@ -5,6 +5,8 @@ import net.minecraft.server.level.ServerPlayer; + import net.minecraft.stats.Stats; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResultHolder; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.LivingEntity; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.level.Level; +@@ -31,7 +33,9 @@ public class MilkBucketItem extends Item { + } + + if (!world.isClientSide) { ++ MobEffectInstance badOmen = user.getEffect(MobEffects.BAD_OMEN); + user.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.MILK); // CraftBukkit ++ if (!world.purpurConfig.milkCuresBadOmen && badOmen != null) user.addEffect(badOmen); // Purpur + } + + return stack.isEmpty() ? new ItemStack(Items.BUCKET) : stack; +diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java +index c6d2f764efa9b8bec730bbe757d480e365b25ccc..33a30d26da2401535f0a72acb2bbffec1aef151e 100644 +--- a/src/main/java/net/minecraft/world/item/MinecartItem.java ++++ b/src/main/java/net/minecraft/world/item/MinecartItem.java +@@ -120,8 +120,9 @@ public class MinecartItem extends Item { + BlockState iblockdata = world.getBlockState(blockposition); + + if (!iblockdata.is(BlockTags.RAILS)) { +- return InteractionResult.FAIL; +- } else { ++ if (!world.purpurConfig.minecartPlaceAnywhere) return InteractionResult.FAIL; ++ if (iblockdata.getMaterial().isSolid()) blockposition = blockposition.relative(context.getClickedFace()); ++ } // else { // Purpur - place minecarts anywhere + ItemStack itemstack = context.getItemInHand(); + + if (!world.isClientSide) { +@@ -149,6 +150,6 @@ public class MinecartItem extends Item { + + itemstack.shrink(1); + return InteractionResult.sidedSuccess(world.isClientSide); +- } ++ // } // Purpur - place minecarts anywhere + } + } +diff --git a/src/main/java/net/minecraft/world/item/NameTagItem.java b/src/main/java/net/minecraft/world/item/NameTagItem.java +index 623f78c078fb3aa2665d7e8a37672438227bce6b..500c69e555c7247e20ef8cc59d83415578f44427 100644 +--- a/src/main/java/net/minecraft/world/item/NameTagItem.java ++++ b/src/main/java/net/minecraft/world/item/NameTagItem.java +@@ -24,6 +24,7 @@ public class NameTagItem extends Item { + if (!event.callEvent()) return InteractionResult.PASS; + LivingEntity newEntityLiving = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getEntity()).getHandle(); + newEntityLiving.setCustomName(event.getName() != null ? PaperAdventure.asVanilla(event.getName()) : null); ++ if (user.level.purpurConfig.armorstandFixNametags && entity instanceof net.minecraft.world.entity.decoration.ArmorStand) entity.setCustomNameVisible(true); // Purpur + if (event.isPersistent() && newEntityLiving instanceof Mob) { + ((Mob) newEntityLiving).setPersistenceRequired(); + // Paper end +diff --git a/src/main/java/net/minecraft/world/item/ShovelItem.java b/src/main/java/net/minecraft/world/item/ShovelItem.java +index c7195f2e12bbd6545f7bffcc2b4ba5cc3d48df20..5e730bc9c8ff94b16ac2bf8567dda8aea2ee4b2a 100644 +--- a/src/main/java/net/minecraft/world/item/ShovelItem.java ++++ b/src/main/java/net/minecraft/world/item/ShovelItem.java +@@ -34,7 +34,7 @@ public class ShovelItem extends DiggerItem { + return InteractionResult.PASS; + } else { + Player player = context.getPlayer(); +- BlockState blockState2 = FLATTENABLES.get(blockState.getBlock()); ++ BlockState blockState2 = level.purpurConfig.shovelTurnsBlockToGrassPath.contains(blockState.getBlock()) ? Blocks.DIRT_PATH.defaultBlockState() : null; // Purpur + BlockState blockState3 = null; + Runnable afterAction = null; // Paper + if (blockState2 != null && level.getBlockState(blockPos.above()).isAir()) { +diff --git a/src/main/java/net/minecraft/world/item/SnowballItem.java b/src/main/java/net/minecraft/world/item/SnowballItem.java +index ef3f90a5bcdd7b9815a4053cff166f9d2552f55d..e7e5e1cc92f56e3daba8fa09c59188febec5e8f2 100644 +--- a/src/main/java/net/minecraft/world/item/SnowballItem.java ++++ b/src/main/java/net/minecraft/world/item/SnowballItem.java +@@ -25,7 +25,7 @@ public class SnowballItem extends Item { + Snowball entitysnowball = new Snowball(world, user); + + entitysnowball.setItem(itemstack); +- entitysnowball.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entitysnowball.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.snowballProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entitysnowball.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entitysnowball)) { +diff --git a/src/main/java/net/minecraft/world/item/SpawnEggItem.java b/src/main/java/net/minecraft/world/item/SpawnEggItem.java +index 31268e25056f980798ef7db72c4f955a074cc639..955822da142a2463af536dd1ce48037deda41402 100644 +--- a/src/main/java/net/minecraft/world/item/SpawnEggItem.java ++++ b/src/main/java/net/minecraft/world/item/SpawnEggItem.java +@@ -68,6 +68,15 @@ public class SpawnEggItem extends Item { + SpawnerBlockEntity tileentitymobspawner = (SpawnerBlockEntity) tileentity; + EntityType entitytypes = this.getType(itemstack.getTag()); + ++ // Purpur start ++ org.bukkit.block.Block bukkitBlock = world.getWorld().getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()); ++ org.purpurmc.purpur.event.PlayerSetSpawnerTypeWithEggEvent event = new org.purpurmc.purpur.event.PlayerSetSpawnerTypeWithEggEvent((org.bukkit.entity.Player) context.getPlayer().getBukkitEntity(), bukkitBlock, (org.bukkit.block.CreatureSpawner) bukkitBlock.getState(), org.bukkit.entity.EntityType.fromName(entitytypes.getName())); ++ if (!event.callEvent()) { ++ return InteractionResult.FAIL; ++ } ++ entitytypes = EntityType.getFromBukkitType(event.getEntityType()); ++ // Purpur end ++ + tileentitymobspawner.setEntityId(entitytypes, world.getRandom()); + tileentity.setChanged(); + world.sendBlockUpdated(blockposition, iblockdata, iblockdata, 3); +diff --git a/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java b/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java +index de5bdceb4c8578fb972a2fd5ee0dfdae509e46dc..bcf63ccb6e679cb97d658780b2663aafa3568bcb 100644 +--- a/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java ++++ b/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java +@@ -18,7 +18,7 @@ public class ThrowablePotionItem extends PotionItem { + if (!world.isClientSide) { + ThrownPotion thrownPotion = new ThrownPotion(world, user); + thrownPotion.setItem(itemStack); +- thrownPotion.shootFromRotation(user, user.getXRot(), user.getYRot(), -20.0F, 0.5F, 1.0F); ++ thrownPotion.shootFromRotation(user, user.getXRot(), user.getYRot(), -20.0F, 0.5F, (float) world.purpurConfig.throwablePotionProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) thrownPotion.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(thrownPotion)) { +diff --git a/src/main/java/net/minecraft/world/item/TridentItem.java b/src/main/java/net/minecraft/world/item/TridentItem.java +index 9365f886a23a71c41091b22d46896ff18a5a0635..d35432087c70ce66b74d1e27df19f462f22b1aa1 100644 +--- a/src/main/java/net/minecraft/world/item/TridentItem.java ++++ b/src/main/java/net/minecraft/world/item/TridentItem.java +@@ -77,11 +77,19 @@ public class TridentItem extends Item implements Vanishable { + if (k == 0) { + ThrownTrident entitythrowntrident = new ThrownTrident(world, entityhuman, stack); + +- entitythrowntrident.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, 2.5F + (float) k * 0.5F, 1.0F); ++ entitythrowntrident.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, 2.5F + (float) k * 0.5F, (float) world.purpurConfig.tridentProjectileOffset); // Purpur + if (entityhuman.getAbilities().instabuild) { + entitythrowntrident.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; + } + ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MOB_LOOTING, stack); ++ ++ if (lootingLevel > 0) { ++ entitythrowntrident.setLootingLevel(lootingLevel); ++ } ++ // Purpur end ++ + // CraftBukkit start + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) entityhuman.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), (org.bukkit.entity.Projectile) entitythrowntrident.getBukkitEntity()); +@@ -130,6 +138,14 @@ public class TridentItem extends Item implements Vanishable { + f2 *= f6 / f5; + f3 *= f6 / f5; + f4 *= f6 / f5; ++ ++ // Purpur start ++ ItemStack chestItem = entityhuman.getItemBySlot(EquipmentSlot.CHEST); ++ if (chestItem.getItem() == Items.ELYTRA && world.purpurConfig.elytraDamagePerTridentBoost > 0) { ++ chestItem.hurtAndBreak(world.purpurConfig.elytraDamagePerTridentBoost, entityhuman, (entity) -> entity.broadcastBreakEvent(EquipmentSlot.CHEST)); ++ } ++ // Purpur end ++ + entityhuman.push((double) f2, (double) f3, (double) f4); + entityhuman.startAutoSpinAttack(20); + if (entityhuman.isOnGround()) { +diff --git a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java +index 8d4aca59bd7518179520f4d4fb7137778e232d90..e24034d1ce4bb529de084aab69a531227e0c2f79 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java ++++ b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java +@@ -39,6 +39,7 @@ public final class Ingredient implements Predicate { + @Nullable + private IntList stackingIds; + public boolean exact; // CraftBukkit ++ public Predicate predicate; + + public Ingredient(Stream entries) { + this.values = (Ingredient.Value[]) entries.toArray((i) -> { +@@ -50,7 +51,11 @@ public final class Ingredient implements Predicate { + if (this.itemStacks == null) { + this.itemStacks = (ItemStack[]) Arrays.stream(this.values).flatMap((recipeitemstack_provider) -> { + return recipeitemstack_provider.getItems().stream(); +- }).distinct().toArray((i) -> { ++ // PaperPR start ++ }).distinct().peek(stack -> { ++ stack.isExactRecipeIngredient = this.exact; ++ }).toArray((i) -> { ++ // PaperPR end + return new ItemStack[i]; + }); + } +@@ -64,6 +69,12 @@ public final class Ingredient implements Predicate { + } else if (this.isEmpty()) { + return itemstack.isEmpty(); + } else { ++ // Purpur start ++ if (predicate != null) { ++ return predicate.test(itemstack.asBukkitCopy()); ++ } ++ // Purpur end ++ + ItemStack[] aitemstack = this.getItems(); + int i = aitemstack.length; + +@@ -99,7 +110,13 @@ public final class Ingredient implements Predicate { + for (int j = 0; j < i; ++j) { + ItemStack itemstack = aitemstack1[j]; + ++ // PaperPR start ++ if (itemstack.isExactRecipeIngredient) { ++ this.stackingIds.add(StackedContents.getExactStackingIndex(itemstack)); ++ } else { ++ // PaperPR end + this.stackingIds.add(StackedContents.getStackingIndex(itemstack)); ++ } // PaperPR + } + + this.stackingIds.sort(IntComparators.NATURAL_COMPARATOR); +diff --git a/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java b/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java +index 3aece8245060dd1ba269c08d226c84247a6f0a83..38703baaef5f04a53081620ce1bf29b45e4d62d1 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java +@@ -7,6 +7,14 @@ public class ArrowInfiniteEnchantment extends Enchantment { + super(weight, EnchantmentCategory.BOW, slotTypes); + } + ++ // Purpur start ++ @Override ++ public boolean canEnchant(net.minecraft.world.item.ItemStack stack) { ++ // we have to cheat the system because this class is loaded before purpur's config is loaded ++ return (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity ? EnchantmentCategory.BOW_AND_CROSSBOW : EnchantmentCategory.BOW).canEnchant(stack.getItem()); ++ } ++ // Purpur end ++ + @Override + public int getMinCost(int level) { + return 20; +@@ -24,6 +32,6 @@ public class ArrowInfiniteEnchantment extends Enchantment { + + @Override + public boolean checkCompatibility(Enchantment other) { +- return other instanceof MendingEnchantment ? false : super.checkCompatibility(other); ++ return other instanceof MendingEnchantment ? org.purpurmc.purpur.PurpurConfig.allowInfinityMending : super.checkCompatibility(other); + } + } +diff --git a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java +index 6f6106ca4d74d50a7b74b086adc96c58c7906cb6..a19dd0946f853193ff32b2b560db27534b8b4abf 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java +@@ -97,6 +97,20 @@ public enum EnchantmentCategory { + public boolean canEnchant(Item item) { + return item instanceof Vanishable || Block.byItem(item) instanceof Vanishable || BREAKABLE.canEnchant(item); + } ++ // Purpur start ++ }, ++ BOW_AND_CROSSBOW { ++ @Override ++ public boolean canEnchant(Item item) { ++ return item instanceof BowItem || item instanceof CrossbowItem; ++ } ++ }, ++ WEAPON_AND_SHEARS { ++ @Override ++ public boolean canEnchant(Item item) { ++ return WEAPON.canEnchant(item) || item instanceof net.minecraft.world.item.ShearsItem; ++ } ++ // Purpur end + }; + + public abstract boolean canEnchant(Item item); +diff --git a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java +index 064783822333d11120daa28f3be5099e10510b72..b96b1e4efa35b796a985bf1eb4a7158c1706a34c 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java +@@ -46,7 +46,7 @@ public class EnchantmentHelper { + } + + public static int getEnchantmentLevel(CompoundTag nbt) { +- return Mth.clamp(nbt.getInt("lvl"), 0, 255); ++ return Mth.clamp(nbt.getInt("lvl"), 0, (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels) ? 255 : 32767); // Purpur + } + + @Nullable +@@ -274,6 +274,29 @@ public class EnchantmentHelper { + return getItemEnchantmentLevel(Enchantments.CHANNELING, stack) > 0; + } + ++ // Purpur start ++ @Nullable ++ public static Map.Entry getMostDamagedEquipment(Enchantment enchantment, LivingEntity entity) { ++ Map map = enchantment.getSlotItems(entity); ++ if (map.isEmpty()) { ++ return null; ++ } ++ Map.Entry item = null; ++ float maxPercent = 0F; ++ for (Map.Entry entry : map.entrySet()) { ++ ItemStack itemstack = entry.getValue(); ++ if (!itemstack.isEmpty() && itemstack.isDamaged() && getItemEnchantmentLevel(enchantment, itemstack) > 0) { ++ float percent = itemstack.getDamagePercent(); ++ if (item == null || percent > maxPercent) { ++ item = entry; ++ maxPercent = percent; ++ } ++ } ++ } ++ return item; ++ } ++ // Purpur end ++ + @Nullable + public static Map.Entry getRandomItemWith(Enchantment enchantment, LivingEntity entity) { + return getRandomItemWith(enchantment, entity, (stack) -> { +diff --git a/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java b/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java +index 6b8a1535086aae7e4e3229d05615fb903188f507..60af917083de1b790b1d93d61835a669143068fb 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java +@@ -7,6 +7,14 @@ public class LootBonusEnchantment extends Enchantment { + super(weight, type, slotTypes); + } + ++ // Purpur start ++ @Override ++ public boolean canEnchant(net.minecraft.world.item.ItemStack stack) { ++ // we have to cheat the system because this class is loaded before purpur's config is loaded ++ return (org.purpurmc.purpur.PurpurConfig.allowShearsLooting && this.category == EnchantmentCategory.WEAPON ? EnchantmentCategory.WEAPON_AND_SHEARS : this.category).canEnchant(stack.getItem()); ++ } ++ // Purpur end ++ + @Override + public int getMinCost(int level) { + return 15 + (level - 1) * 9; +diff --git a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +index 8a9a701baabdaf066cd9b28c05430f673fcafb4e..17cc3237c7fc8ceda136b2371fabf6f004a991aa 100644 +--- a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java ++++ b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +@@ -132,7 +132,12 @@ public class MerchantOffer { + } + + public void updateDemand() { +- this.demand = Math.max(0, this.demand + this.uses - (this.maxUses - this.uses)); // Paper ++ // Purpur start ++ this.updateDemand(0); ++ } ++ public void updateDemand(int minimumDemand) { ++ this.demand = Math.max(minimumDemand, this.demand + this.uses - (this.maxUses - this.uses)); ++ // Purpur end + } + + public ItemStack assemble() { +diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java +index af799b61cec48ca290ed66cb47cfc0b244ac41a7..4e1e1fdbf12768b95dd499bf011009a4c4ca2306 100644 +--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java ++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java +@@ -55,6 +55,7 @@ public abstract class BaseSpawner { + } + + public boolean isNearPlayer(Level world, BlockPos pos) { ++ if (world.purpurConfig.spawnerDeactivateByRedstone && world.hasNeighborSignal(pos)) return false; // Purpur + return world.hasNearbyAlivePlayerThatAffectsSpawning((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); // Paper - Affects Spawning API + } + +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 3b959f42d958bf0f426853aee56753d6c455fcdb..d17abb283ea818244df0379d6b57fc634071e0b9 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -154,7 +154,7 @@ public interface EntityGetter { + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { + for(Player player : this.players()) { +- if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { ++ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player) && EntitySelector.notAfk.test(player)) { + double d = player.distanceToSqr(x, y, z); + if (range < 0.0D || d < range * range) { + return true; +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index a213f4098859858a73ddd601bbe8c7511972e0d5..cd63170e56b402fbf8bbd904270979fa51330e26 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -86,7 +86,7 @@ public class Explosion { + this.hitPlayers = Maps.newHashMap(); + this.level = world; + this.source = entity; +- this.radius = (float) Math.max(power, 0.0); // CraftBukkit - clamp bad values ++ this.radius = (float) (world.purpurConfig.explosionClampRadius ? Math.max(power, 0.0) : power); // CraftBukkit - clamp bad values // Purpur + this.x = x; + this.y = y; + this.z = z; +@@ -137,10 +137,27 @@ public class Explosion { + + public void explode() { + // CraftBukkit start +- if (this.radius < 0.1F) { ++ if (this.level.purpurConfig.explosionClampRadius && this.radius < 0.1F) { // Purpur + return; + } + // CraftBukkit end ++ ++ // Purpur start - add PreExplodeEvents ++ if(this.source != null){ ++ Location location = new Location(this.level.getWorld(), this.x, this.y, this.z); ++ if(!new org.purpurmc.purpur.event.entity.PreEntityExplodeEvent(this.source.getBukkitEntity(), location, this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F).callEvent()) { ++ this.wasCanceled = true; ++ return; ++ } ++ }else { ++ Location location = new Location(this.level.getWorld(), this.x, this.y, this.z); ++ if(!new org.purpurmc.purpur.event.PreBlockExplodeEvent(location.getBlock(), this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F).callEvent()) { ++ this.wasCanceled = true; ++ return; ++ } ++ } ++ //Purpur end ++ + this.level.gameEvent(this.source, GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); + Set set = Sets.newHashSet(); + boolean flag = true; +@@ -360,7 +377,7 @@ public class Explosion { + if (!iblockdata.isAir() && iblockdata.isDestroyable()) { // Paper + BlockPos blockposition1 = blockposition.immutable(); + +- this.level.getProfiler().push("explosion_blocks"); ++ //this.level.getProfiler().push("explosion_blocks"); // Purpur + if (block.dropFromExplosion(this)) { + Level world = this.level; + +@@ -382,7 +399,7 @@ public class Explosion { + + this.level.setBlock(blockposition, Blocks.AIR.defaultBlockState(), 3); + block.wasExploded(this.level, blockposition, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + } + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index eb0a31c885ea64da00abcd5e67083392138b1ca0..fef709fce7309795b6d62d33a220a2be2399efd3 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -173,6 +173,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Paper end + + public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray ++ public final org.purpurmc.purpur.PurpurWorldConfig purpurConfig; // Purpur ++ + public final co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; +@@ -190,6 +192,49 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - fix and optimise world upgrading + ++ // Purpur start ++ private com.google.common.cache.Cache playerBreedingCooldowns; ++ ++ private com.google.common.cache.Cache getNewBreedingCooldownCache() { ++ return com.google.common.cache.CacheBuilder.newBuilder().expireAfterWrite(this.purpurConfig.animalBreedingCooldownSeconds, java.util.concurrent.TimeUnit.SECONDS).build(); ++ } ++ ++ public void resetBreedingCooldowns() { ++ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); ++ } ++ ++ public boolean hasBreedingCooldown(java.util.UUID player, Class animalType) { // Purpur ++ return this.playerBreedingCooldowns.getIfPresent(new BreedingCooldownPair(player, animalType)) != null; ++ } ++ ++ public void addBreedingCooldown(java.util.UUID player, Class animalType) { ++ this.playerBreedingCooldowns.put(new BreedingCooldownPair(player, animalType), new Object()); ++ } ++ ++ private static final class BreedingCooldownPair { ++ private final java.util.UUID playerUUID; ++ private final Class animalType; ++ ++ public BreedingCooldownPair(java.util.UUID playerUUID, Class animalType) { ++ this.playerUUID = playerUUID; ++ this.animalType = animalType; ++ } ++ ++ @Override ++ public boolean equals(Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ BreedingCooldownPair that = (BreedingCooldownPair) o; ++ return playerUUID.equals(that.playerUUID) && animalType.equals(that.animalType); ++ } ++ ++ @Override ++ public int hashCode() { ++ return java.util.Objects.hash(playerUUID, animalType); ++ } ++ } ++ // Purpur end ++ + public CraftWorld getWorld() { + return this.world; + } +@@ -270,7 +315,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey getTypeKey(); + +- protected final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(java.util.concurrent.ThreadLocalRandom.current().nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - move thread unsafe random initialization // Pufferfish - getter ++ //protected final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(java.util.concurrent.ThreadLocalRandom.current().nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - move thread unsafe random initialization // Pufferfish - getter // Purpur - dont break ABI + + // Pufferfish start - ensure these get inlined + private final int minBuildHeight, minSection, height, maxBuildHeight, maxSection; +@@ -284,6 +329,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper ++ this.purpurConfig = new org.purpurmc.purpur.PurpurWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), env); // Purpur ++ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur + this.generator = gen; + this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env); + +@@ -666,9 +713,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + BlockState iblockdata2 = this.getBlockState(pos); + + if ((flags & 128) == 0 && iblockdata2 != iblockdata1 && (iblockdata2.getLightBlock(this, pos) != iblockdata1.getLightBlock(this, pos) || iblockdata2.getLightEmission() != iblockdata1.getLightEmission() || iblockdata2.useShapeForLightOcclusion() || iblockdata1.useShapeForLightOcclusion())) { +- this.getProfiler().push("queueCheckLight"); ++ //this.getProfiler().push("queueCheckLight"); // Purpur + this.getChunkSource().getLightEngine().checkBlock(pos); +- this.getProfiler().pop(); ++ //this.getProfiler().pop(); // Purpur + } + + /* +@@ -967,18 +1014,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + protected void tickBlockEntities() { +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push("blockEntities"); +- timings.tileEntityPending.startTiming(); // Spigot ++ //gameprofilerfiller.push("blockEntities"); // Purpur ++ //timings.tileEntityPending.startTiming(); // Spigot // Purpur + this.tickingBlockEntities = true; + if (!this.pendingBlockEntityTickers.isEmpty()) { + this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); + this.pendingBlockEntityTickers.clear(); + } +- timings.tileEntityPending.stopTiming(); // Spigot ++ //timings.tileEntityPending.stopTiming(); // Spigot // Purpur + +- timings.tileEntityTick.startTiming(); // Spigot ++ //timings.tileEntityTick.startTiming(); // Spigot // Purpur + // Spigot start + // Iterator iterator = this.blockEntityTickers.iterator(); + int tilesThisCycle = 0; +@@ -1011,10 +1058,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + this.blockEntityTickers.removeAll(toRemove); + +- timings.tileEntityTick.stopTiming(); // Spigot ++ //timings.tileEntityTick.stopTiming(); // Spigot // Purpur + this.tickingBlockEntities = false; + co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + spigotConfig.currentPrimedTnt = 0; // Spigot + } + +@@ -1207,7 +1254,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { +- this.getProfiler().incrementCounter("getEntities"); ++ //this.getProfiler().incrementCounter("getEntities"); // Purpur + List list = Lists.newArrayList(); + ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call + return list; +@@ -1226,7 +1273,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { +- this.getProfiler().incrementCounter("getEntities"); ++ //this.getProfiler().incrementCounter("getEntities"); // Purpur + // Paper start - optimise this call + //TODO use limit + if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) { +@@ -1555,7 +1602,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public ProfilerFiller getProfiler() { +- if (gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish ++ if (true || gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish // Purpur + return (ProfilerFiller) this.profiler.get(); + } + +@@ -1637,4 +1684,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return null; + } + // Paper end ++ ++ // Purpur start ++ public boolean isNether() { ++ return getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER; ++ } ++ ++ public boolean isTheEnd() { ++ return getWorld().getEnvironment() == org.bukkit.World.Environment.THE_END; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 5521418fa307b3eeb4f02a10c39f05b360d1d06e..81ba3c4fa9502cdd2a5c58b0ff51fea6b7553f4a 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -132,8 +132,8 @@ public final class NaturalSpawner { + } + + public static void spawnForChunk(ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rareSpawn) { +- world.getProfiler().push("spawner"); +- world.timings.mobSpawn.startTiming(); // Spigot ++ //world.getProfiler().push("spawner"); // Purpur ++ //world.timings.mobSpawn.startTiming(); // Spigot // Purpur + MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES; + int i = aenumcreaturetype.length; + +@@ -188,8 +188,8 @@ public final class NaturalSpawner { + } + } + +- world.timings.mobSpawn.stopTiming(); // Spigot +- world.getProfiler().pop(); ++ //world.timings.mobSpawn.stopTiming(); // Spigot // Purpur ++ //world.getProfiler().pop(); // Purpur + } + + // Paper start +@@ -260,7 +260,7 @@ public final class NaturalSpawner { + blockposition_mutableblockposition.set(l, i, i1); + double d0 = (double) l + 0.5D; + double d1 = (double) i1 + 0.5D; +- Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); // Paper - use chunk's player cache to optimize search in range ++ Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, world.purpurConfig.mobSpawningIgnoreCreativePlayers ? net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR : net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, world.purpurConfig.mobSpawningIgnoreCreativePlayers); // Paper - use chunk's player cache to optimize search in range // Purpur + + if (entityhuman != null) { + double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); +diff --git a/src/main/java/net/minecraft/world/level/block/AnvilBlock.java b/src/main/java/net/minecraft/world/level/block/AnvilBlock.java +index 2aac479ce9886cfef99823a41205eb52b7996d26..ff9945a04ffbda5766bc794fced24f14d8efcdeb 100644 +--- a/src/main/java/net/minecraft/world/level/block/AnvilBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/AnvilBlock.java +@@ -55,6 +55,54 @@ public class AnvilBlock extends FallingBlock { + + @Override + public InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { ++ // Purpur start - repairable/damageable anvils ++ if (world.purpurConfig.anvilRepairIngotsAmount > 0) { ++ net.minecraft.world.item.ItemStack itemstack = player.getItemInHand(hand); ++ if (itemstack.is(net.minecraft.world.item.Items.IRON_INGOT)) { ++ if (itemstack.getCount() < world.purpurConfig.anvilRepairIngotsAmount) { ++ // not enough iron ingots, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ if (state.is(Blocks.DAMAGED_ANVIL)) { ++ world.setBlock(pos, Blocks.CHIPPED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.CHIPPED_ANVIL)) { ++ world.setBlock(pos, Blocks.ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.ANVIL)) { ++ // anvil is already fully repaired, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(world.purpurConfig.anvilRepairIngotsAmount); ++ } ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_PLACE, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ } ++ if (world.purpurConfig.anvilDamageObsidianAmount > 0) { ++ net.minecraft.world.item.ItemStack itemstack = player.getItemInHand(hand); ++ if (itemstack.is(net.minecraft.world.item.Items.OBSIDIAN)) { ++ if (itemstack.getCount() < world.purpurConfig.anvilDamageObsidianAmount) { ++ // not enough obsidian, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ if (state.is(Blocks.DAMAGED_ANVIL)) { ++ world.destroyBlock(pos, false); ++ } else if (state.is(Blocks.CHIPPED_ANVIL)) { ++ world.setBlock(pos, Blocks.DAMAGED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.ANVIL)) { ++ world.setBlock(pos, Blocks.CHIPPED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(world.purpurConfig.anvilDamageObsidianAmount); ++ } ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_LAND, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ } ++ // Purpur end + if (world.isClientSide) { + return InteractionResult.SUCCESS; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java b/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java +index 023ed8441d629629828051b4098b09b06ce51a75..0123904a521be6b2f8d9056769e98982d9e14ffa 100644 +--- a/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java +@@ -43,6 +43,20 @@ public class AzaleaBlock extends BushBlock implements BonemealableBlock { + + @Override + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ // Purpur start ++ growTree(world, random, pos, state); ++ } ++ ++ @Override ++ public void randomTick(net.minecraft.world.level.block.state.BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { ++ double chance = state.getBlock() == Blocks.FLOWERING_AZALEA ? world.purpurConfig.floweringAzaleaGrowthChance : world.purpurConfig.azaleaGrowthChance; ++ if (chance > 0.0D && world.getMaxLocalRawBrightness(pos.above()) > 9 && random.nextDouble() < chance) { ++ growTree(world, random, pos, state); ++ } ++ } ++ ++ private void growTree(ServerLevel world, RandomSource random, BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { ++ // Purpur end + TREE_GROWER.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java b/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java +index 3d2b34c5a7c9b00c1164b4f89c2cbff81fc460eb..b5505e926e5cdb447de68e8eb8e46c97eb988e27 100644 +--- a/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java +@@ -35,6 +35,7 @@ public class BaseCoralPlantTypeBlock extends Block implements SimpleWaterloggedB + } + + protected static boolean scanForWater(BlockState state, BlockGetter world, BlockPos pos) { ++ if (!((net.minecraft.world.level.LevelAccessor) world).getMinecraftWorld().purpurConfig.coralDieOutsideWater) return true; // Purpur + if (state.getValue(WATERLOGGED)) { + return true; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/BedBlock.java b/src/main/java/net/minecraft/world/level/block/BedBlock.java +index 64e68bf6decc765274caaabfd34a5b2d7d82434c..fd83291fc9137527513c492c9e3c670ed5e09236 100644 +--- a/src/main/java/net/minecraft/world/level/block/BedBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java +@@ -98,7 +98,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + Vec3 vec3d = pos.getCenter(); + +- world.explode((Entity) null, DamageSource.badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // Paper - exploded block state ++ if (world.purpurConfig.bedExplode) world.explode((Entity) null, DamageSource.badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, (float) world.purpurConfig.bedExplosionPower, world.purpurConfig.bedExplosionFire, Level.ExplosionInteraction.BLOCK); // Paper - exploded block state // Purpur + return InteractionResult.SUCCESS; + } else if ((Boolean) state.getValue(BedBlock.OCCUPIED)) { + if (!this.kickVillagerOutOfBed(world, pos)) { +@@ -150,7 +150,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + Vec3 vec3d = blockposition.getCenter(); + +- world.explode((Entity) null, DamageSource.badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // Paper - exploded block state ++ if (world.purpurConfig.bedExplode) world.explode((Entity) null, DamageSource.badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, (float) world.purpurConfig.bedExplosionPower, world.purpurConfig.bedExplosionFire, world.purpurConfig.bedExplosionEffect); // Paper - exploded block state // Purpur + return InteractionResult.SUCCESS; + } + } +@@ -175,7 +175,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- super.fallOn(world, state, pos, entity, fallDistance * 0.5F); ++ super.fallOn(world, state, pos, entity, fallDistance); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java b/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java +index 8537581e7ca1f4efb492a2e734f46f947f36cffa..5f89229ff68d923c5cdee40e72e379ba7024f961 100644 +--- a/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java +@@ -236,7 +236,7 @@ public class BigDripleafBlock extends HorizontalDirectionalBlock implements Bone + BigDripleafBlock.playTiltSound(world, blockposition, soundeffect); + } + +- int i = BigDripleafBlock.DELAY_UNTIL_NEXT_TILT_STATE.getInt(tilt); ++ int i = world.purpurConfig.bigDripleafTiltDelay.getOrDefault(tilt, -1); // Purpur + + if (i != -1) { + world.scheduleTick(blockposition, (Block) this, i); +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index 7b71073027f4cf79736546500ededdfbb83d968e..0c348b366623e350393f035d760adc6ee4142687 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -63,6 +63,13 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.slf4j.Logger; + ++// Purpur start ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.StringTag; ++import net.minecraft.world.Nameable; ++// Purpur end ++ + public class Block extends BlockBehaviour implements ItemLike { + + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -89,6 +96,10 @@ public class Block extends BlockBehaviour implements ItemLike { + public static final int UPDATE_LIMIT = 512; + protected final StateDefinition stateDefinition; + private BlockState defaultBlockState; ++ // Purpur start ++ public float fallDamageMultiplier = 1.0F; ++ public float fallDistanceMultiplier = 1.0F; ++ // Purpur end + // Paper start + public final boolean isDestroyable() { + return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || +@@ -325,7 +336,7 @@ public class Block extends BlockBehaviour implements ItemLike { + public static void dropResources(BlockState state, LevelAccessor world, BlockPos pos, @Nullable BlockEntity blockEntity) { + if (world instanceof ServerLevel) { + Block.getDrops(state, (ServerLevel) world, pos, blockEntity).forEach((itemstack) -> { +- Block.popResource((ServerLevel) world, pos, itemstack); ++ Block.popResource((ServerLevel) world, pos, applyDisplayNameAndLoreFromTile(itemstack, blockEntity)); // Purpur + }); + state.spawnAfterBreak((ServerLevel) world, pos, ItemStack.EMPTY, true); + } +@@ -341,7 +352,7 @@ public class Block extends BlockBehaviour implements ItemLike { + io.papermc.paper.event.block.BlockBreakBlockEvent event = new io.papermc.paper.event.block.BlockBreakBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(world, pos), org.bukkit.craftbukkit.block.CraftBlock.at(world, source), items); + event.callEvent(); + for (var drop : event.getDrops()) { +- popResource(world.getMinecraftWorld(), pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop)); ++ popResource(world.getMinecraftWorld(), pos, applyDisplayNameAndLoreFromTile(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop), blockEntity)); // Purpur + } + state.spawnAfterBreak(world.getMinecraftWorld(), pos, ItemStack.EMPTY, true); + } +@@ -352,13 +363,53 @@ public class Block extends BlockBehaviour implements ItemLike { + public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, Entity entity, ItemStack stack) { + if (world instanceof ServerLevel) { + Block.getDrops(state, (ServerLevel) world, pos, blockEntity, entity, stack).forEach((itemstack1) -> { +- Block.popResource(world, pos, itemstack1); ++ Block.popResource(world, pos, applyDisplayNameAndLoreFromTile(itemstack1, blockEntity)); // Purpur + }); + state.spawnAfterBreak((ServerLevel) world, pos, stack, true); + } + + } + ++ // Purpur start ++ private static ItemStack applyDisplayNameAndLoreFromTile(ItemStack stack, BlockEntity blockEntity) { ++ if (stack.getItem() instanceof BlockItem) { ++ if (blockEntity != null && blockEntity.getLevel() instanceof ServerLevel && blockEntity.getLevel().purpurConfig.persistentTileEntityDisplayNames) { ++ String name = blockEntity.getPersistentDisplayName(); ++ ListTag lore = blockEntity.getPersistentLore(); ++ if (blockEntity instanceof Nameable) { ++ Nameable namedTile = (Nameable) blockEntity; ++ if (namedTile.hasCustomName()) { ++ name = Component.Serializer.toJson(namedTile.getCustomName()); ++ } ++ } ++ ++ if (name != null || lore != null) { ++ CompoundTag display = stack.getTagElement("display"); ++ if (display == null) { ++ display = new CompoundTag(); ++ } ++ ++ if (name != null) { ++ display.put("Name", StringTag.valueOf(name)); ++ } ++ if (lore != null) { ++ display.put("Lore", lore); ++ } ++ ++ CompoundTag tag = stack.getTag(); ++ if (tag == null) { ++ tag = new CompoundTag(); ++ } ++ tag.put("display", display); ++ ++ stack.setTag(tag); ++ } ++ } ++ } ++ return stack; ++ } ++ // Purpur end ++ + public static void popResource(Level world, BlockPos pos, ItemStack stack) { + float f = EntityType.ITEM.getHeight() / 2.0F; + // Paper start - don't convert potentially massive numbers to floats +@@ -438,7 +489,17 @@ public class Block extends BlockBehaviour implements ItemLike { + Block.dropResources(state, world, pos, blockEntity, player, stack); + } + +- public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) {} ++ // Purpur start ++ @Nullable protected LivingEntity placer = null; ++ ++ public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) { ++ this.placer = placer; ++ } ++ ++ public void forgetPlacer() { ++ this.placer = null; ++ } ++ // Purpur end + + public boolean isPossibleToRespawnInThis() { + return !this.material.isSolid() && !this.material.isLiquid(); +@@ -457,7 +518,7 @@ public class Block extends BlockBehaviour implements ItemLike { + } + + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- entity.causeFallDamage(fallDistance, 1.0F, DamageSource.FALL); ++ entity.causeFallDamage(fallDistance * fallDistanceMultiplier, fallDamageMultiplier, DamageSource.FALL); // Purpur + } + + public void updateEntityAfterFallOn(BlockGetter world, Entity entity) { +diff --git a/src/main/java/net/minecraft/world/level/block/Blocks.java b/src/main/java/net/minecraft/world/level/block/Blocks.java +index 42f46d338886e2892ee4219d19be4dc97f61616f..c75404ddcb36105b84ff8c21545549bee5b4cb8e 100644 +--- a/src/main/java/net/minecraft/world/level/block/Blocks.java ++++ b/src/main/java/net/minecraft/world/level/block/Blocks.java +@@ -1063,8 +1063,8 @@ public class Blocks { + public static final Block CAVE_VINES = register("cave_vines", new CaveVinesBlock(BlockBehaviour.Properties.of(Material.PLANT).randomTicks().noCollission().lightLevel(CaveVines.emission(14)).instabreak().sound(SoundType.CAVE_VINES))); + public static final Block CAVE_VINES_PLANT = register("cave_vines_plant", new CaveVinesPlantBlock(BlockBehaviour.Properties.of(Material.PLANT).noCollission().lightLevel(CaveVines.emission(14)).instabreak().sound(SoundType.CAVE_VINES))); + public static final Block SPORE_BLOSSOM = register("spore_blossom", new SporeBlossomBlock(BlockBehaviour.Properties.of(Material.PLANT).instabreak().noCollission().sound(SoundType.SPORE_BLOSSOM))); +- public static final Block AZALEA = register("azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).instabreak().sound(SoundType.AZALEA).noOcclusion())); +- public static final Block FLOWERING_AZALEA = register("flowering_azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).instabreak().sound(SoundType.FLOWERING_AZALEA).noOcclusion())); ++ public static final Block AZALEA = register("azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).randomTicks().instabreak().sound(SoundType.AZALEA).noOcclusion())); // Purpur ++ public static final Block FLOWERING_AZALEA = register("flowering_azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).randomTicks().instabreak().sound(SoundType.FLOWERING_AZALEA).noOcclusion())); // Purpur + public static final Block MOSS_CARPET = register("moss_carpet", new CarpetBlock(BlockBehaviour.Properties.of(Material.PLANT, MaterialColor.COLOR_GREEN).strength(0.1F).sound(SoundType.MOSS_CARPET))); + public static final Block MOSS_BLOCK = register("moss_block", new MossBlock(BlockBehaviour.Properties.of(Material.MOSS, MaterialColor.COLOR_GREEN).strength(0.1F).sound(SoundType.MOSS))); + public static final Block BIG_DRIPLEAF = register("big_dripleaf", new BigDripleafBlock(BlockBehaviour.Properties.of(Material.PLANT).strength(0.1F).sound(SoundType.BIG_DRIPLEAF))); +@@ -1127,7 +1127,7 @@ public class Blocks { + } + + private static Boolean ocelotOrParrot(BlockState state, BlockGetter world, BlockPos pos, EntityType type) { +- return (boolean)type == EntityType.OCELOT || type == EntityType.PARROT; ++ return type == EntityType.OCELOT || type == EntityType.PARROT; // Purpur - decompile error + } + + private static BedBlock bed(DyeColor color) { +diff --git a/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java b/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java +index bedccb8717d08d5a60058445b04ddff149e7d36c..5293ffca3da94c9c485a87d1232b6a902fcafd6a 100644 +--- a/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java +@@ -53,4 +53,14 @@ public class BuddingAmethystBlock extends AmethystBlock { + public static boolean canClusterGrowAtState(BlockState state) { + return state.isAir() || state.is(Blocks.WATER) && state.getFluidState().getAmount() == 8; + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(net.minecraft.world.level.Level level, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, net.minecraft.world.level.block.entity.BlockEntity blockEntity, net.minecraft.world.item.ItemStack stack) { ++ if (level.purpurConfig.buddingAmethystSilkTouch && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH, stack) > 0) { ++ popResource(level, pos, net.minecraft.world.item.Items.BUDDING_AMETHYST.getDefaultInstance()); ++ } ++ super.playerDestroy(level, player, pos, state, blockEntity, stack); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java +index 03fde6e47c4a347c62fe9b4a3351769aedf874f6..ca906b0250e5332f7ececf1419ca6d2c1d385adc 100644 +--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java +@@ -48,4 +48,24 @@ public class BushBlock extends Block { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return type == PathComputationType.AIR && !this.hasCollision ? true : super.isPathfindable(state, world, pos, type); + } ++ ++ // Purpur start ++ public void playerDestroyAndReplant(net.minecraft.world.level.Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, net.minecraft.world.item.ItemStack itemInHand, net.minecraft.world.level.ItemLike itemToReplant) { ++ player.awardStat(net.minecraft.stats.Stats.BLOCK_MINED.get(this)); ++ player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); ++ java.util.List dropList = Block.getDrops(state, (net.minecraft.server.level.ServerLevel) world, pos, blockEntity, player, itemInHand); ++ ++ boolean planted = false; ++ for (net.minecraft.world.item.ItemStack itemToDrop : dropList) { ++ if (!planted && itemToDrop.getItem() == itemToReplant) { ++ world.setBlock(pos, defaultBlockState(), 3); ++ itemToDrop.setCount(itemToDrop.getCount() - 1); ++ planted = true; ++ } ++ Block.popResource(world, pos, itemToDrop); ++ } ++ ++ state.spawnAfterBreak((net.minecraft.server.level.ServerLevel) world, pos, itemInHand, true); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +index 1ec242205b82a5a1f10deb2312795cc5dc157a76..44abdcc5b71c289601f412a20ee50ad4388a7a74 100644 +--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +@@ -23,7 +23,7 @@ import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit + +-public class CactusBlock extends Block { ++public class CactusBlock extends Block implements BonemealableBlock { // Purpur + + public static final IntegerProperty AGE = BlockStateProperties.AGE_15; + public static final int MAX_AGE = 15; +@@ -110,7 +110,7 @@ public class CactusBlock extends Block { + BlockState iblockdata2 = world.getBlockState(pos.relative(enumdirection)); + + material = iblockdata2.getMaterial(); +- } while (!material.isSolid() && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); ++ } while ((!world.getWorldBorder().world.purpurConfig.cactusBreaksFromSolidNeighbors || !material.isSolid()) && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); // Purpur + + return false; + } +@@ -132,4 +132,34 @@ public class CactusBlock extends Block { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state, boolean isClient) { ++ if (!((Level) world).purpurConfig.cactusAffectedByBonemeal || !world.isEmptyBlock(pos.above())) return false; ++ ++ int cactusHeight = 0; ++ while (world.getBlockState(pos.below(cactusHeight)).is(this)) { ++ cactusHeight++; ++ } ++ ++ return cactusHeight < ((Level) world).paperConfig().maxGrowthHeight.cactus; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int cactusHeight = 0; ++ while (world.getBlockState(pos.below(cactusHeight)).is(this)) { ++ cactusHeight++; ++ } ++ for (int i = 0; i <= world.paperConfig().maxGrowthHeight.cactus - cactusHeight; i++) { ++ world.setBlockAndUpdate(pos.above(i), state.setValue(CactusBlock.AGE, 0)); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +index a4c44cb59dee29cf227dbb51bfc1576d89dfb2e3..551bacade8642e6aad17120d8a901bcc293f2eb2 100644 +--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +@@ -123,7 +123,7 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB + BlockPos blockposition = ctx.getClickedPos(); + boolean flag = world.getFluidState(blockposition).getType() == Fluids.WATER; + +- return (BlockState) ((BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(CampfireBlock.WATERLOGGED, flag)).setValue(CampfireBlock.SIGNAL_FIRE, this.isSmokeSource(world.getBlockState(blockposition.below())))).setValue(CampfireBlock.LIT, !flag)).setValue(CampfireBlock.FACING, ctx.getHorizontalDirection()); ++ return (BlockState) ((BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(CampfireBlock.WATERLOGGED, flag)).setValue(CampfireBlock.SIGNAL_FIRE, this.isSmokeSource(world.getBlockState(blockposition.below())))).setValue(CampfireBlock.LIT, world.purpurConfig.campFireLitWhenPlaced ? !flag : world.purpurConfig.campFireLitWhenPlaced)).setValue(CampfireBlock.FACING, ctx.getHorizontalDirection()); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +index f77dd9f9dc89d880386cc2da398cd7ec9c768c43..26c3aa77576a0c3b3df21f1b12189e6a42bdc0a7 100644 +--- a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +@@ -68,7 +68,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock implements We + SnowGolem entitysnowman = (SnowGolem) EntityType.SNOW_GOLEM.create(world); + + if (entitysnowman != null) { +- CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection, entitysnowman, shapedetector_shapedetectorcollection.getBlock(0, 2, 0).getPos()); ++ CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection, entitysnowman, shapedetector_shapedetectorcollection.getBlock(0, 2, 0).getPos(), this.placer); // Purpur + } + } else { + BlockPattern.BlockPatternMatch shapedetector_shapedetectorcollection1 = this.getOrCreateIronGolemFull().find(world, pos); +@@ -78,7 +78,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock implements We + + if (entityirongolem != null) { + entityirongolem.setPlayerCreated(true); +- CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection1, entityirongolem, shapedetector_shapedetectorcollection1.getBlock(1, 2, 0).getPos()); ++ CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection1, entityirongolem, shapedetector_shapedetectorcollection1.getBlock(1, 2, 0).getPos(), this.placer); // Purpur + } + } + } +@@ -86,6 +86,16 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock implements We + } + + private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos) { ++ // Purpur start ++ spawnGolemInWorld(world, patternResult, entity, pos, null); ++ } ++ private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos, net.minecraft.world.entity.LivingEntity placer) { ++ if (entity instanceof SnowGolem snowGolem) { ++ snowGolem.setSummoner(placer == null ? null : placer.getUUID()); ++ } else if (entity instanceof IronGolem ironGolem) { ++ ironGolem.setSummoner(placer == null ? null : placer.getUUID()); ++ } ++ // Purpur end + // clearPatternBlocks(world, shapedetector_shapedetectorcollection); // CraftBukkit - moved down + entity.moveTo((double) pos.getX() + 0.5D, (double) pos.getY() + 0.05D, (double) pos.getZ() + 0.5D, 0.0F, 0.0F); + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/world/level/block/CauldronBlock.java b/src/main/java/net/minecraft/world/level/block/CauldronBlock.java +index 2f85b893dd0abc39fcedec65acc89e1567faf6f0..3ee012a9ef8cada0b2203e53b2f731f60f697cb1 100644 +--- a/src/main/java/net/minecraft/world/level/block/CauldronBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CauldronBlock.java +@@ -29,7 +29,7 @@ public class CauldronBlock extends AbstractCauldronBlock { + } + + protected static boolean shouldHandlePrecipitation(Level world, Biome.Precipitation precipitation) { +- return precipitation == Biome.Precipitation.RAIN ? world.getRandom().nextFloat() < 0.05F : (precipitation == Biome.Precipitation.SNOW ? world.getRandom().nextFloat() < 0.1F : false); ++ return precipitation == Biome.Precipitation.RAIN ? world.getRandom().nextFloat() < world.purpurConfig.cauldronRainChance : (precipitation == Biome.Precipitation.SNOW ? world.getRandom().nextFloat() < world.purpurConfig.cauldronPowderSnowChance : false); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java b/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java +index fc76cd43655e0f4b8a8d87f90f0a48a8678ef16c..a5b27c064096c9572a8fe0dc40e68d0982507103 100644 +--- a/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java +@@ -89,4 +89,11 @@ public class CaveVinesBlock extends GrowingPlantHeadBlock implements Bonemealabl + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { + world.setBlock(pos, (BlockState) state.setValue(CaveVinesBlock.BERRIES, true), 2); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.caveVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/ChestBlock.java b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +index c6b57d45383441aa35510e759ce3cb82bc98f305..330ff3bc5fd8625e37b79e1204eddbe88de62c03 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +@@ -355,6 +355,7 @@ public class ChestBlock extends AbstractChestBlock implements + } + + private static boolean isBlockedChestByBlock(BlockGetter world, BlockPos pos) { ++ if (world instanceof Level && ((Level) world).purpurConfig.chestOpenWithBlockOnTop) return false; // Purpur + BlockPos blockposition1 = pos.above(); + + return world.getBlockState(blockposition1).isRedstoneConductor(world, blockposition1); +diff --git a/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java b/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java +index a6c25647fb37f59307de0d390f8e8cf55504d7d3..52aae8bd4023b2bb48f12983f54b20fa3c95d403 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java +@@ -21,6 +21,7 @@ public class ChorusPlantBlock extends PipeBlock { + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { ++ if (org.purpurmc.purpur.PurpurConfig.disableChorusPlantUpdates) return this.defaultBlockState(); // Purpur + return this.getStateForPlacement(ctx.getLevel(), ctx.getClickedPos()); + } + +@@ -36,6 +37,7 @@ public class ChorusPlantBlock extends PipeBlock { + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { ++ if (org.purpurmc.purpur.PurpurConfig.disableChorusPlantUpdates) return state; // Purpur + if (!state.canSurvive(world, pos)) { + world.scheduleTick(pos, this, 1); + return super.updateShape(state, direction, neighborState, world, pos, neighborPos); +diff --git a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +index f6268231e39f50bb6adedd85e3c18d746ae3792d..558563aed82adaa44d874a6cbb3e381819c2f638 100644 +--- a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +@@ -220,26 +220,28 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + ItemStack itemstack = player.getItemInHand(hand); + + if (i < 8 && ComposterBlock.COMPOSTABLES.containsKey(itemstack.getItem())) { +- if (i < 7 && !world.isClientSide) { +- // Paper start - EntityChangeBlockEvent +- double rand = world.getRandom().nextDouble(); +- BlockState dummyBlockState = ComposterBlock.addItem(state, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, pos, itemstack, rand); +- if (state != dummyBlockState && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, dummyBlockState).isCancelled()) { // if block state will change and event cancelled +- return InteractionResult.sidedSuccess(world.isClientSide); +- } +- BlockState iblockdata1 = ComposterBlock.addItem(state, world, pos, itemstack, player); +- if (iblockdata1 == null) { +- return InteractionResult.PASS; +- } +- // Paper end +- +- world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); +- player.awardStat(Stats.ITEM_USED.get(itemstack.getItem())); +- if (!player.getAbilities().instabuild) { +- itemstack.shrink(1); +- } ++ // Purpur start ++ BlockState newState = process(i, state, world, itemstack, pos, player); ++ if (newState == null) { ++ return InteractionResult.PASS; + } + ++ if (world.purpurConfig.composterBulkProcess && player.isShiftKeyDown() && newState != state) { ++ BlockState oldState; ++ int oldCount, newCount, oldLevel, newLevel; ++ do { ++ oldState = newState; ++ oldCount = itemstack.getCount(); ++ oldLevel = oldState.getValue(ComposterBlock.LEVEL); ++ newState = process(oldLevel, oldState, world, itemstack, pos, player); ++ if (newState == null) { ++ return InteractionResult.PASS; ++ } ++ newCount = itemstack.getCount(); ++ newLevel = newState.getValue(ComposterBlock.LEVEL); ++ } while (newCount > 0 && (newCount != oldCount || newLevel != oldLevel || newState != oldState)); ++ } ++ // Purpur end + return InteractionResult.sidedSuccess(world.isClientSide); + } else if (i == 8) { + ComposterBlock.extractProduce(state, world, pos, (Entity) null); // CraftBukkit - no event for players +@@ -249,6 +251,32 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + ++ // Purpur start ++ private static BlockState process(int level, BlockState state, Level world, ItemStack itemstack, BlockPos pos, Player player) { ++ if (level < 7 && !world.isClientSide) { ++ // Paper start - EntityChangeBlockEvent ++ double rand = world.getRandom().nextDouble(); ++ BlockState dummyBlockState = ComposterBlock.addItem(state, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, pos, itemstack, rand); ++ if (state != dummyBlockState && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, dummyBlockState).isCancelled()) { // if block state will change and event cancelled ++ return state; ++ } ++ BlockState iblockdata1 = ComposterBlock.addItem(state, world, pos, itemstack, player); ++ if (iblockdata1 == null) { ++ return iblockdata1; ++ } ++ // Paper end ++ ++ world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); ++ player.awardStat(Stats.ITEM_USED.get(itemstack.getItem())); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return dummyBlockState; ++ } ++ return state; ++ } ++ // Purpur end ++ + public static BlockState insertItem(BlockState iblockdata, ServerLevel worldserver, ItemStack itemstack, BlockPos blockposition, Entity entity) { // CraftBukkit + int i = (Integer) iblockdata.getValue(ComposterBlock.LEVEL); + +diff --git a/src/main/java/net/minecraft/world/level/block/CoralBlock.java b/src/main/java/net/minecraft/world/level/block/CoralBlock.java +index 88faea00be60a519f56f975a5311df5e1eb3e6b8..cbb726ac367be81e27d3a86643baf7c4f0746edf 100644 +--- a/src/main/java/net/minecraft/world/level/block/CoralBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CoralBlock.java +@@ -45,6 +45,7 @@ public class CoralBlock extends Block { + } + + protected boolean scanForWater(BlockGetter world, BlockPos pos) { ++ if (!((net.minecraft.world.level.LevelAccessor) world).getMinecraftWorld().purpurConfig.coralDieOutsideWater) return true; // Purpur + Direction[] aenumdirection = Direction.values(); + int i = aenumdirection.length; + +diff --git a/src/main/java/net/minecraft/world/level/block/CropBlock.java b/src/main/java/net/minecraft/world/level/block/CropBlock.java +index 519d02a2009c4f09c9e8be7196a701f0f042012d..74620e6aee75334498d903c616c090caa615f0b4 100644 +--- a/src/main/java/net/minecraft/world/level/block/CropBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CropBlock.java +@@ -164,7 +164,7 @@ public class CropBlock extends BushBlock implements BonemealableBlock { + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper +- if (entity instanceof Ravager && !CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // CraftBukkit ++ if (entity instanceof Ravager && world.purpurConfig.ravagerGriefableBlocks.contains(world.getBlockState(pos).getBlock()) && !CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), (!world.purpurConfig.ravagerBypassMobGriefing && !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))).isCancelled()) { // CraftBukkit // Purpur + world.destroyBlock(pos, true, entity); + } + +@@ -199,4 +199,15 @@ public class CropBlock extends BushBlock implements BonemealableBlock { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(CropBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, ItemStack itemInHand) { ++ if (world.purpurConfig.hoeReplantsCrops && itemInHand.getItem() instanceof net.minecraft.world.item.HoeItem) { ++ super.playerDestroyAndReplant(world, player, pos, state, blockEntity, itemInHand, getBaseSeedId()); ++ } else { ++ super.playerDestroy(world, player, pos, state, blockEntity, itemInHand); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/DoorBlock.java b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +index fc4793fefe52adfeb0272bf5324c32c1c3946416..58e5acea025287214757cba632e1268e418d7dfa 100644 +--- a/src/main/java/net/minecraft/world/level/block/DoorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +@@ -167,6 +167,7 @@ public class DoorBlock extends Block { + public InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (this.material == Material.METAL) { + return InteractionResult.PASS; ++ } else if (requiresRedstone(world, state, pos)) { return InteractionResult.CONSUME; // Purpur + } else { + state = (BlockState) state.cycle(DoorBlock.OPEN); + world.setBlock(pos, state, 10); +@@ -262,4 +263,18 @@ public class DoorBlock extends Block { + public static boolean isWoodenDoor(BlockState state) { + return state.getBlock() instanceof DoorBlock && (state.getMaterial() == Material.WOOD || state.getMaterial() == Material.NETHER_WOOD); + } ++ ++ // Purpur start ++ public static boolean requiresRedstone(Level level, BlockState state, BlockPos pos) { ++ if (level.purpurConfig.doorRequiresRedstone.contains(state.getBlock())) { ++ // force update client ++ BlockPos otherPos = pos.relative(state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN); ++ BlockState otherState = level.getBlockState(otherPos); ++ level.sendBlockUpdated(pos, state, state, 3); ++ level.sendBlockUpdated(otherPos, otherState, otherState, 3); ++ return true; ++ } ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java b/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java +index 7e1edcc7b9f170b7c649437c2f0dd78c0bab9be4..5f8ac1fdac2c334951261f2b9702f5e711743c88 100644 +--- a/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java +@@ -42,8 +42,8 @@ public class DragonEggBlock extends FallingBlock { + } + + private void teleport(BlockState state, Level world, BlockPos pos) { ++ if (!world.purpurConfig.dragonEggTeleport) return; // Purpur + WorldBorder worldborder = world.getWorldBorder(); +- + for (int i = 0; i < 1000; ++i) { + BlockPos blockposition1 = pos.offset(world.random.nextInt(16) - world.random.nextInt(16), world.random.nextInt(8) - world.random.nextInt(8), world.random.nextInt(16) - world.random.nextInt(16)); + +diff --git a/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java b/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java +index f4ee3ce287528337a0f9a3b612c157254f895a58..c4a91d7f1320027ee6a2b364303c01ebbacde584 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java +@@ -28,6 +28,8 @@ import net.minecraft.world.level.pathfinder.PathComputationType; + import net.minecraft.world.phys.BlockHitResult; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; ++import net.minecraft.world.Containers; // Purpur ++import net.minecraft.world.item.Items; // Purpur + + public class EnchantmentTableBlock extends BaseEntityBlock { + protected static final VoxelShape SHAPE = Block.box(0.0D, 0.0D, 0.0D, 16.0D, 12.0D, 16.0D); +@@ -40,6 +42,10 @@ public class EnchantmentTableBlock extends BaseEntityBlock { + } + + public static boolean isValidBookShelf(Level world, BlockPos tablePos, BlockPos bookshelfOffset) { ++ // Purpur Start ++ if(org.purpurmc.purpur.PurpurConfig.allowTransparentBlocksInEnchantmentBox){ ++ return world.getBlockState(tablePos.offset(bookshelfOffset)).is(Blocks.BOOKSHELF) && !world.getBlockState(tablePos.offset(bookshelfOffset.getX() / 2, bookshelfOffset.getY(), bookshelfOffset.getZ() / 2)).isSuffocating(world, bookshelfOffset); ++ } // Purpur end + return world.getBlockState(tablePos.offset(bookshelfOffset)).is(Blocks.BOOKSHELF) && world.isEmptyBlock(tablePos.offset(bookshelfOffset.getX() / 2, bookshelfOffset.getY(), bookshelfOffset.getZ() / 2)); + } + +@@ -120,4 +126,18 @@ public class EnchantmentTableBlock extends BaseEntityBlock { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ ++ if (level.purpurConfig.enchantmentTableLapisPersists && blockEntity instanceof EnchantmentTableBlockEntity enchantmentTable) { ++ Containers.dropItemStack(level, pos.getX(), pos.getY(), pos.getZ(), new ItemStack(Items.LAPIS_LAZULI, enchantmentTable.getLapis())); ++ level.updateNeighbourForOutputSignal(pos, this); ++ } ++ ++ super.onRemove(state, level, pos, newState, moved); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +index 15c5cccfe02c924c02f605eb47dd0b420b189891..e7658fa9806701505e15cbf1d28ea3bd2ed6f113 100644 +--- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +@@ -45,7 +45,15 @@ public class EndPortalBlock extends BaseEntityBlock { + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper +- if (world instanceof ServerLevel && !entity.isPassenger() && !entity.isVehicle() && entity.canChangeDimensions() && Shapes.joinIsNotEmpty(Shapes.create(entity.getBoundingBox().move((double) (-pos.getX()), (double) (-pos.getY()), (double) (-pos.getZ()))), state.getShape(world, pos), BooleanOp.AND)) { ++ // Purpur start ++ if (world instanceof ServerLevel && /*!entity.isPassenger() && !entity.isVehicle() &&*/ entity.canChangeDimensions() && Shapes.joinIsNotEmpty(Shapes.create(entity.getBoundingBox().move((double) (-pos.getX()), (double) (-pos.getY()), (double) (-pos.getZ()))), state.getShape(world, pos), BooleanOp.AND)) { ++ if (entity.isPassenger() || entity.isVehicle()) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, PlayerTeleportEvent.TeleportCause.END_PORTAL).callEvent()) { ++ this.entityInside(state, world, pos, entity); ++ } ++ return; ++ } ++ // Purpur end + ResourceKey resourcekey = world.getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END; // CraftBukkit - SPIGOT-6152: send back to main overworld in custom ends + ServerLevel worldserver = ((ServerLevel) world).getServer().getLevel(resourcekey); + +@@ -53,6 +61,22 @@ public class EndPortalBlock extends BaseEntityBlock { + // return; // CraftBukkit - always fire event in case plugins wish to change it + } + ++ // Purpur start ++ if (!world.purpurConfig.endPortalSafeTeleporting) { ++ // CraftBukkit start - Entity in portal ++ EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ())); ++ world.getCraftServer().getPluginManager().callEvent(event); ++ ++ if (entity instanceof ServerPlayer) { ++ ((ServerPlayer) entity).changeDimension(worldserver, PlayerTeleportEvent.TeleportCause.END_PORTAL); ++ return; ++ } ++ // CraftBukkit end ++ entity.changeDimension(worldserver); ++ return; ++ } ++ // Purpur end ++ + // Paper start - move all of this logic into portal tick + entity.portalWorld = ((ServerLevel)world); + entity.portalBlock = pos.immutable(); +diff --git a/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java b/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java +index 7385e91f32f070e86a4e0fd3d214f55d832c7979..c3b78dd2d06be7d64920c6bcffcd16c82caa52b4 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java +@@ -85,6 +85,27 @@ public class EnderChestBlock extends AbstractChestBlock i + EnderChestBlockEntity enderChestBlockEntity = (EnderChestBlockEntity)blockEntity; + playerEnderChestContainer.setActiveChest(enderChestBlockEntity); + player.openMenu(new SimpleMenuProvider((syncId, inventory, playerx) -> { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.enderChestSixRows) { ++ if (org.purpurmc.purpur.PurpurConfig.enderChestPermissionRows) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity bukkitPlayer = player.getBukkitEntity(); ++ if (bukkitPlayer.hasPermission("purpur.enderchest.rows.six")) { ++ return ChestMenu.sixRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.five")) { ++ return ChestMenu.fiveRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.four")) { ++ return ChestMenu.fourRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.three")) { ++ return ChestMenu.threeRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.two")) { ++ return ChestMenu.twoRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.one")) { ++ return ChestMenu.oneRow(syncId, inventory, playerEnderChestContainer); ++ } ++ } ++ return ChestMenu.sixRows(syncId, inventory, playerEnderChestContainer); ++ } ++ // Purpur end + return ChestMenu.threeRows(syncId, inventory, playerEnderChestContainer); + }, CONTAINER_TITLE)); + player.awardStat(Stats.OPEN_ENDERCHEST); +diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +index d089887030ac7c7a79abca97134ba9291e244059..7068cb39ab264fa0c65febff01236b8de564b883 100644 +--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +@@ -98,7 +98,7 @@ public class FarmBlock extends Block { + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { + super.fallOn(world, state, pos, entity, fallDistance); // CraftBukkit - moved here as game rules / events shouldn't affect fall damage. +- if (!world.isClientSide && world.random.nextFloat() < fallDistance - 0.5F && entity instanceof LivingEntity && (entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { ++ if (!world.isClientSide && (world.purpurConfig.farmlandTrampleHeight >= 0D ? fallDistance >= world.purpurConfig.farmlandTrampleHeight : world.random.nextFloat() < fallDistance - 0.5F) && entity instanceof LivingEntity && (entity instanceof Player || world.purpurConfig.farmlandBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { // Purpur + // CraftBukkit start - Interact soil + org.bukkit.event.Cancellable cancellable; + if (entity instanceof Player) { +@@ -112,6 +112,22 @@ public class FarmBlock extends Block { + return; + } + ++ // Purpur start ++ if (world.purpurConfig.farmlandTramplingDisabled) return; ++ if (world.purpurConfig.farmlandTramplingOnlyPlayers && !(entity instanceof Player)) return; ++ if (world.purpurConfig.farmlandAlpha) { ++ Block block = world.getBlockState(pos.below()).getBlock(); ++ if (block instanceof FenceBlock || block instanceof WallBlock) { ++ return; ++ } ++ } ++ if (world.purpurConfig.farmlandTramplingFeatherFalling) { ++ Iterator armor = entity.getArmorSlots().iterator(); ++ if (armor.hasNext() && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.FALL_PROTECTION, armor.next()) >= (int) entity.fallDistance) { ++ return; ++ } ++ } ++ // Purpur end + if (CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { + return; + } +@@ -158,7 +174,7 @@ public class FarmBlock extends Block { + } + } + +- return false; ++ return ((ServerLevel) world).purpurConfig.farmlandGetsMoistFromBelow && world.getFluidState(pos.relative(Direction.DOWN)).is(FluidTags.WATER); // Purpur; + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +index 3a1aa4e2405090ccebefb7f5944f36462929e221..f3cf9f06de40054720d1847c1869a9d82592134d 100644 +--- a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +@@ -30,12 +30,12 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + + @Override + public BlockState getStateForPlacement(LevelAccessor world) { +- return (BlockState) this.defaultBlockState().setValue(GrowingPlantHeadBlock.AGE, world.getRandom().nextInt(25)); ++ return (BlockState) this.defaultBlockState().setValue(GrowingPlantHeadBlock.AGE, world.getRandom().nextInt(getMaxGrowthAge())); // Purpur + } + + @Override + public boolean isRandomlyTicking(BlockState state) { +- return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25; ++ return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) < getMaxGrowthAge(); // Purpur + } + + @Override +@@ -51,7 +51,7 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } else { + modifier = world.spigotConfig.caveVinesModifier; + } +- if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25 && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution ++ if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < getMaxGrowthAge() && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution // Purpur + // Spigot end + BlockPos blockposition1 = pos.relative(this.growthDirection); + +@@ -73,11 +73,11 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } + + public BlockState getMaxAgeState(BlockState state) { +- return (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, 25); ++ return (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, getMaxGrowthAge()); // Purpur + } + + public boolean isMaxAge(BlockState state) { +- return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) == 25; ++ return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) >= getMaxGrowthAge(); // Purpur + } + + protected BlockState updateBodyAfterConvertedFromHead(BlockState from, BlockState to) { +@@ -119,13 +119,13 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + @Override + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { + BlockPos blockposition1 = pos.relative(this.growthDirection); +- int i = Math.min((Integer) state.getValue(GrowingPlantHeadBlock.AGE) + 1, 25); ++ int i = Math.min((Integer) state.getValue(GrowingPlantHeadBlock.AGE) + 1, getMaxGrowthAge()); // Purpur + int j = this.getBlocksToGrowWhenBonemealed(random); + + for (int k = 0; k < j && this.canGrowInto(world.getBlockState(blockposition1)); ++k) { + world.setBlockAndUpdate(blockposition1, (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, i)); + blockposition1 = blockposition1.relative(this.growthDirection); +- i = Math.min(i + 1, 25); ++ i = Math.min(i + 1, getMaxGrowthAge()); // Purpur + } + + } +@@ -138,4 +138,6 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + protected GrowingPlantHeadBlock getHeadBlock() { + return this; + } ++ ++ public abstract int getMaxGrowthAge(); // Purpur + } +diff --git a/src/main/java/net/minecraft/world/level/block/HayBlock.java b/src/main/java/net/minecraft/world/level/block/HayBlock.java +index c316fb1d3081c1fbf4602dd72e96e57491bc8efd..3b054f2bda6fae31e8ab7bce088e228f800b0d43 100644 +--- a/src/main/java/net/minecraft/world/level/block/HayBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HayBlock.java +@@ -16,6 +16,6 @@ public class HayBlock extends RotatedPillarBlock { + + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- entity.causeFallDamage(fallDistance, 0.2F, DamageSource.FALL); ++ super.fallOn(world, state, pos, entity, fallDistance); // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java b/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java +index 3c6d97b51c6fec130b80e5965afa2c49d48843c9..b456cb8efd8f0be8a6860c82462ce9bdde3a8383 100644 +--- a/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java +@@ -22,29 +22,65 @@ public class HugeMushroomBlock extends Block { + + public HugeMushroomBlock(BlockBehaviour.Properties settings) { + super(settings); +- this.registerDefaultState(this.stateDefinition.any().setValue(NORTH, Boolean.valueOf(true)).setValue(EAST, Boolean.valueOf(true)).setValue(SOUTH, Boolean.valueOf(true)).setValue(WEST, Boolean.valueOf(true)).setValue(UP, Boolean.valueOf(true)).setValue(DOWN, Boolean.valueOf(true))); ++ // Purpur start ++ this.registerDefaultState(this.stateDefinition.any() ++ .setValue(NORTH, true) ++ .setValue(EAST, true) ++ .setValue(SOUTH, true) ++ .setValue(WEST, true) ++ .setValue(UP, true) ++ .setValue(DOWN, true)); ++ // Purpur end + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return this.defaultBlockState(); // Purpur + BlockGetter blockGetter = ctx.getLevel(); + BlockPos blockPos = ctx.getClickedPos(); +- return this.defaultBlockState().setValue(DOWN, Boolean.valueOf(!blockGetter.getBlockState(blockPos.below()).is(this))).setValue(UP, Boolean.valueOf(!blockGetter.getBlockState(blockPos.above()).is(this))).setValue(NORTH, Boolean.valueOf(!blockGetter.getBlockState(blockPos.north()).is(this))).setValue(EAST, Boolean.valueOf(!blockGetter.getBlockState(blockPos.east()).is(this))).setValue(SOUTH, Boolean.valueOf(!blockGetter.getBlockState(blockPos.south()).is(this))).setValue(WEST, Boolean.valueOf(!blockGetter.getBlockState(blockPos.west()).is(this))); ++ // Purpur start ++ return this.defaultBlockState() ++ .setValue(DOWN, this != blockGetter.getBlockStateIfLoaded(blockPos.below()).getBlock()) ++ .setValue(UP, this != blockGetter.getBlockStateIfLoaded(blockPos.above()).getBlock()) ++ .setValue(NORTH, this != blockGetter.getBlockStateIfLoaded(blockPos.north()).getBlock()) ++ .setValue(EAST, this != blockGetter.getBlockStateIfLoaded(blockPos.east()).getBlock()) ++ .setValue(SOUTH, this != blockGetter.getBlockStateIfLoaded(blockPos.south()).getBlock()) ++ .setValue(WEST, this != blockGetter.getBlockStateIfLoaded(blockPos.west()).getBlock()); ++ // Purpur end + } + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return state; // Purpur + return neighborState.is(this) ? state.setValue(PROPERTY_BY_DIRECTION.get(direction), Boolean.valueOf(false)) : super.updateShape(state, direction, neighborState, world, pos, neighborPos); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { +- return state.setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.NORTH)), state.getValue(NORTH)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.SOUTH)), state.getValue(SOUTH)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.EAST)), state.getValue(EAST)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.WEST)), state.getValue(WEST)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.UP)), state.getValue(UP)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return state; ++ return state ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.NORTH)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.SOUTH)), state.getValue(SOUTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.EAST)), state.getValue(EAST)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.WEST)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.UP)), state.getValue(UP)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur end + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { +- return state.setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.NORTH)), state.getValue(NORTH)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.SOUTH)), state.getValue(SOUTH)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.EAST)), state.getValue(EAST)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.WEST)), state.getValue(WEST)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.UP)), state.getValue(UP)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return state; ++ return state ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.NORTH)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.SOUTH)), state.getValue(SOUTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.EAST)), state.getValue(EAST)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.WEST)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.UP)), state.getValue(UP)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/IceBlock.java b/src/main/java/net/minecraft/world/level/block/IceBlock.java +index 64206d94a5bf210116d208f9678618b905a61428..d9dbd36be7693cb3f3eafe97e80efc169e3cef65 100644 +--- a/src/main/java/net/minecraft/world/level/block/IceBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/IceBlock.java +@@ -31,7 +31,7 @@ public class IceBlock extends HalfTransparentBlock { + public void afterDestroy(Level world, BlockPos pos, ItemStack stack) { + // Paper end + if (EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, stack) == 0) { +- if (world.dimensionType().ultraWarm()) { ++ if (world.isNether() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) { // Purpur + world.removeBlock(pos, false); + return; + } +@@ -59,7 +59,7 @@ public class IceBlock extends HalfTransparentBlock { + return; + } + // CraftBukkit end +- if (world.dimensionType().ultraWarm()) { ++ if (world.isNether() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) { // Purpur + world.removeBlock(pos, false); + } else { + world.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState()); +diff --git a/src/main/java/net/minecraft/world/level/block/KelpBlock.java b/src/main/java/net/minecraft/world/level/block/KelpBlock.java +index bc66fa91ec3e13431d5d9b6e17935cab73066be7..0f16b5ed2e249f3d8f583dc941e32066d354cf95 100644 +--- a/src/main/java/net/minecraft/world/level/block/KelpBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/KelpBlock.java +@@ -64,4 +64,11 @@ public class KelpBlock extends GrowingPlantHeadBlock implements LiquidBlockConta + public FluidState getFluidState(BlockState state) { + return Fluids.WATER.getSource(false); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.kelpMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java +index 43e8ef1d6a65d4fd3fe53a587639ffb814368217..9c22a730772f71b34c63d1e43d48943f71e9990b 100644 +--- a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java +@@ -105,7 +105,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { +- if (this.shouldSpreadLiquid(world, pos, state)) { ++ if (world.purpurConfig.tickFluids && this.shouldSpreadLiquid(world, pos, state)) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper + } + +@@ -129,7 +129,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { +- if (state.getFluidState().isSource() || neighborState.getFluidState().isSource()) { ++ if (world.getMinecraftWorld().purpurConfig.tickFluids && state.getFluidState().isSource() || neighborState.getFluidState().isSource()) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(world)); + } + +@@ -138,7 +138,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + public void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { +- if (this.shouldSpreadLiquid(world, pos, state)) { ++ if (world.purpurConfig.tickFluids && this.shouldSpreadLiquid(world, pos, state)) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper + } + +diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +index d3540a4daaa8021ae009bfd4d9ef4f1172ab4c56..2b250439f263f64db7920536ed6eaf6440644a11 100644 +--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +@@ -28,7 +28,7 @@ public class MagmaBlock extends Block { + + @Override + public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) { +- if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { ++ if ((!entity.isSteppingCarefully() || world.purpurConfig.magmaBlockDamageWhenSneaking) && entity instanceof LivingEntity && (world.purpurConfig.magmaBlockDamageWithFrostWalker || !EnchantmentHelper.hasFrostWalker((LivingEntity) entity))) { // Purpur + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit + entity.hurt(DamageSource.HOT_FLOOR, 1.0F); + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit +diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +index 192689be9dfc9373876921bd4da0715d58f9421c..307a05fa07bdfbc1586dde5f7672522f9f7dd9ca 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -52,7 +52,7 @@ public class NetherPortalBlock extends Block { + + @Override + public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { +- if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(2000) < world.getDifficulty().getId()) { // Spigot ++ if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(world.purpurConfig.piglinPortalSpawnModifier) < world.getDifficulty().getId()) { // Spigot // Purpur + while (world.getBlockState(pos).is((Block) this)) { + pos = pos.below(); + } +@@ -83,7 +83,15 @@ public class NetherPortalBlock extends Block { + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper +- if (!entity.isPassenger() && !entity.isVehicle() && entity.canChangeDimensions()) { ++ // Purpur start ++ if (/*!entity.isPassenger() && !entity.isVehicle() &&*/ entity.canChangeDimensions()) { ++ if (entity.isPassenger() || entity.isVehicle()) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.NETHER_PORTAL).callEvent()) { ++ this.entityInside(state, world, pos, entity); ++ } ++ return; ++ } ++ // Purpur end + // CraftBukkit start - Entity in portal + EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ())); + world.getCraftServer().getPluginManager().callEvent(event); +diff --git a/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java b/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java +index e55720c4d2fbdf6aae526910e87a67c29cf906fd..bf4485b4cad324d5aace657ebf284c4d97197f53 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java +@@ -14,7 +14,7 @@ import net.minecraft.world.level.block.state.properties.IntegerProperty; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class NetherWartBlock extends BushBlock { ++public class NetherWartBlock extends BushBlock implements BonemealableBlock { // Purpur + + public static final int MAX_AGE = 3; + public static final IntegerProperty AGE = BlockStateProperties.AGE_3; +@@ -60,4 +60,32 @@ public class NetherWartBlock extends BushBlock { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(NetherWartBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(net.minecraft.world.level.Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, ItemStack itemInHand) { ++ if (world.purpurConfig.hoeReplantsNetherWarts && itemInHand.getItem() instanceof net.minecraft.world.item.HoeItem) { ++ super.playerDestroyAndReplant(world, player, pos, state, blockEntity, itemInHand, Items.NETHER_WART); ++ } else { ++ super.playerDestroy(world, player, pos, state, blockEntity, itemInHand); ++ } ++ } ++ ++ @Override ++ public boolean isValidBonemealTarget(net.minecraft.world.level.LevelReader world, BlockPos pos, BlockState state, boolean isClient) { ++ return ((net.minecraft.world.level.Level) world).purpurConfig.netherWartAffectedByBonemeal && state.getValue(NetherWartBlock.AGE) < 3; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(net.minecraft.world.level.Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int i = Math.min(3, state.getValue(NetherWartBlock.AGE) + 1); ++ state = state.setValue(NetherWartBlock.AGE, i); ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos, state, 2); // CraftBukkit ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/NoteBlock.java b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +index df7965c86b9c9e89b07b76c75b638d391ea6cc34..5c183107df821b67e175bba81f4dbe13d1e316bf 100644 +--- a/src/main/java/net/minecraft/world/level/block/NoteBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +@@ -60,11 +60,13 @@ public class NoteBlock extends Block { + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { ++ if (org.purpurmc.purpur.PurpurConfig.disableNoteBlockUpdates) return this.defaultBlockState(); // Purpur + return this.setInstrument(ctx.getLevel(), ctx.getClickedPos(), this.defaultBlockState()); + } + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { ++ if (org.purpurmc.purpur.PurpurConfig.disableNoteBlockUpdates) return state; // Purpur + boolean flag = NoteBlock.isFeatureFlagEnabled(world) ? direction.getAxis() == Direction.Axis.Y : direction == Direction.DOWN; + + return flag ? this.setInstrument(world, pos, state) : super.updateShape(state, direction, neighborState, world, pos, neighborPos); +@@ -80,13 +82,14 @@ public class NoteBlock extends Block { + state = world.getBlockState(pos); // CraftBukkit - SPIGOT-5617: update in case changed in event + } + ++ if (!org.purpurmc.purpur.PurpurConfig.disableNoteBlockUpdates) // Purpur + world.setBlock(pos, (BlockState) state.setValue(NoteBlock.POWERED, flag1), 3); + } + + } + + private void playNote(@Nullable Entity entity, BlockState state, Level world, BlockPos pos) { +- if (!((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).requiresAirAbove() || world.getBlockState(pos.above()).isAir()) { ++ if (world.purpurConfig.noteBlockIgnoreAbove || !((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).requiresAirAbove() || world.getBlockState(pos.above()).isAir()) { // Purpur + // CraftBukkit start + // org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(world, pos, state.getValue(NoteBlock.INSTRUMENT), state.getValue(NoteBlock.NOTE)); + // if (event.isCancelled()) { +diff --git a/src/main/java/net/minecraft/world/level/block/ObserverBlock.java b/src/main/java/net/minecraft/world/level/block/ObserverBlock.java +index 7b45d6b9a005036ca5051d089a7be792eb87012f..8806c97ecc6bdd8a64c2d82bb2f58f46ac37c468 100644 +--- a/src/main/java/net/minecraft/world/level/block/ObserverBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ObserverBlock.java +@@ -64,6 +64,7 @@ public class ObserverBlock extends DirectionalBlock { + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(ObserverBlock.FACING) == direction && !(Boolean) state.getValue(ObserverBlock.POWERED)) { ++ if (!world.getMinecraftWorld().purpurConfig.disableObserverClocks || !(neighborState.getBlock() instanceof ObserverBlock) || neighborState.getValue(ObserverBlock.FACING).getOpposite() != direction) // Purpur + this.startSignal(world, pos); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +index 1d7149fb6baeaf045c3680b6dec293f074614612..8ba5f519fbc4ee81b36e9052274fc644c4787a3d 100644 +--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +@@ -188,7 +188,7 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + + @VisibleForTesting + public static void maybeTransferFluid(BlockState state, ServerLevel world, BlockPos pos, float dripChance) { +- if (dripChance <= 0.17578125F || dripChance <= 0.05859375F) { ++ if (dripChance <= world.purpurConfig.cauldronDripstoneWaterFillChance || dripChance <= world.purpurConfig.cauldronDripstoneLavaFillChance) { // Purpur + if (PointedDripstoneBlock.isStalactiteStartPos(state, world, pos)) { + Optional optional = PointedDripstoneBlock.getFluidAboveStalactite(world, pos, state); + +@@ -197,13 +197,13 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + float f1; + + if (fluidtype == Fluids.WATER) { +- f1 = 0.17578125F; ++ f1 = world.purpurConfig.cauldronDripstoneWaterFillChance; // Purpur + } else { + if (fluidtype != Fluids.LAVA) { + return; + } + +- f1 = 0.05859375F; ++ f1 = world.purpurConfig.cauldronDripstoneLavaFillChance; // Purpur + } + + if (dripChance < f1) { +diff --git a/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java b/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java +index 518d3832c36c9ecf1ed9267ffc1f926dc84b7989..af5933b886abf3fd17bfdb8c1cb1ea63f6f2a757 100644 +--- a/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java +@@ -72,7 +72,7 @@ public class PowderSnowBlock extends Block implements BucketPickup { + if (!world.isClientSide) { + // CraftBukkit start + if (entity.isOnFire() && entity.mayInteract(world, pos)) { +- if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !(world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player)).isCancelled()) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !((world.purpurConfig.powderSnowBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) || entity instanceof Player)).isCancelled()) { + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +index 7fddb6fa8fd30ef88346a59f7867aae792f13772..40893e71fe8447b695350273bef9623bd5accdcd 100644 +--- a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +@@ -23,7 +23,7 @@ public class PoweredRailBlock extends BaseRailBlock { + } + + protected boolean findPoweredRailSignal(Level world, BlockPos pos, BlockState state, boolean flag, int distance) { +- if (distance >= 8) { ++ if (distance >= world.purpurConfig.railActivationRange) { // Purpur + return false; + } else { + int j = pos.getX(); +diff --git a/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java b/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java +index 1b7140ffab0492ab130743a2d158b30efb2cfece..63b536555d97c158003722c21a25394ba50f3533 100644 +--- a/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java +@@ -126,7 +126,7 @@ public class RespawnAnchorBlock extends Block { + } + }; + Vec3 vec3 = explodedPos.getCenter(); +- world.explode((Entity)null, DamageSource.badRespawnPointExplosion(vec3, explodedBlockState), explosionDamageCalculator, vec3, 5.0F, true, Level.ExplosionInteraction.BLOCK); // Paper - exploded block state ++ if (world.purpurConfig.respawnAnchorExplode) world.explode((Entity)null, DamageSource.badRespawnPointExplosion(vec3, explodedBlockState), explosionDamageCalculator, vec3, (float) world.purpurConfig.respawnAnchorExplosionPower, world.purpurConfig.respawnAnchorExplosionFire, world.purpurConfig.respawnAnchorExplosionEffect); // Paper - exploded block state // Purpur + } + + public static boolean canSetSpawn(Level world) { +diff --git a/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java b/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java +index e0998215841e500e5982a242e9f4e646402e1521..11038ba560439dab04c54c31a32d63bed2b4698a 100644 +--- a/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java +@@ -130,7 +130,7 @@ public class SculkShriekerBlock extends BaseEntityBlock implements SimpleWaterlo + @Nullable + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { +- return (BlockState) this.defaultBlockState().setValue(SculkShriekerBlock.WATERLOGGED, ctx.getLevel().getFluidState(ctx.getClickedPos()).getType() == Fluids.WATER); ++ return (BlockState) this.defaultBlockState().setValue(SculkShriekerBlock.WATERLOGGED, ctx.getLevel().getFluidState(ctx.getClickedPos()).getType() == Fluids.WATER).setValue(SculkShriekerBlock.CAN_SUMMON, ctx.getLevel().purpurConfig.sculkShriekerCanSummonDefault); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java +index c89978ecbc5a13dda6f76ea6d1cc3056efc9a174..39868ad3ee4bb573a4dd562894d93f64be4ee5ac 100644 +--- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -138,7 +138,7 @@ public class ShulkerBoxBlock extends BaseEntityBlock { + public void playerWillDestroy(Level world, BlockPos pos, BlockState state, Player player) { + BlockEntity blockEntity = world.getBlockEntity(pos); + if (blockEntity instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity) { +- if (!world.isClientSide && player.isCreative() && !shulkerBoxBlockEntity.isEmpty()) { ++ if (world.purpurConfig.shulkerBoxAllowOversizedStacks || (!world.isClientSide && player.isCreative() && !shulkerBoxBlockEntity.isEmpty())) { // Purpur + ItemStack itemStack = getColoredItemStack(this.getColor()); + blockEntity.saveToItem(itemStack); + if (shulkerBoxBlockEntity.hasCustomName()) { +diff --git a/src/main/java/net/minecraft/world/level/block/SignBlock.java b/src/main/java/net/minecraft/world/level/block/SignBlock.java +index aface9a9697095a29edaf73c9cdabc2c1414b9d7..1a04d0a601b8e481dd6e2592b849b907a5b9f63f 100644 +--- a/src/main/java/net/minecraft/world/level/block/SignBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SignBlock.java +@@ -14,6 +14,7 @@ import net.minecraft.world.item.DyeItem; + import net.minecraft.world.item.Item; + import net.minecraft.world.item.ItemStack; + import net.minecraft.world.item.Items; ++import net.minecraft.world.item.SignItem; + import net.minecraft.world.level.BlockGetter; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelAccessor; +@@ -76,11 +77,11 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo + if (world.isClientSide) { + return bl4 ? InteractionResult.SUCCESS : InteractionResult.CONSUME; + } else { +- BlockEntity bl5 = world.getBlockEntity(pos); +- if (!(bl5 instanceof SignBlockEntity)) { ++ BlockEntity blockEntity = world.getBlockEntity(pos); // Purpur - decompile fix ++ if (!(blockEntity instanceof SignBlockEntity)) { // Purpur - decompile fix + return InteractionResult.PASS; + } else { +- SignBlockEntity signBlockEntity = (SignBlockEntity)bl5; ++ SignBlockEntity signBlockEntity = (SignBlockEntity)blockEntity; // Purpur - decompile fix + boolean bl5 = signBlockEntity.hasGlowingText(); + if ((!bl2 || !bl5) && (!bl3 || bl5)) { + if (bl4) { +@@ -108,6 +109,17 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo + } + } + ++ // Purpur start - right click to open sign editor ++ if (world.purpurConfig.signRightClickEdit && itemStack.getItem() instanceof SignItem && ++ !player.isCrouching() && player.getAbilities().mayBuild && ++ player.getBukkitEntity().hasPermission("purpur.sign.edit")) { ++ signBlockEntity.setEditable(true); ++ signBlockEntity.setAllowedPlayerEditor(player.getUUID()); ++ player.openTextEdit(signBlockEntity); ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end ++ + return signBlockEntity.executeClickCommands((ServerPlayer)player) ? InteractionResult.SUCCESS : InteractionResult.PASS; + } else { + return InteractionResult.PASS; +diff --git a/src/main/java/net/minecraft/world/level/block/SlabBlock.java b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +index 18b603d646081926343dea108b55d641df1c2c34..03ad3e45fc6d48091ac0c0ba5dc3d014b1d4ddfa 100644 +--- a/src/main/java/net/minecraft/world/level/block/SlabBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +@@ -130,4 +130,25 @@ public class SlabBlock extends Block implements SimpleWaterloggedBlock { + return false; + } + } ++ ++ // Purpur start ++ public boolean halfBreak(BlockState state, BlockPos pos, net.minecraft.server.level.ServerPlayer player) { ++ if (state.getValue(SlabBlock.TYPE) != SlabType.DOUBLE) { ++ return false; ++ } ++ net.minecraft.world.phys.HitResult result = player.getRayTrace(16, net.minecraft.world.level.ClipContext.Fluid.NONE); ++ if (result.getType() != net.minecraft.world.phys.HitResult.Type.BLOCK) { ++ return false; ++ } ++ double hitY = result.getLocation().y(); ++ int blockY = org.bukkit.util.NumberConversions.floor(hitY); ++ player.level.setBlock(pos, state.setValue(SlabBlock.TYPE, (hitY - blockY > 0.5 || blockY - pos.getY() == 1) ? SlabType.BOTTOM : SlabType.TOP), 3); ++ if (!player.getAbilities().instabuild) { ++ net.minecraft.world.entity.item.ItemEntity item = new net.minecraft.world.entity.item.ItemEntity(player.level, pos.getX(), pos.getY(), pos.getZ(), new ItemStack(asItem())); ++ item.setDefaultPickUpDelay(); ++ player.level.addFreshEntity(item); ++ } ++ return true; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java b/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java +index 14e00c7feb1c051d56a3d27cd00dcef072dd771a..4952fb1aaaafb55baa0fddb389f966a120a4786c 100644 +--- a/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java +@@ -81,6 +81,12 @@ public class SnowLayerBlock extends Block { + public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) { + BlockState iblockdata1 = world.getBlockState(pos.below()); + ++ // Purpur start ++ if (iblockdata1.is(Blocks.BLUE_ICE) && !world.getWorldBorder().world.purpurConfig.snowOnBlueIce) { ++ return false; ++ } ++ // Purpur end ++ + return iblockdata1.is(BlockTags.SNOW_LAYER_CANNOT_SURVIVE_ON) ? false : (iblockdata1.is(BlockTags.SNOW_LAYER_CAN_SURVIVE_ON) ? true : Block.isFaceFull(iblockdata1.getCollisionShape(world, pos.below()), Direction.UP) || iblockdata1.is((Block) this) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 8); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java b/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java +index 5af520104f25d597917e99ac09aa9d68c4864e44..c4e5ff55a629ec57889175a0082abf95b8183069 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java +@@ -40,6 +40,58 @@ public class SpawnerBlock extends BaseEntityBlock { + return createTickerHelper(type, BlockEntityType.MOB_SPAWNER, world.isClientSide ? SpawnerBlockEntity::clientTick : SpawnerBlockEntity::serverTick); + } + ++ // Purpur start ++ @Override ++ public void playerDestroy(Level level, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, BlockEntity blockEntity, ItemStack stack) { ++ if (level.purpurConfig.silkTouchEnabled && player.getBukkitEntity().hasPermission("purpur.drop.spawners") && isSilkTouch(level, stack)) { ++ Optional> type = net.minecraft.world.entity.EntityType.by(((SpawnerBlockEntity) blockEntity).getSpawner().nextSpawnData.getEntityToSpawn()); ++ ++ net.minecraft.world.entity.EntityType entityType = type.orElse(null); ++ final net.kyori.adventure.text.Component mobName = io.papermc.paper.adventure.PaperAdventure.asAdventure(entityType == null ? Component.empty() : entityType.getDescription()); ++ CompoundTag display = new CompoundTag(); ++ CompoundTag tag = new CompoundTag(); ++ ++ String name = level.purpurConfig.silkTouchSpawnerName; ++ if (name != null && !name.isEmpty() && !name.equals("Monster Spawner")) { ++ net.kyori.adventure.text.Component displayName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name, net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("mob", mobName)); ++ if (name.startsWith("")) { ++ displayName = displayName.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ display.put("Name", net.minecraft.nbt.StringTag.valueOf(io.papermc.paper.adventure.PaperAdventure.asJsonString(displayName, java.util.Locale.ROOT))); ++ tag.put("display", display); ++ } ++ ++ List lore = level.purpurConfig.silkTouchSpawnerLore; ++ if (lore != null && !lore.isEmpty()) { ++ net.minecraft.nbt.ListTag list = new net.minecraft.nbt.ListTag(); ++ for (String line : lore) { ++ net.kyori.adventure.text.Component lineComponent = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(line, net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("mob", mobName)); ++ if (line.startsWith("")) { ++ lineComponent = lineComponent.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ list.add(net.minecraft.nbt.StringTag.valueOf(io.papermc.paper.adventure.PaperAdventure.asJsonString(lineComponent, java.util.Locale.ROOT))); ++ } ++ display.put("Lore", list); ++ tag.put("display", display); ++ } ++ ++ ItemStack item = new ItemStack(Blocks.SPAWNER.asItem()); ++ if (entityType != null) { ++ tag.putString("Purpur.mob_type", entityType.getName()); ++ tag.putDouble("HideFlags", 32); // hides the "Interact with Spawn Egg" tooltip ++ item.setTag(tag); ++ } ++ ++ popResource(level, pos, item); ++ } ++ super.playerDestroy(level, player, pos, state, blockEntity, stack); ++ } ++ ++ private boolean isSilkTouch(Level level, ItemStack stack) { ++ return stack != null && level.purpurConfig.silkTouchTools.contains(stack.getItem()) && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH, stack) >= level.purpurConfig.minimumSilkTouchSpawnerRequire; ++ } ++ // Purpur end ++ + @Override + public void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack stack, boolean dropExperience) { + super.spawnAfterBreak(state, world, pos, stack, dropExperience); +@@ -48,6 +100,7 @@ public class SpawnerBlock extends BaseEntityBlock { + + @Override + public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) { ++ if (isSilkTouch(worldserver, itemstack)) return 0; // Purpur + if (flag) { + int i = 15 + worldserver.random.nextInt(15) + worldserver.random.nextInt(15); + +diff --git a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +index 7304b2659eb45bc4bc9fa7c43e6ca07221d0fc73..df04a571ebd3c04bc7b58c1ee5661a1f03c69d2f 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +@@ -73,16 +73,16 @@ public class SpongeBlock extends Block { + // CraftBukkit end + Material material = iblockdata.getMaterial(); + +- if (fluid.is(FluidTags.WATER)) { ++ if (fluid.is(FluidTags.WATER) || (world.purpurConfig.spongeAbsorbsLava && fluid.is(FluidTags.LAVA))) { // Purpur + if (iblockdata.getBlock() instanceof BucketPickup && !((BucketPickup) iblockdata.getBlock()).pickupBlock(blockList, blockposition2, iblockdata).isEmpty()) { // CraftBukkit + ++i; +- if (j < 6) { ++ if (j < world.purpurConfig.spongeAbsorptionRadius) { // Purpur + queue.add(new Tuple<>(blockposition2, j + 1)); + } + } else if (iblockdata.getBlock() instanceof LiquidBlock) { + blockList.setBlock(blockposition2, Blocks.AIR.defaultBlockState(), 3); // CraftBukkit + ++i; +- if (j < 6) { ++ if (j < world.purpurConfig.spongeAbsorptionRadius) { // Purpur + queue.add(new Tuple<>(blockposition2, j + 1)); + } + } else if (material == Material.WATER_PLANT || material == Material.REPLACEABLE_WATER_PLANT) { +@@ -93,14 +93,14 @@ public class SpongeBlock extends Block { + blockList.setBlock(blockposition2, Blocks.AIR.defaultBlockState(), 3); + // CraftBukkit end + ++i; +- if (j < 6) { ++ if (j < world.purpurConfig.spongeAbsorptionRadius) { // Purpur + queue.add(new Tuple<>(blockposition2, j + 1)); + } + } + } + } + +- if (i > 64) { ++ if (i > world.purpurConfig.spongeAbsorptionArea) { // Purpur + break; + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java b/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java +index 0a95842c53a9d0286c57bcb42db97e468e30fb7d..b6742a1efcceb0fb950d995101b6be16b0d05978 100644 +--- a/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java +@@ -92,4 +92,16 @@ public class StonecutterBlock extends Block { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public void stepOn(Level level, BlockPos pos, BlockState state, net.minecraft.world.entity.Entity entity) { ++ if (level.purpurConfig.stonecutterDamage > 0.0F && entity instanceof net.minecraft.world.entity.LivingEntity) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); ++ entity.hurt(net.minecraft.world.damagesource.DamageSource.STONECUTTER, level.purpurConfig.stonecutterDamage); ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; ++ } ++ super.stepOn(level, pos, state, entity); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +index 6b400a4759c8c8612a3b5c96ca0d87ef9dc71435..992de1ab2c00a2545a857f1b5533926bc895f996 100644 +--- a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +@@ -19,7 +19,7 @@ import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class SugarCaneBlock extends Block { ++public class SugarCaneBlock extends Block implements BonemealableBlock { // Purpur + + public static final IntegerProperty AGE = BlockStateProperties.AGE_15; + protected static final float AABB_OFFSET = 6.0F; +@@ -106,4 +106,34 @@ public class SugarCaneBlock extends Block { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(SugarCaneBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state, boolean isClient) { ++ if (!((net.minecraft.world.level.Level) world).purpurConfig.sugarCanAffectedByBonemeal || !world.isEmptyBlock(pos.above())) return false; ++ ++ int reedHeight = 0; ++ while (world.getBlockState(pos.below(reedHeight)).is(this)) { ++ reedHeight++; ++ } ++ ++ return reedHeight < ((net.minecraft.world.level.Level) world).paperConfig().maxGrowthHeight.reeds; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(net.minecraft.world.level.Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int reedHeight = 0; ++ while (world.getBlockState(pos.below(reedHeight)).is(this)) { ++ reedHeight++; ++ } ++ for (int i = 0; i <= world.paperConfig().maxGrowthHeight.reeds - reedHeight; i++) { ++ world.setBlockAndUpdate(pos.above(i), state.setValue(SugarCaneBlock.AGE, 0)); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java b/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java +index 70d46aafa9c16921e5c5bed3d97b8f402e25038a..a5bd3f463ee8ab402d518d18a28425441af31686 100644 +--- a/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java +@@ -160,7 +160,7 @@ public class TurtleEggBlock extends Block { + private boolean shouldUpdateHatchLevel(Level world) { + float f = world.getTimeOfDay(1.0F); + +- return (double) f < 0.69D && (double) f > 0.65D ? true : world.random.nextInt(500) == 0; ++ return (double) f < 0.69D && (double) f > 0.65D ? true : world.random.nextInt(world.purpurConfig.turtleEggsRandomTickCrackChance) == 0; + } + + @Override +@@ -193,6 +193,31 @@ public class TurtleEggBlock extends Block { + } + + private boolean canDestroyEgg(Level world, Entity entity) { +- return !(entity instanceof Turtle) && !(entity instanceof Bat) ? (!(entity instanceof LivingEntity) ? false : entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) : false; ++ // Purpur start ++ if (entity instanceof Turtle || entity instanceof Bat) { ++ return false; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromExpOrbs && entity instanceof net.minecraft.world.entity.ExperienceOrb) { ++ return true; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromItems && entity instanceof net.minecraft.world.entity.item.ItemEntity) { ++ return true; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromMinecarts && entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart) { ++ return true; ++ } ++ if (!(entity instanceof LivingEntity)) { ++ return false; ++ } ++ if (world.purpurConfig.turtleEggsTramplingFeatherFalling) { ++ java.util.Iterator armor = entity.getArmorSlots().iterator(); ++ return !armor.hasNext() || net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.FALL_PROTECTION, armor.next()) < (int) entity.fallDistance; ++ } ++ if (entity instanceof Player) { ++ return true; ++ } ++ ++ return world.purpurConfig.turtleEggsBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ // Purpur end + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java b/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java +index 6866605c7ef5361b21130a19a59c3fa3660dfb19..dee5d76d29da13f8639ab5d392cd0143201e71ba 100644 +--- a/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java +@@ -27,4 +27,11 @@ public class TwistingVinesBlock extends GrowingPlantHeadBlock { + protected boolean canGrowInto(BlockState state) { + return NetherVines.isValidGrowthState(state); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.twistingVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java b/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java +index e5c135ec059746b75fe58516809584221285cdbe..713c7e6e31a3e1097b612c77a4fce147c9252e0b 100644 +--- a/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java +@@ -27,4 +27,11 @@ public class WeepingVinesBlock extends GrowingPlantHeadBlock { + protected boolean canGrowInto(BlockState state) { + return NetherVines.isValidGrowthState(state); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.weepingVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +index b91effe91dad2e1aeea0ea31140f7432833b343f..bb628bd3fe8b185f356968697b17e1c4a442a6d2 100644 +--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -71,6 +71,7 @@ public class WitherSkullBlock extends SkullBlock { + entitywither.moveTo((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.55D, (double) blockposition1.getZ() + 0.5D, shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F, 0.0F); + entitywither.yBodyRot = shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F; + entitywither.makeInvulnerable(); ++ entitywither.setSummoner(iblockdata.getBlock().placer == null ? null : iblockdata.getBlock().placer.getUUID()); // Purpur + // CraftBukkit start + if (!world.addFreshEntity(entitywither, SpawnReason.BUILD_WITHER)) { + return; +diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index cac2768fe520b591990c7bc943ae7e95f49efb31..5ae858b81e6f9903b7296077cf497f62bb8d6995 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -43,6 +43,7 @@ import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.AbstractFurnaceBlock; + import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.phys.Vec3; + // CraftBukkit start + import org.bukkit.craftbukkit.block.CraftBlock; +@@ -206,6 +207,22 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + // Paper end + } + ++ // Purpur start ++ public static void addFuel(ItemStack itemStack, Integer burnTime) { ++ Map map = Maps.newLinkedHashMap(); ++ map.putAll(getFuel()); ++ map.put(itemStack.getItem(), burnTime); ++ cachedBurnDurations = com.google.common.collect.ImmutableMap.copyOf(map); ++ } ++ ++ public static void removeFuel(ItemStack itemStack) { ++ Map map = Maps.newLinkedHashMap(); ++ map.putAll(getFuel()); ++ map.remove(itemStack.getItem()); ++ cachedBurnDurations = com.google.common.collect.ImmutableMap.copyOf(map); ++ } ++ // Purpur End ++ + // CraftBukkit start - add fields and methods + private int maxStack = MAX_STACK; + public List transaction = new java.util.ArrayList(); +@@ -323,6 +340,21 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + } + + ItemStack itemstack = (ItemStack) blockEntity.items.get(1); ++ // Purpur start ++ boolean usedLavaFromUnderneath = false; ++ if (world.purpurConfig.furnaceUseLavaFromUnderneath && !blockEntity.isLit() && itemstack.isEmpty() && !blockEntity.items.get(0).isEmpty() && world.getGameTime() % 20 == 0) { ++ BlockPos below = blockEntity.getBlockPos().below(); ++ BlockState belowState = world.getBlockStateIfLoaded(below); ++ if (belowState != null && belowState.is(Blocks.LAVA)) { ++ FluidState fluidState = belowState.getFluidState(); ++ if (fluidState != null && fluidState.isSource()) { ++ world.setBlock(below, Blocks.AIR.defaultBlockState(), 3); ++ itemstack = Items.LAVA_BUCKET.getDefaultInstance(); ++ usedLavaFromUnderneath = true; ++ } ++ } ++ } ++ // Purpur end + boolean flag2 = !((ItemStack) blockEntity.items.get(0)).isEmpty(); + boolean flag3 = !itemstack.isEmpty(); + +@@ -408,6 +440,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + setChanged(world, pos, state); + } + ++ if (usedLavaFromUnderneath) blockEntity.items.set(1, ItemStack.EMPTY); // Purpur + } + + private static boolean canBurn(@Nullable Recipe recipe, NonNullList slots, int count) { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +index 416aa989ebb18a8741cc9d605a1180ab830f6643..e38a0adf5463c48311ad08b8d2e5b5c2d989a3b5 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +@@ -67,7 +67,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + public BarrelBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BARREL, pos, state); +- this.items = NonNullList.withSize(27, ItemStack.EMPTY); ++ // Purpur start ++ this.items = NonNullList.withSize(switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }, ItemStack.EMPTY); ++ // Purpur end + this.openersCounter = new ContainerOpenersCounter() { + @Override + protected void onOpen(Level world, BlockPos pos, BlockState state) { +@@ -118,7 +127,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + @Override + public int getContainerSize() { +- return 27; ++ // Purpur start ++ return switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }; ++ // Purpur end + } + + @Override +@@ -138,7 +156,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + @Override + protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) { +- return ChestMenu.threeRows(syncId, playerInventory, this); ++ // Purpur start ++ return switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> ChestMenu.sixRows(syncId, playerInventory, this); ++ case 5 -> ChestMenu.fiveRows(syncId, playerInventory, this); ++ case 4 -> ChestMenu.fourRows(syncId, playerInventory, this); ++ case 2 -> ChestMenu.twoRows(syncId, playerInventory, this); ++ case 1 -> ChestMenu.oneRow(syncId, playerInventory, this); ++ default -> ChestMenu.threeRows(syncId, playerInventory, this); ++ }; ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +index 928625b5ab054ffa412be8a438f58291cc7a3cc0..c49175dcb7ca4469f729d3afb305fca42da82bcf 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +@@ -84,6 +84,16 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + + public double getEffectRange() { + if (this.effectRange < 0) { ++ // Purpur Start ++ if (this.level != null) { ++ switch (this.levels) { ++ case 1: return this.level.purpurConfig.beaconLevelOne; ++ case 2: return this.level.purpurConfig.beaconLevelTwo; ++ case 3: return this.level.purpurConfig.beaconLevelThree; ++ case 4: return this.level.purpurConfig.beaconLevelFour; ++ } ++ } ++ // Purpur End + return this.levels * 10 + 10; + } else { + return effectRange; +@@ -155,6 +165,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + int j = pos.getY(); + int k = pos.getZ(); + BlockPos blockposition1; ++ boolean isTintedGlass = false; + + if (blockEntity.lastCheckY < j) { + blockposition1 = pos; +@@ -188,6 +199,9 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + } + } + } else { ++ if (world.purpurConfig.beaconAllowEffectsWithTintedGlass && block.equals(Blocks.TINTED_GLASS)) { ++ isTintedGlass = true; ++ } + if (tileentitybeacon_beaconcolortracker == null || iblockdata1.getLightBlock(world, blockposition1) >= 15 && !iblockdata1.is(Blocks.BEDROCK)) { + blockEntity.checkingBeamSections.clear(); + blockEntity.lastCheckY = l; +@@ -207,7 +221,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); + } + +- if (blockEntity.levels > 0 && !blockEntity.beamSections.isEmpty()) { ++ if (blockEntity.levels > 0 && (!blockEntity.beamSections.isEmpty() || (world.purpurConfig.beaconAllowEffectsWithTintedGlass && isTintedGlass))) { + BeaconBlockEntity.applyEffects(world, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower, blockEntity); // Paper + BeaconBlockEntity.playSound(world, pos, SoundEvents.BEACON_AMBIENT); + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +index 41c9f074203915c31c1ae7a160ce509c13383f84..7b82842b97ce795745cf6ee6399f618c55acbbf3 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -43,7 +43,7 @@ public class BeehiveBlockEntity extends BlockEntity { + private final List stored = Lists.newArrayList(); + @Nullable + public BlockPos savedFlowerPos; +- public int maxBees = 3; // CraftBukkit - allow setting max amount of bees a hive can hold ++ public int maxBees = org.purpurmc.purpur.PurpurConfig.beeInsideBeeHive; // CraftBukkit - allow setting max amount of bees a hive can hold // Purpur + + public BeehiveBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BEEHIVE, pos, state); +@@ -203,7 +203,7 @@ public class BeehiveBlockEntity extends BlockEntity { + } + + private static boolean releaseBee(Level world, BlockPos blockposition, BlockState iblockdata, BeehiveBlockEntity.BeeData tileentitybeehive_hivebee, @Nullable List list, BeehiveBlockEntity.BeeReleaseStatus tileentitybeehive_releasestatus, @Nullable BlockPos blockposition1, boolean force) { +- if (!force && (world.isNight() || world.isRaining()) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) { ++ if (!force && ((world.isNight() && !world.purpurConfig.beeCanWorkAtNight) || (world.isRaining() && !world.purpurConfig.beeCanWorkInRain)) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) { // Purpur + // CraftBukkit end + return false; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +index 1b248db497500aa6bd346b306dcb908af77626f3..e438e7e018f643d82ddf5efbf72779876c516d1a 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -6,6 +6,8 @@ import net.minecraft.CrashReportCategory; + import net.minecraft.core.BlockPos; + import net.minecraft.core.registries.BuiltInRegistries; + import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.StringTag; + import net.minecraft.network.protocol.Packet; + import net.minecraft.network.protocol.game.ClientGamePacketListener; + import net.minecraft.resources.ResourceLocation; +@@ -74,10 +76,27 @@ public abstract class BlockEntity { + if (persistentDataTag instanceof CompoundTag) { + this.persistentDataContainer.putAll((CompoundTag) persistentDataTag); + } ++ // Purpur start ++ if (nbt.contains("Purpur.persistentDisplayName")) { ++ this.persistentDisplayName = nbt.getString("Purpur.persistentDisplayName"); ++ } ++ if (nbt.contains("Purpur.persistentLore")) { ++ this.persistentLore = nbt.getList("Purpur.persistentLore", 8); ++ } ++ // Purpur end + } + // CraftBukkit end + +- protected void saveAdditional(CompoundTag nbt) {} ++ protected void saveAdditional(CompoundTag nbt) { ++ // Purpur start ++ if (this.persistentDisplayName != null) { ++ nbt.put("Purpur.persistentDisplayName", StringTag.valueOf(this.persistentDisplayName)); ++ } ++ if (this.persistentLore != null) { ++ nbt.put("Purpur.persistentLore", this.persistentLore); ++ } ++ // Purpur end ++ } + + public final CompoundTag saveWithFullMetadata() { + CompoundTag nbttagcompound = this.saveWithoutMetadata(); +@@ -187,10 +206,24 @@ public abstract class BlockEntity { + + @Nullable + public Packet getUpdatePacket() { ++ // Purpur start ++ if (this instanceof net.minecraft.world.Nameable nameable && nameable.hasCustomName()) { ++ CompoundTag nbt = this.saveWithoutMetadata(); ++ nbt.remove("Items"); ++ return net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket.create(this, $ -> nbt); ++ } ++ // Purpur end + return null; + } + + public CompoundTag getUpdateTag() { ++ // Purpur start ++ if (this instanceof net.minecraft.world.Nameable nameable && nameable.hasCustomName()) { ++ CompoundTag nbt = this.saveWithoutMetadata(); ++ nbt.remove("Items"); ++ return nbt; ++ } ++ // Purpur end + return new CompoundTag(); + } + +@@ -264,4 +297,24 @@ public abstract class BlockEntity { + } + // Paper end + ++ // Purpur start ++ private String persistentDisplayName = null; ++ private ListTag persistentLore = null; ++ ++ public void setPersistentDisplayName(String json) { ++ this.persistentDisplayName = json; ++ } ++ ++ public void setPersistentLore(ListTag lore) { ++ this.persistentLore = lore; ++ } ++ ++ public String getPersistentDisplayName() { ++ return this.persistentDisplayName; ++ } ++ ++ public ListTag getPersistentLore() { ++ return this.persistentLore; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +index 05eab04e4aec4151018f25b59f92ddbbb4c09f87..3b5c502fc211940dd908f1d276fa11e3826abda7 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -172,7 +172,7 @@ public class ConduitBlockEntity extends BlockEntity { + if ((l > 1 || i1 > 1 || j1 > 1) && (i == 0 && (i1 == 2 || j1 == 2) || j == 0 && (l == 2 || j1 == 2) || k == 0 && (l == 2 || i1 == 2))) { + BlockPos blockposition2 = pos.offset(i, j, k); + BlockState iblockdata = world.getBlockState(blockposition2); +- Block[] ablock = ConduitBlockEntity.VALID_BLOCKS; ++ Block[] ablock = world.purpurConfig.conduitBlocks; // Purpur + int k1 = ablock.length; + + for (int l1 = 0; l1 < k1; ++l1) { +@@ -192,7 +192,7 @@ public class ConduitBlockEntity extends BlockEntity { + + private static void applyEffects(Level world, BlockPos pos, List activatingBlocks) { + int i = activatingBlocks.size(); +- int j = i / 7 * 16; ++ int j = i / 7 * world.purpurConfig.conduitDistance; // Purpur + int k = pos.getX(); + int l = pos.getY(); + int i1 = pos.getZ(); +@@ -223,21 +223,21 @@ public class ConduitBlockEntity extends BlockEntity { + blockEntity.destroyTarget = ConduitBlockEntity.findDestroyTarget(world, pos, blockEntity.destroyTargetUUID); + blockEntity.destroyTargetUUID = null; + } else if (blockEntity.destroyTarget == null) { +- List list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos), (entityliving1) -> { ++ List list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos, world), (entityliving1) -> { // Purpur + return entityliving1 instanceof Enemy && entityliving1.isInWaterOrRain(); + }); + + if (!list1.isEmpty()) { + blockEntity.destroyTarget = (LivingEntity) list1.get(world.random.nextInt(list1.size())); + } +- } else if (!blockEntity.destroyTarget.isAlive() || !pos.closerThan(blockEntity.destroyTarget.blockPosition(), 8.0D)) { ++ } else if (!blockEntity.destroyTarget.isAlive() || !pos.closerThan(blockEntity.destroyTarget.blockPosition(), world.purpurConfig.conduitDamageDistance)) { // Purpur + blockEntity.destroyTarget = null; + } + + if (blockEntity.destroyTarget != null) { + // CraftBukkit start + CraftEventFactory.blockDamage = CraftBlock.at(world, pos); +- if (blockEntity.destroyTarget.hurt(DamageSource.MAGIC, 4.0F)) { ++ if (blockEntity.destroyTarget.hurt(DamageSource.MAGIC, world.purpurConfig.conduitDamageAmount)) { // Purpur + world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F); + } + CraftEventFactory.blockDamage = null; +@@ -263,16 +263,22 @@ public class ConduitBlockEntity extends BlockEntity { + } + + private static AABB getDestroyRangeAABB(BlockPos pos) { ++ // Purpur start ++ return getDestroyRangeAABB(pos, null); ++ } ++ ++ private static AABB getDestroyRangeAABB(BlockPos pos, Level level) { ++ // Purpur end + int i = pos.getX(); + int j = pos.getY(); + int k = pos.getZ(); + +- return (new AABB((double) i, (double) j, (double) k, (double) (i + 1), (double) (j + 1), (double) (k + 1))).inflate(8.0D); ++ return (new AABB((double) i, (double) j, (double) k, (double) (i + 1), (double) (j + 1), (double) (k + 1))).inflate(level == null ? 8.0D : level.purpurConfig.conduitDamageDistance); // Purpur + } + + @Nullable + private static LivingEntity findDestroyTarget(Level world, BlockPos pos, UUID uuid) { +- List list = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos), (entityliving) -> { ++ List list = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos, world), (entityliving) -> { // Purpur + return entityliving.getUUID().equals(uuid); + }); + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java +index 65e1381bb2d10bd212463feb602c60f8fdb9ade1..b7370e64fd0d50e8725d7d5afc30af2e8bc8455d 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java +@@ -24,6 +24,7 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + public float tRot; + private static final RandomSource RANDOM = RandomSource.create(); + private Component name; ++ private int lapis = 0; // Purpur + + public EnchantmentTableBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.ENCHANTING_TABLE, pos, state); +@@ -35,6 +36,7 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + if (this.hasCustomName()) { + nbt.putString("CustomName", Component.Serializer.toJson(this.name)); + } ++ nbt.putInt("Purpur.Lapis", this.lapis); // Purpur + + } + +@@ -44,6 +46,7 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + if (nbt.contains("CustomName", 8)) { + this.name = io.papermc.paper.util.MCUtil.getBaseComponentFromNbt("CustomName", nbt); // Paper - Catch ParseException + } ++ this.lapis = nbt.getInt("Purpur.Lapis"); // Purpur + + } + +@@ -117,4 +120,14 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + public Component getCustomName() { + return this.name; + } ++ ++ // Purpur start ++ public int getLapis() { ++ return this.lapis; ++ } ++ ++ public void setLapis(int lapis) { ++ this.lapis = lapis; ++ } ++ // Purpur + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java +index 4da4edae517a0efec6e03a719ec47b700509dab1..9e760a8e8244b15daaf0abdfc5f8a51d5c663e12 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java +@@ -203,6 +203,23 @@ public class SignBlockEntity extends BlockEntity implements CommandSource { // C + return ClientboundBlockEntityDataPacket.create(this); + } + ++ // Purpur start ++ public ClientboundBlockEntityDataPacket getTranslatedUpdatePacket(boolean filtered) { ++ final CompoundTag nbt = new CompoundTag(); ++ this.saveAdditional(nbt); ++ final Component[] lines = getMessages(filtered); ++ for (int i = 0; i < 4; i++) { ++ final var component = io.papermc.paper.adventure.PaperAdventure.asAdventure(lines[i]); ++ final String line = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacyAmpersand().serialize(component); ++ final var text = net.kyori.adventure.text.Component.text(line); ++ final String json = net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().serialize(text); ++ nbt.putString("Text" + (i + 1), json); ++ } ++ nbt.putString("PurpurEditor", "true"); ++ return ClientboundBlockEntityDataPacket.create(this, entity -> nbt); ++ } ++ // Purpur end ++ + @Override + public CompoundTag getUpdateTag() { + return this.saveWithoutMetadata(); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index f80545f80948db27d1fbde77d0505c916eb504ed..053ec306027a83cdd06d10197d47d7edf8c213ac 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -178,6 +178,15 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + + public static void teleportEntity(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { + if (world instanceof ServerLevel && !blockEntity.isCoolingDown()) { ++ if (!entity.canChangeDimensions()) return; // Purpur ++ // Purpur start ++ if (world.purpurConfig.imposeTeleportRestrictionsOnGateways && (entity.isVehicle() || entity.isPassenger())) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, PlayerTeleportEvent.TeleportCause.END_GATEWAY).callEvent()) { ++ teleportEntity(world, pos, state, entity, blockEntity); ++ } ++ return; ++ } ++ // Purpur end + ServerLevel worldserver = (ServerLevel) world; + + blockEntity.teleportCooldown = 100; +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java b/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java +index 744d91546d1a810f60a43c15ed74b4158f341a4a..354538daefa603f6df5a139b6bff87dbb4cef178 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java +@@ -86,7 +86,7 @@ public class PistonStructureResolver { + return true; + } else { + int i = 1; +- if (i + this.toPush.size() > 12) { ++ if (i + this.toPush.size() > this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } else { + while(isSticky(blockState)) { +@@ -98,7 +98,7 @@ public class PistonStructureResolver { + } + + ++i; +- if (i + this.toPush.size() > 12) { ++ if (i + this.toPush.size() > this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } + } +@@ -142,7 +142,7 @@ public class PistonStructureResolver { + return true; + } + +- if (this.toPush.size() >= 12) { ++ if (this.toPush.size() >= this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index 25ce337ed266be7bafeacd9eb6f53a9474775fc5..37360b896edecf3f2e09f0f7da6a37d6c769c630 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -77,9 +77,9 @@ import net.minecraft.world.phys.shapes.VoxelShape; + public abstract class BlockBehaviour implements FeatureElement { + + protected static final Direction[] UPDATE_SHAPE_ORDER = new Direction[]{Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP}; +- protected final Material material; ++ public final Material material; // Purpur - protected -> public + public final boolean hasCollision; +- protected final float explosionResistance; ++ public float explosionResistance; // Purpur - protected final -> public + protected final boolean isRandomlyTicking; + protected final SoundType soundType; + protected final float friction; +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index de7a5f3812a017131fd1b32fbeff10e325b1cd2e..79bf9c277fe98df176113de39360fb34ad917577 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -130,7 +130,7 @@ public class LevelChunk extends ChunkAccess { + this.fluidTicks = fluidTickScheduler; + // CraftBukkit start + this.bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); +- this.lightningTick = this.level.getThreadUnsafeRandom().nextInt(100000) << 1; // Pufferfish - initialize lightning tick ++ this.lightningTick = java.util.concurrent.ThreadLocalRandom.current().nextInt(100000) << 1; // Pufferfish - initialize lightning tick // Purpur - any random will do + } + + public org.bukkit.Chunk bukkitChunk; +@@ -930,7 +930,7 @@ public class LevelChunk extends ChunkAccess { + this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system + + if (this.needsDecoration) { +- try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper // Purpur + this.needsDecoration = false; + java.util.Random random = new java.util.Random(); + random.setSeed(this.level.getSeed()); +@@ -950,7 +950,7 @@ public class LevelChunk extends ChunkAccess { + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(this.bukkitChunk)); +- } // Paper ++ //} // Paper // Purpur + } + } + } +@@ -1307,10 +1307,10 @@ public class LevelChunk extends ChunkAccess { + + if (LevelChunk.this.isTicking(blockposition)) { + try { +- ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler(); // Purpur + +- gameprofilerfiller.push(this::getType); +- this.blockEntity.tickTimer.startTiming(); // Spigot ++ //gameprofilerfiller.push(this::getType); // Purpur ++ //this.blockEntity.tickTimer.startTiming(); // Spigot // Purpur + BlockState iblockdata = LevelChunk.this.getBlockState(blockposition); + + if (this.blockEntity.getType().isValid(iblockdata)) { +@@ -1321,7 +1321,7 @@ public class LevelChunk extends ChunkAccess { + LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata}); + } + +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent tile entity and entity crashes +@@ -1332,7 +1332,7 @@ public class LevelChunk extends ChunkAccess { + // Paper end + // Spigot start + } finally { +- this.blockEntity.tickTimer.stopTiming(); ++ //this.blockEntity.tickTimer.stopTiming(); // Purpur + // Spigot end + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 1782d6957fa0290368e443e2e8d7b3c77ac6b8ef..2c9e9d35cd6500aa0ce3b53122067c6a2a15cbf7 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -112,6 +112,7 @@ public class EntityStorage implements EntityPersistentStorage { + ListTag listTag = new ListTag(); + final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper + entities.forEach((entity) -> { // diff here: use entities parameter ++ if (!entity.canSaveToDisk()) return; // Purpur + // Paper start + final EntityType entityType = entity.getType(); + final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +index 1c3718d9244513d9fc795dceb564a81375734557..69753f0b67a78c565ff455676860dc05b24bb285 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -51,7 +51,7 @@ public class PhantomSpawner implements CustomSpawner { + int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; + this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; + // Paper end +- if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { ++ if (world.getSkyDarken() < world.purpurConfig.phantomSpawnMinSkyDarkness && world.dimensionType().hasSkyLight()) { // Purpur + return 0; + } else { + int i = 0; +@@ -63,10 +63,10 @@ public class PhantomSpawner implements CustomSpawner { + if (!entityhuman.isSpectator() && (!world.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !entityhuman.isCreative())) { // Paper + BlockPos blockposition = entityhuman.blockPosition(); + +- if (!world.dimensionType().hasSkyLight() || blockposition.getY() >= world.getSeaLevel() && world.canSeeSky(blockposition)) { ++ if (!world.dimensionType().hasSkyLight() || (!world.purpurConfig.phantomSpawnOnlyAboveSeaLevel || blockposition.getY() >= world.getSeaLevel()) && (!world.purpurConfig.phantomSpawnOnlyWithVisibleSky || world.canSeeSky(blockposition))) { // Purpur + DifficultyInstance difficultydamagescaler = world.getCurrentDifficultyAt(blockposition); + +- if (difficultydamagescaler.isHarderThan(randomsource.nextFloat() * 3.0F)) { ++ if (difficultydamagescaler.isHarderThan(randomsource.nextFloat() * (float) world.purpurConfig.phantomSpawnLocalDifficultyChance)) { // Purpur + ServerStatsCounter serverstatisticmanager = ((ServerPlayer) entityhuman).getStats(); + int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), (int) 1, Integer.MAX_VALUE); + boolean flag2 = true; +@@ -78,7 +78,7 @@ public class PhantomSpawner implements CustomSpawner { + + if (NaturalSpawner.isValidEmptySpawnBlock(world, blockposition1, iblockdata, fluid, EntityType.PHANTOM)) { + SpawnGroupData groupdataentity = null; +- int k = 1 + randomsource.nextInt(difficultydamagescaler.getDifficulty().getId() + 1); ++ int k = world.purpurConfig.phantomSpawnMinPerAttempt + world.random.nextInt((world.purpurConfig.phantomSpawnMaxPerAttempt < 0 ? difficultydamagescaler.getDifficulty().getId() : world.purpurConfig.phantomSpawnMaxPerAttempt - world.purpurConfig.phantomSpawnMinPerAttempt) + 1); // Purpur + + for (int l = 0; l < k; ++l) { + // Paper start +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index 3f72703d2063a082546305eeb0a1b21629ddb1b2..93f31a99363e73a24571b66cd5bbaf7b5b9084b2 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -226,7 +226,7 @@ public abstract class FlowingFluid extends Fluid { + } + } + +- if (this.canConvertToSource(world) && j >= 2) { ++ if (this.canConvertToSource(world) && j >= getRequiredSources(world)) { + BlockState iblockdata2 = world.getBlockState(pos.below()); + FluidState fluid1 = iblockdata2.getFluidState(); + +@@ -324,6 +324,12 @@ public abstract class FlowingFluid extends Fluid { + + protected abstract boolean canConvertToSource(Level world); + ++ // Purpur start ++ protected int getRequiredSources(Level level) { ++ return 2; ++ } ++ // Purpur end ++ + protected void spreadTo(LevelAccessor world, BlockPos pos, BlockState state, Direction direction, FluidState fluidState) { + if (state.getBlock() instanceof LiquidBlockContainer) { + ((LiquidBlockContainer) state.getBlock()).placeLiquid(world, pos, state, fluidState); +diff --git a/src/main/java/net/minecraft/world/level/material/LavaFluid.java b/src/main/java/net/minecraft/world/level/material/LavaFluid.java +index 783e315d92227cbcb5cd207b0a06a12e0778d14b..b05b4d3d97bca159c297f150005b5ab5bf6087e0 100644 +--- a/src/main/java/net/minecraft/world/level/material/LavaFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/LavaFluid.java +@@ -180,7 +180,7 @@ public abstract class LavaFluid extends FlowingFluid { + + @Override + public int getTickDelay(LevelReader world) { +- return world.dimensionType().ultraWarm() ? 10 : 30; ++ return world.dimensionType().ultraWarm() ? world.getWorldBorder().world.purpurConfig.lavaSpeedNether : world.getWorldBorder().world.purpurConfig.lavaSpeedNotNether; // Purpur + } + + @Override +@@ -198,6 +198,13 @@ public abstract class LavaFluid extends FlowingFluid { + world.levelEvent(1501, pos, 0); + } + ++ // Purpur start ++ @Override ++ protected int getRequiredSources(Level level) { ++ return level.purpurConfig.lavaInfiniteRequiredSources; ++ } ++ // Purpur end ++ + @Override + protected boolean canConvertToSource(Level world) { + return world.getGameRules().getBoolean(GameRules.RULE_LAVA_SOURCE_CONVERSION); +@@ -232,7 +239,7 @@ public abstract class LavaFluid extends FlowingFluid { + + @Override + protected float getExplosionResistance() { +- return 100.0F; ++ return Blocks.LAVA.getExplosionResistance(); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/material/WaterFluid.java b/src/main/java/net/minecraft/world/level/material/WaterFluid.java +index 82e85fbbd45244d02df90fa00c9046e7f51275a2..0f16deddd8cbb506ef7886f57ae640a42e841703 100644 +--- a/src/main/java/net/minecraft/world/level/material/WaterFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/WaterFluid.java +@@ -64,6 +64,13 @@ public abstract class WaterFluid extends FlowingFluid { + return world.getGameRules().getBoolean(GameRules.RULE_WATER_SOURCE_CONVERSION); + } + ++ // Purpur start ++ @Override ++ protected int getRequiredSources(Level level) { ++ return level.purpurConfig.waterInfiniteRequiredSources; ++ } ++ // Purpur end ++ + // Paper start + @Override + protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state, BlockPos source) { +@@ -109,7 +116,7 @@ public abstract class WaterFluid extends FlowingFluid { + + @Override + protected float getExplosionResistance() { +- return 100.0F; ++ return Blocks.WATER.getExplosionResistance(); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +index d23481453717f715124156b5d83f6448f720d049..a8af51a25b0f99c3a64d9150fdfcd6b818aa7581 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +@@ -53,8 +53,8 @@ public class PathFinder { + @Nullable + // Paper start - optimize collection + private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { +- profiler.push("find_path"); +- profiler.markForCharting(MetricCategory.PATH_FINDING); ++ //profiler.push("find_path"); // Purpur ++ //profiler.markForCharting(MetricCategory.PATH_FINDING); // Purpur + // Set set = positions.keySet(); + startNode.g = 0.0F; + startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +index 894881018c659d874f28f5744f0b8247cfecb1c1..365c3d01a59d117ee9f238b1c1ded645d6b758d3 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +@@ -243,7 +243,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + } + + if (blockPathTypes != BlockPathTypes.WALKABLE && (!this.isAmphibious() || blockPathTypes != BlockPathTypes.WATER)) { +- if ((node == null || node.costMalus < 0.0F) && maxYStep > 0 && (blockPathTypes != BlockPathTypes.FENCE || this.canWalkOverFences()) && blockPathTypes != BlockPathTypes.UNPASSABLE_RAIL && blockPathTypes != BlockPathTypes.TRAPDOOR && blockPathTypes != BlockPathTypes.POWDER_SNOW) { ++ if ((node == null || node.costMalus < 0.0F) && maxYStep > 0 && (blockPathTypes != BlockPathTypes.FENCE || this.canWalkOverFences()) && (this.mob.level.purpurConfig.mobsIgnoreRails || blockPathTypes != BlockPathTypes.UNPASSABLE_RAIL) && blockPathTypes != BlockPathTypes.TRAPDOOR && blockPathTypes != BlockPathTypes.POWDER_SNOW) { // Purpur + node = this.findAcceptedNode(x, y + 1, z, maxYStep - 1, prevFeetY, direction, nodeType); + if (node != null && (node.type == BlockPathTypes.OPEN || node.type == BlockPathTypes.WALKABLE) && this.mob.getBbWidth() < 1.0F) { + double g = (double)(x - direction.getStepX()) + 0.5D; +@@ -475,7 +475,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + return BlockPathTypes.DANGER_CACTUS; + } + +- if (blockState.is(Blocks.SWEET_BERRY_BUSH)) { ++ if (blockState.is(Blocks.SWEET_BERRY_BUSH) || blockState.is(Blocks.STONECUTTER)) { // Purpur + return BlockPathTypes.DANGER_OTHER; + } + +@@ -507,7 +507,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + return BlockPathTypes.POWDER_SNOW; + } else if (blockState.is(Blocks.CACTUS)) { + return BlockPathTypes.DAMAGE_CACTUS; +- } else if (blockState.is(Blocks.SWEET_BERRY_BUSH)) { ++ } else if (blockState.is(Blocks.SWEET_BERRY_BUSH) || blockState.is(Blocks.STONECUTTER)) { // Purpur + return BlockPathTypes.DAMAGE_OTHER; + } else if (blockState.is(Blocks.HONEY_BLOCK)) { + return BlockPathTypes.STICKY_HONEY; +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalShape.java b/src/main/java/net/minecraft/world/level/portal/PortalShape.java +index c461e0d04047db9c0c5ecc04063cebd38bf96ec2..e7554ec800f321e4e34c926c53f2375a8c3aa979 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalShape.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalShape.java +@@ -34,7 +34,7 @@ public class PortalShape { + private static final int MIN_HEIGHT = 3; + public static final int MAX_HEIGHT = 21; + private static final BlockBehaviour.StatePredicate FRAME = (iblockdata, iblockaccess, blockposition) -> { +- return iblockdata.is(Blocks.OBSIDIAN); ++ return iblockdata.is(Blocks.OBSIDIAN) || (org.purpurmc.purpur.PurpurConfig.cryingObsidianValidForPortalFrame && iblockdata.is(Blocks.CRYING_OBSIDIAN)); // Purpur + }; + private static final float SAFE_TRAVEL_MAX_ENTITY_XY = 4.0F; + private static final double SAFE_TRAVEL_MAX_VERTICAL_DELTA = 1.0D; +diff --git a/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java b/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java +index 31918fa2eb38e42a5ea5366e559f25ea9d7d59ae..15d8e9261a89da30529ac347462c520920ca4e7d 100644 +--- a/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java ++++ b/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java +@@ -49,6 +49,13 @@ public class LootingEnchantFunction extends LootItemConditionalFunction { + + if (entity instanceof LivingEntity) { + int i = EnchantmentHelper.getMobLooting((LivingEntity) entity); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.fixProjectileLootingTransfer && ++ context.getParamOrNull(LootContextParams.DIRECT_KILLER_ENTITY) ++ instanceof net.minecraft.world.entity.projectile.AbstractArrow arrow) { ++ i = arrow.lootingLevel; ++ } ++ // Purpur end + // CraftBukkit start - use lootingModifier if set by plugin + if (context.hasParam(LootContextParams.LOOTING_MOD)) { + i = context.getParamOrNull(LootContextParams.LOOTING_MOD); +diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index 68cc6f2a78a06293a29317fda72ab3ee79b3533a..cfb2e46b34b2982d6724f18214557fc80cf4adfa 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -367,4 +367,10 @@ public class AABB { + public static AABB ofSize(Vec3 center, double dx, double dy, double dz) { + return new AABB(center.x - dx / 2.0D, center.y - dy / 2.0D, center.z - dz / 2.0D, center.x + dx / 2.0D, center.y + dy / 2.0D, center.z + dz / 2.0D); + } ++ ++ // Purpur - tuinity added method ++ public final AABB offsetY(double dy) { ++ return new AABB(this.minX, this.minY + dy, this.minZ, this.maxX, this.maxY + dy, this.maxZ); ++ } ++ // Purpur + } +diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +index 7f1ac2cb29eb84833c0895442d611dfa0504527e..5dea8414964e0d2d1fb15a6baa27227e9722bfc7 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +@@ -86,20 +86,20 @@ public class LevelTicks implements LevelTickAccess { + } + + public void tick(long time, int maxTicks, BiConsumer ticker) { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("collect"); +- this.collectTicks(time, maxTicks, profilerFiller); +- profilerFiller.popPush("run"); +- profilerFiller.incrementCounter("ticksToRun", this.toRunThisTick.size()); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("collect"); // Purpur ++ this.collectTicks(time, maxTicks, null); // Purpur ++ //profilerFiller.popPush("run"); // Purpur ++ //profilerFiller.incrementCounter("ticksToRun", this.toRunThisTick.size()); // Purpur + this.runCollectedTicks(ticker); +- profilerFiller.popPush("cleanup"); ++ //profilerFiller.popPush("cleanup"); // Purpur + this.cleanupAfterTick(); +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + } + + private void collectTicks(long time, int maxTicks, ProfilerFiller profiler) { + this.sortContainersToTick(time); +- profiler.incrementCounter("containersToTick", this.containersToTick.size()); ++ //profiler.incrementCounter("containersToTick", this.containersToTick.size()); // Purpur + this.drainContainers(time, maxTicks); + this.rescheduleLeftoverContainers(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +index 714afc98b5150907b45a00060be4e41582333204..312a6d90c0a09570aef24c205dc2ff277dcd4279 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +@@ -549,4 +549,213 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa + manager.save(); + } + } ++ ++ // Purpur start - OfflinePlayer API ++ @Override ++ public boolean getAllowFlight() { ++ if (this.isOnline()) { ++ return this.getPlayer().getAllowFlight(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ if (!data.contains("abilities")) return false; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getByte("mayfly") == (byte) 1; ++ } ++ } ++ ++ @Override ++ public void setAllowFlight(boolean flight) { ++ if (this.isOnline()) { ++ this.getPlayer().setAllowFlight(flight); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putByte("mayfly", (byte) (flight ? 1 : 0)); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public boolean isFlying() { ++ if (this.isOnline()) { ++ return this.isFlying(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ if (!data.contains("abilities")) return false; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getByte("flying") == (byte) 1; ++ } ++ } ++ ++ @Override ++ public void setFlying(boolean value) { ++ if (this.isOnline()) { ++ this.getPlayer().setFlying(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putByte("mayfly", (byte) (value ? 1 : 0)); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public void setFlySpeed(float value) throws IllegalArgumentException { ++ if (value < -1f || value > 1f) throw new IllegalArgumentException("FlySpeed needs to be between -1 and 1"); ++ if (this.isOnline()) { ++ this.getPlayer().setFlySpeed(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putFloat("flySpeed", value); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public float getFlySpeed() { ++ if (this.isOnline()) { ++ return this.getPlayer().getFlySpeed(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return 0; ++ if (!data.contains("abilities")) return 0; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getFloat("flySpeed"); ++ } ++ } ++ ++ @Override ++ public void setWalkSpeed(float value) throws IllegalArgumentException { ++ if (value < -1f || value > 1f) throw new IllegalArgumentException("WalkSpeed needs to be between -1 and 1"); ++ if (this.isOnline()) { ++ this.getPlayer().setWalkSpeed(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putFloat("walkSpeed", value); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public float getWalkSpeed() { ++ if (this.isOnline()) { ++ return this.getPlayer().getWalkSpeed(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return 0; ++ if (!data.contains("abilities")) return 0; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getFloat("walkSpeed"); ++ } ++ } ++ ++ @Override ++ public Location getLocation() { ++ if (this.isOnline()) { ++ return this.getPlayer().getLocation(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return null; ++ long worldUUIDMost = data.getLong("WorldUUIDMost"); ++ long worldUUIDLeast = data.getLong("WorldUUIDLeast"); ++ net.minecraft.nbt.ListTag position = data.getList("Pos", org.bukkit.craftbukkit.util.CraftMagicNumbers.NBT.TAG_DOUBLE); ++ net.minecraft.nbt.ListTag rotation = data.getList("Rotation", org.bukkit.craftbukkit.util.CraftMagicNumbers.NBT.TAG_FLOAT); ++ UUID worldUuid = new UUID(worldUUIDMost, worldUUIDLeast); ++ org.bukkit.World world = server.getWorld(worldUuid); ++ double x = position.getDouble(0); ++ double y = position.getDouble(1); ++ double z = position.getDouble(2); ++ float yaw = rotation.getFloat(0); ++ float pitch = rotation.getFloat(1); ++ return new Location(world, x, y, z, yaw, pitch); ++ } ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleport(destination); ++ } else { ++ return setLocation(destination); ++ } ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause){ ++ if (this.isOnline()) { ++ return this.getPlayer().teleport(destination, cause); ++ } else { ++ return setLocation(destination); ++ } ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(Location destination) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleportAsync(destination); ++ } else { ++ return java.util.concurrent.CompletableFuture.completedFuture(setLocation(destination)); ++ } ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(Location destination, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleportAsync(destination, cause); ++ } else { ++ return java.util.concurrent.CompletableFuture.completedFuture(setLocation(destination)); ++ } ++ } ++ ++ private boolean setLocation(Location location) { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ data.putLong("WorldUUIDMost", location.getWorld().getUID().getMostSignificantBits()); ++ data.putLong("WorldUUIDLeast", location.getWorld().getUID().getLeastSignificantBits()); ++ net.minecraft.nbt.ListTag position = new net.minecraft.nbt.ListTag(); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getX())); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getY())); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getZ())); ++ data.put("Pos", position); ++ net.minecraft.nbt.ListTag rotation = new net.minecraft.nbt.ListTag(); ++ rotation.add(net.minecraft.nbt.FloatTag.valueOf(location.getYaw())); ++ rotation.add(net.minecraft.nbt.FloatTag.valueOf(location.getPitch())); ++ data.put("Rotation", rotation); ++ save(data); ++ return true; ++ } ++ ++ /** ++ * Safely replaces player's .dat file with provided CompoundTag ++ * @param compoundTag ++ */ ++ private void save(CompoundTag compoundTag) { ++ File playerDir = server.console.playerDataStorage.getPlayerDir(); ++ try { ++ File tempFile = File.createTempFile(this.getUniqueId()+"-", ".dat", playerDir); ++ net.minecraft.nbt.NbtIo.writeCompressed(compoundTag, tempFile); ++ File playerDataFile = new File(playerDir, this.getUniqueId()+".dat"); ++ File playerDataFileOld = new File(playerDir, this.getUniqueId()+".dat_old"); ++ net.minecraft.Util.safeReplaceFile(playerDataFile, tempFile, playerDataFileOld); ++ } catch (java.io.IOException e) { ++ e.printStackTrace(); ++ } ++ } ++ // Purpur end - OfflinePlayer API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 7ddf52de4b095f63c75b696008fcdde6345fc3c8..584596c8849b4dd7f955216f313eefb3229b375c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -261,7 +261,7 @@ import javax.annotation.Nullable; // Paper + import javax.annotation.Nonnull; // Paper + + public final class CraftServer implements Server { +- private final String serverName = "Pufferfish"; // Paper // Pufferfish ++ private final String serverName = "Purpur"; // Paper // Purpur + private final String serverVersion; + private final String bukkitVersion = Versioning.getBukkitVersion(); + private final Logger logger = Logger.getLogger("Minecraft"); +@@ -321,6 +321,20 @@ public final class CraftServer implements Server { + this.structureManager = new CraftStructureManager(console.getStructureManager()); + + Bukkit.setServer(this); ++ // Purpur start ++ org.purpurmc.purpur.language.Language.setLanguage(new org.purpurmc.purpur.language.Language() { ++ private net.minecraft.locale.Language language = net.minecraft.locale.Language.getInstance(); ++ @Override ++ public boolean has(@org.jetbrains.annotations.NotNull String key) { ++ return language.has(key); ++ } ++ ++ @Override ++ public @org.jetbrains.annotations.NotNull String getOrDefault(@org.jetbrains.annotations.NotNull String key) { ++ return language.getOrDefault(key); ++ } ++ }); ++ // Purpur end + + // Register all the Enchantments and PotionTypes now so we can stop new registration immediately after + Enchantments.SHARPNESS.getClass(); +@@ -961,6 +975,7 @@ public final class CraftServer implements Server { + + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot + this.console.paperConfigurations.reloadConfigs(this.console); ++ org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur + for (ServerLevel world : this.console.getAllLevels()) { + // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty + world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) +@@ -976,6 +991,7 @@ public final class CraftServer implements Server { + } + } + world.spigotConfig.init(); // Spigot ++ world.purpurConfig.init(); // Purpur + } + + Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper +@@ -991,6 +1007,7 @@ public final class CraftServer implements Server { + this.reloadData(); + org.spigotmc.SpigotConfig.registerCommands(); // Spigot + io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper ++ org.purpurmc.purpur.PurpurConfig.registerCommands(); // Purpur + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); + +@@ -1433,6 +1450,55 @@ public final class CraftServer implements Server { + return true; + } + ++ // Purpur Start ++ @Override ++ public void addFuel(org.bukkit.Material material, int burnTime) { ++ Preconditions.checkArgument(burnTime > 0, "BurnTime must be greater than 0"); ++ net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.addFuel(net.minecraft.world.item.ItemStack.fromBukkitCopy(new ItemStack(material)), burnTime); ++ } ++ ++ @Override ++ public void removeFuel(org.bukkit.Material material) { ++ net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.removeFuel(net.minecraft.world.item.ItemStack.fromBukkitCopy(new ItemStack(material))); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ this.worlds.forEach((name, world) -> world.sendBlockHighlight(location, duration, text, argb)); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ this.worlds.forEach((name, world) -> clearBlockHighlights()); ++ } ++ // Purpur End ++ + @Override + public List getRecipesFor(ItemStack result) { + Validate.notNull(result, "Result cannot be null"); +@@ -2705,6 +2771,7 @@ public final class CraftServer implements Server { + @Override + public double[] getTPS() { + return new double[] { ++ net.minecraft.server.MinecraftServer.getServer().tps5s.getAverage(), // Purpur + net.minecraft.server.MinecraftServer.getServer().tps1.getAverage(), + net.minecraft.server.MinecraftServer.getServer().tps5.getAverage(), + net.minecraft.server.MinecraftServer.getServer().tps15.getAverage() +@@ -2751,6 +2818,18 @@ public final class CraftServer implements Server { + return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console); + } + ++ // Purpur start ++ @Override ++ public YamlConfiguration getPurpurConfig() { ++ return org.purpurmc.purpur.PurpurConfig.config; ++ } ++ ++ @Override ++ public java.util.Properties getServerProperties() { ++ return getProperties().properties; ++ } ++ // Purpur end ++ + @Override + public void restart() { + org.spigotmc.RestartCommand.restart(); +@@ -2926,4 +3005,16 @@ public final class CraftServer implements Server { + } + + // Paper end ++ ++ // Purpur start ++ @Override ++ public String getServerName() { ++ return this.getProperties().serverName; ++ } ++ ++ @Override ++ public boolean isLagging() { ++ return getServer().lagging; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index c91bac6f3f1d60c950b93d157531cd8f7500e8d8..6a327616cd590b70170f8441c003a2109640201d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2253,6 +2253,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return (this.getHandle().dragonFight() == null) ? null : new CraftDragonBattle(this.getHandle().dragonFight()); + } + ++ // Purpur start ++ public float getLocalDifficultyAt(Location location) { ++ return getHandle().getCurrentDifficultyAt(io.papermc.paper.util.MCUtil.toBlockPosition(location)).getEffectiveDifficulty(); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ net.minecraft.network.protocol.game.DebugPackets.sendGameTestAddMarker(getHandle(), io.papermc.paper.util.MCUtil.toBlockPosition(location), text, argb, duration); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ net.minecraft.network.protocol.game.DebugPackets.sendGameTestClearPacket(getHandle()); ++ } ++ // Purpur end ++ + @Override + public PersistentDataContainer getPersistentDataContainer() { + return this.persistentDataContainer; +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 190e17eb28d166aedee06e9578561ba402d1b6d2..576cd8e20982bb20d10213b6c7a229428eec1c2f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -166,6 +166,20 @@ public class Main { + .describedAs("Jar file"); + // Paper end + ++ // Purpur Start ++ acceptsAll(asList("purpur", "purpur-settings"), "File for purpur settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("purpur.yml")) ++ .describedAs("Yml file"); ++ ++ acceptsAll(asList("pufferfish", "pufferfish-settings"), "File for pufferfish settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("pufferfish.yml")) ++ .describedAs("Yml file"); ++ // Purpur end ++ + // Paper start + acceptsAll(asList("server-name"), "Name of the server") + .withRequiredArg() +@@ -270,7 +284,7 @@ public class Main { + System.setProperty(TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper + } + +- if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { ++ if (false && Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { // Purpur + Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper + + Calendar deadline = Calendar.getInstance(); +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +index 4e56018b64d11f76c8da43fd8f85c6de72204e36..aa8212432825db65cf485cd93f734ccd9eefcb5a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +@@ -21,7 +21,12 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + + @Override + public void sendMessage(String message) { +- this.sendRawMessage(message); ++ // Purpur start ++ String[] parts = message.split("\n"); ++ for (String part : parts) { ++ this.sendRawMessage(part); ++ } ++ // Purpur end + } + + @Override +@@ -91,7 +96,7 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + // Paper start + @Override + public void sendMessage(final net.kyori.adventure.identity.Identity identity, final net.kyori.adventure.text.Component message, final net.kyori.adventure.audience.MessageType type) { +- this.sendRawMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message)); ++ this.sendMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message)); // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java b/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java +index 6035af2cf08353b3d3801220d8116d8611a0cd37..7774ab6a2e553a40def4bb4dceea9e5f58d31c1e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java +@@ -94,6 +94,7 @@ public final class VanillaCommandWrapper extends BukkitCommand { + } + + public static String getPermission(CommandNode vanillaCommand) { ++ if (vanillaCommand.getPermission() != null) return vanillaCommand.getPermission(); // Purpur + // Paper start + final String commandName; + if (vanillaCommand.getRedirect() == null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java +index 3d0ce0803e1da8a2681a3cb41096ac942ece54a1..bcd075a771c7f43c6d1549aeec2ccb20ee168b57 100644 +--- a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java ++++ b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java +@@ -59,6 +59,7 @@ public class CraftEnchantment extends Enchantment { + return EnchantmentTarget.CROSSBOW; + case VANISHABLE: + return EnchantmentTarget.VANISHABLE; ++ case BOW_AND_CROSSBOW: return EnchantmentTarget.BOW_AND_CROSSBOW; // Purpur + default: + return null; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +index 75c7645fb5732c43d1da15181cf5c7ee4c3ecd6c..e7f5ea4d8d72672cf03483e720c6389425f28f6d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +@@ -27,12 +27,12 @@ public class CraftEndermite extends CraftMonster implements Endermite { + + @Override + public boolean isPlayerSpawned() { +- return false; ++ return getHandle().isPlayerSpawned(); // Purpur + } + + @Override + public void setPlayerSpawned(boolean playerSpawned) { +- // Nop ++ getHandle().setPlayerSpawned(playerSpawned); // Purpur + } + // Paper start + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 750ac80eed6ba03e138dd4c03f57ddfe4a123276..2d5b125b6420ceb3deb5c05fadae458189fe1c0d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -206,6 +206,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + this.entity = entity; + } + ++ @Override ++ public boolean isInDaylight() { ++ return getHandle().isSunBurnTick(); ++ } ++ + public static CraftEntity getEntity(CraftServer server, Entity entity) { + /* + * Order is *EXTREMELY* important -- keep it right! =D +@@ -575,6 +580,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + // Paper end + + if ((!ignorePassengers && this.entity.isVehicle()) || this.entity.isRemoved()) { // Paper - Teleport passenger API ++ // Purpur start ++ if (!entity.isRemoved() && new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, cause).callEvent()) ++ return teleport(location, cause); ++ // Purpur end + return false; + } + +@@ -1407,4 +1416,37 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return !this.getHandle().level.noCollision(this.getHandle(), aabb); + } + // Paper End - Collision API ++ ++ // Purpur start ++ @Override ++ public org.bukkit.entity.Player getRider() { ++ Player rider = getHandle().getRider(); ++ return rider != null ? (org.bukkit.entity.Player) rider.getBukkitEntity() : null; ++ } ++ ++ @Override ++ public boolean hasRider() { ++ return getHandle().getRider() != null; ++ } ++ ++ @Override ++ public boolean isRidable() { ++ return getHandle().isRidable(); ++ } ++ ++ @Override ++ public boolean isRidableInWater() { ++ return getHandle().rideableUnderWater(); ++ } ++ ++ @Override ++ public boolean isImmuneToFire() { ++ return getHandle().fireImmune(); ++ } ++ ++ @Override ++ public void setImmuneToFire(Boolean fireImmune) { ++ getHandle().immuneToFire = fireImmune; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +index e54574c60c5cbc4e083c4ca11e0b7dd49bd03478..bf69ec45329720980dc126b03e1a6b67f79a17c0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +@@ -258,6 +258,7 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { + @Override + public void recalculatePermissions() { + this.perm.recalculatePermissions(); ++ getHandle().canPortalInstant = hasPermission("purpur.portal.instant"); // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +index 2966d4d466f44751b2f02afda2273a708c12b251..55f19324f92f98e497da49d3022e0edfc2351461 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +@@ -33,4 +33,17 @@ public class CraftIronGolem extends CraftGolem implements IronGolem { + public EntityType getType() { + return EntityType.IRON_GOLEM; + } ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +index ecec5e17807a760769fc0ea79c2a0161cc5db1ef..2042d65e9470fca2c35e492d2f8bb4dbf11813cf 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +@@ -160,4 +160,51 @@ public class CraftItem extends CraftEntity implements Item { + public EntityType getType() { + return EntityType.DROPPED_ITEM; + } ++ ++ // Purpur start ++ @Override ++ public void setImmuneToCactus(boolean immuneToCactus) { ++ item.immuneToCactus = immuneToCactus; ++ } ++ ++ @Override ++ public boolean isImmuneToCactus() { ++ return item.immuneToCactus; ++ } ++ ++ @Override ++ public void setImmuneToExplosion(boolean immuneToExplosion) { ++ item.immuneToExplosion = immuneToExplosion; ++ } ++ ++ @Override ++ public boolean isImmuneToExplosion() { ++ return item.immuneToExplosion; ++ } ++ ++ @Override ++ public void setImmuneToFire(@org.jetbrains.annotations.Nullable Boolean immuneToFire) { ++ item.immuneToFire = (immuneToFire != null && immuneToFire); ++ } ++ ++ @Override ++ public void setImmuneToFire(boolean immuneToFire) { ++ this.setImmuneToFire((Boolean) immuneToFire); ++ } ++ ++ @Override ++ public boolean isImmuneToFire() { ++ return item.immuneToFire; ++ } ++ ++ @Override ++ public void setImmuneToLightning(boolean immuneToLightning) { ++ item.immuneToLightning = immuneToLightning; ++ } ++ ++ @Override ++ public boolean isImmuneToLightning() { ++ return item.immuneToLightning; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +index 2cff68a5c448c0e971d95e9264223eb943730968..78ac748859e21a61140e9bff67e4527a8d35b4b6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +@@ -444,7 +444,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + net.minecraft.server.level.ServerPlayer entityPlayer = killer == null ? null : ((CraftPlayer) killer).getHandle(); + getHandle().lastHurtByPlayer = entityPlayer; + getHandle().lastHurtByMob = entityPlayer; +- getHandle().lastHurtByPlayerTime = entityPlayer == null ? 0 : 100; // 100 value taken from EntityLiving#damageEntity ++ getHandle().lastHurtByPlayerTime = entityPlayer == null ? 0 : getHandle().level.purpurConfig.mobLastHurtByPlayerTime; // 100 value taken from EntityLiving#damageEntity // Purpur + } + // Paper end + +@@ -455,7 +455,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + + @Override + public boolean addPotionEffect(PotionEffect effect, boolean force) { +- this.getHandle().addEffect(new MobEffectInstance(MobEffect.byId(effect.getType().getId()), effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles(), effect.hasIcon()), EntityPotionEffectEvent.Cause.PLUGIN); // Paper - Don't ignore icon ++ this.getHandle().addEffect(new MobEffectInstance(MobEffect.byId(effect.getType().getId()), effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles(), effect.hasIcon(), effect.getKey()), EntityPotionEffectEvent.Cause.PLUGIN); // Purpur - add key // Paper - Don't ignore icon + return true; + } + +@@ -476,7 +476,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + @Override + public PotionEffect getPotionEffect(PotionEffectType type) { + MobEffectInstance handle = this.getHandle().getEffect(MobEffect.byId(type.getId())); +- return (handle == null) ? null : new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible()); ++ return (handle == null) ? null : new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible(), handle.getKey()); // Purpur - add key + } + + @Override +@@ -488,7 +488,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + public Collection getActivePotionEffects() { + List effects = new ArrayList(); + for (MobEffectInstance handle : this.getHandle().activeEffects.values()) { +- effects.add(new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible())); ++ effects.add(new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible(), handle.getKey())); // Purpur - add key + } + return effects; + } +@@ -883,7 +883,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + return EntityCategory.WATER; + } + +- throw new UnsupportedOperationException("Unsupported monster type: " + type + ". This is a bug, report this to Spigot."); ++ throw new UnsupportedOperationException("Unsupported monster type: " + type + ". This is a bug, report this to Purpur."); // Purpur + } + + @Override +@@ -1070,4 +1070,32 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + getHandle().knockback(strength, directionX, directionZ); + }; + // Paper end ++ ++ // Purpur start ++ @Override ++ public float getSafeFallDistance() { ++ return getHandle().safeFallDistance; ++ } ++ ++ @Override ++ public void setSafeFallDistance(float safeFallDistance) { ++ getHandle().safeFallDistance = safeFallDistance; ++ } ++ ++ @Override ++ public void broadcastItemBreak(org.bukkit.inventory.EquipmentSlot slot) { ++ if (slot == null) return; ++ getHandle().broadcastBreakEvent(org.bukkit.craftbukkit.CraftEquipmentSlot.getNMS(slot)); ++ } ++ ++ @Override ++ public boolean shouldBurnInDay() { ++ return getHandle().shouldBurnInDay(); ++ } ++ ++ @Override ++ public void setShouldBurnInDay(boolean shouldBurnInDay) { ++ getHandle().setShouldBurnInDay(shouldBurnInDay); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +index 4d7a2c4c1001aefe9fcd4be8dbcb414f721bfff9..2c7716a9d65ebda209a144b82c2126b602aa9182 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +@@ -96,4 +96,16 @@ public class CraftLlama extends CraftChestedHorse implements Llama, com.destroys + return this.getHandle().caravanTail == null ? null : (Llama) this.getHandle().caravanTail.getBukkitEntity(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public boolean shouldJoinCaravan() { ++ return getHandle().shouldJoinCaravan; ++ } ++ ++ @Override ++ public void setShouldJoinCaravan(boolean shouldJoinCaravan) { ++ getHandle().shouldJoinCaravan = shouldJoinCaravan; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index c33e92cee1291c0ca1863fa8f33cd3b1c5449e92..4bce90ba8d2316fad9a8f89f06d17325d353667f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -524,10 +524,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setPlayerListName(String name) { ++ // Purpur start ++ setPlayerListName(name, false); ++ } ++ public void setPlayerListName(String name, boolean useMM) { ++ // Purpur end + if (name == null) { + name = getName(); + } +- this.getHandle().listName = name.equals(getName()) ? null : CraftChatMessage.fromStringOrNull(name); ++ this.getHandle().listName = name.equals(getName()) ? null : useMM ? io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name)) : CraftChatMessage.fromStringOrNull(name); // Purpur + for (ServerPlayer player : (List) server.getHandle().players) { + if (player.getBukkitEntity().canSee(this)) { + player.connection.send(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, this.getHandle())); +@@ -1307,6 +1312,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + + if (entity.isVehicle() && !ignorePassengers) { // Paper - Teleport API ++ // Purpur start ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, cause).callEvent()) ++ return teleport(location, cause); ++ // Purpur end + return false; + } + +@@ -2273,6 +2282,28 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.getHandle().getAbilities().walkingSpeed * 2f; + } + ++ // Purpur start - OfflinePlayer API ++ @Override ++ public boolean teleportOffline(@NotNull Location destination) { ++ return this.teleport(destination); ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination, PlayerTeleportEvent.TeleportCause cause) { ++ return this.teleport(destination, cause); ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination) { ++ return this.teleportAsync(destination); ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination, PlayerTeleportEvent.TeleportCause cause) { ++ return this.teleportAsync(destination, cause); ++ } ++ // Purpur end - OfflinePlayer API ++ + private void validateSpeed(float value) { + if (value < 0) { + if (value < -1f) { +@@ -3066,4 +3097,97 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.spigot; + } + // Spigot end ++ ++ // Purpur start ++ @Override ++ public boolean usesPurpurClient() { ++ return getHandle().purpurClient; ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return getHandle().isAfk(); ++ } ++ ++ @Override ++ public void setAfk(boolean setAfk) { ++ getHandle().setAfk(setAfk); ++ } ++ ++ @Override ++ public void resetIdleTimer() { ++ getHandle().resetLastActionTime(); ++ } ++ ++ @Override ++ public boolean isSpawnInvulnerable() { ++ return getHandle().isSpawnInvulnerable(); ++ } ++ ++ @Override ++ public int getSpawnInvulnerableTicks() { ++ return getHandle().spawnInvulnerableTime; ++ } ++ ++ @Override ++ public void setSpawnInvulnerableTicks(int spawnInvulnerableTime) { ++ getHandle().spawnInvulnerableTime = spawnInvulnerableTime; ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ if (this.getHandle().connection == null) return; ++ FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); ++ buf.writeBlockPos(io.papermc.paper.util.MCUtil.toBlockPosition(location)); ++ buf.writeInt(argb); ++ buf.writeUtf(text); ++ buf.writeInt(duration); ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.DEBUG_GAME_TEST_ADD_MARKER, buf)); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.DEBUG_GAME_TEST_CLEAR, new FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()))); ++ } ++ ++ @Override ++ public void sendDeathScreen(net.kyori.adventure.text.Component message) { ++ sendDeathScreen(message, null); ++ } ++ ++ @Override ++ public void sendDeathScreen(net.kyori.adventure.text.Component message, org.bukkit.entity.Entity killer) { ++ if (this.getHandle().connection == null) return; ++ net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket packet = new net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket(getEntityId(), killer == null ? -1 : killer.getEntityId(), null); ++ packet.adventure$message = message; ++ this.getHandle().connection.send(packet); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +index 659e2959c5330e4764ea1edc7f8de9f464f9ff52..c2bac8ae958630acaaa8d758e31428d2ac556ccf 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +@@ -34,4 +34,17 @@ public class CraftSnowman extends CraftGolem implements Snowman, com.destroystok + public EntityType getType() { + return EntityType.SNOWMAN; + } ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +index a1a8ac55e572156671e47317ba061855be79e5ac..ec3fb8865211bd7625103c37af7b96df37163a07 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +@@ -222,4 +222,11 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { + getHandle().getGossips().gossips.clear(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public boolean isLobotomized() { ++ return getHandle().isLobotomized(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +index 4cf3a374c9ee7c7bcf82e778aa094eb4f8463595..5c1bfd37d4494525d7890f6530a68ae47353e157 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +@@ -88,4 +88,17 @@ public class CraftWither extends CraftMonster implements Wither, com.destroystok + getHandle().setCanTravelThroughPortals(value); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +index e43fd3e59fd8c74828ae65965fade27f56beef65..b2f133c8baabba1cffa6e92ea0f854532f4c181b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +@@ -63,4 +63,16 @@ public class CraftWolf extends CraftTameableAnimal implements Wolf { + public void setInterested(boolean flag) { + this.getHandle().setIsInterested(flag); + } ++ ++ // Purpur start ++ @Override ++ public boolean isRabid() { ++ return getHandle().isRabid(); ++ } ++ ++ @Override ++ public void setRabid(boolean isRabid) { ++ getHandle().setRabid(isRabid); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 6a52ae70b5f7fd9953b6b2605cae722f606e7fec..b65c39645aa437fdb1ac745ec18bba11f63f092d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -545,6 +545,15 @@ public class CraftEventFactory { + // Paper end + craftServer.getPluginManager().callEvent(event); + ++ // Purpur start ++ if (who != null) { ++ switch (action) { ++ case LEFT_CLICK_BLOCK, LEFT_CLICK_AIR -> who.processClick(InteractionHand.MAIN_HAND); ++ case RIGHT_CLICK_BLOCK, RIGHT_CLICK_AIR -> who.processClick(InteractionHand.OFF_HAND); ++ } ++ } ++ // Purpur end ++ + return event; + } + +@@ -982,6 +991,7 @@ public class CraftEventFactory { + damageCause = DamageCause.ENTITY_EXPLOSION; + } + event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), entity.getBukkitEntity(), damageCause, modifiers, modifierFunctions, source.isCritical()); // Paper - add critical damage API ++ damager.processClick(InteractionHand.MAIN_HAND); // Purpur + } + event.setCancelled(cancelled); + +@@ -1047,6 +1057,10 @@ public class CraftEventFactory { + cause = DamageCause.MAGIC; + } else if (source == DamageSource.IN_FIRE) { + cause = DamageCause.FIRE; ++ // Purpur start ++ } else if (source == DamageSource.STONECUTTER) { ++ cause = DamageCause.CONTACT; ++ // Purpur end + } else { + throw new IllegalStateException(String.format("Unhandled damage of %s by %s from %s", entity, damager, source.msgId)); + } +@@ -1088,6 +1102,7 @@ public class CraftEventFactory { + } else { + entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled + } ++ damager.getHandle().processClick(InteractionHand.MAIN_HAND); // Purpur + return event; + } + +@@ -1122,6 +1137,10 @@ public class CraftEventFactory { + cause = DamageCause.FREEZE; + } else if (source == DamageSource.GENERIC) { + cause = DamageCause.CUSTOM; ++ // Purpur start ++ } else if (source == DamageSource.SCISSORS) { ++ cause = DamageCause.SUICIDE; ++ // Purpur end + } + + if (cause != null) { +@@ -1147,6 +1166,7 @@ public class CraftEventFactory { + EntityDamageEvent event; + if (damager != null) { + event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), damagee.getBukkitEntity(), cause, modifiers, modifierFunctions, critical); // Paper - add critical damage API ++ damager.processClick(InteractionHand.MAIN_HAND); // Purpur + } else { + event = new EntityDamageEvent(damagee.getBukkitEntity(), cause, modifiers, modifierFunctions); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +index 1f73834043c2d2be17ae647589653d517db36a1b..39f03d0b74b9bfc2eb62d95f2975bcd15bb25bc2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +@@ -164,8 +164,19 @@ public class CraftContainer extends AbstractContainerMenu { + case PLAYER: + case CHEST: + case ENDER_CHEST: ++ // Purpur start ++ this.delegate = new ChestMenu(org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? MenuType.GENERIC_9x6 : MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9); ++ break; + case BARREL: +- this.delegate = new ChestMenu(MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9); ++ this.delegate = new ChestMenu(switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> MenuType.GENERIC_9x6; ++ case 5 -> MenuType.GENERIC_9x5; ++ case 4 -> MenuType.GENERIC_9x4; ++ case 2 -> MenuType.GENERIC_9x2; ++ case 1 -> MenuType.GENERIC_9x1; ++ default -> MenuType.GENERIC_9x3; ++ }, windowId, bottom, top, top.getContainerSize() / 9); ++ // Purpur end + break; + case DISPENSER: + case DROPPER: +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java +index 59457378820d7f2899254a6aeef4c30c926ce543..b280d42a0298c04647945cde7bd5a4f5766c301b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java +@@ -82,7 +82,7 @@ public class CraftInventory implements Inventory { + + @Override + public void setContents(ItemStack[] items) { +- if (this.getSize() < items.length) { ++ if (false && this.getSize() < items.length) { // Purpur + throw new IllegalArgumentException("Invalid inventory size; expected " + this.getSize() + " or less"); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java +index 88d3ca586ff6905f18a8ab9f0e229f440ed44088..27dd4eb4781a3c75772860c11db886e1038cecd2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java +@@ -9,7 +9,7 @@ import org.bukkit.inventory.AnvilInventory; + public class CraftInventoryAnvil extends CraftResultInventory implements AnvilInventory { + + private final Location location; +- private final AnvilMenu container; ++ public final AnvilMenu container; // Purpur - private -> public + + public CraftInventoryAnvil(Location location, Container inventory, Container resultInventory, AnvilMenu container) { + super(inventory, resultInventory); +@@ -57,4 +57,26 @@ public class CraftInventoryAnvil extends CraftResultInventory implements AnvilIn + Preconditions.checkArgument(levels >= 0, "Maximum repair cost must be positive (or 0)"); + container.maximumRepairCost = levels; + } ++ ++ // Purpur start ++ @Override ++ public boolean canBypassCost() { ++ return container.bypassCost; ++ } ++ ++ @Override ++ public void setBypassCost(boolean bypassCost) { ++ container.bypassCost = bypassCost; ++ } ++ ++ @Override ++ public boolean canDoUnsafeEnchants() { ++ return container.canDoUnsafeEnchants; ++ } ++ ++ @Override ++ public void setDoUnsafeEnchants(boolean canDoUnsafeEnchants) { ++ container.canDoUnsafeEnchants = canDoUnsafeEnchants; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java +index 298326115f048bb79e3c949861c187134eb5efb8..9b060070ddbbade4c1b6830b9b861f462d60b138 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java +@@ -13,6 +13,7 @@ import net.minecraft.nbt.ListTag; + import org.apache.commons.lang.Validate; + import org.bukkit.Color; + import org.bukkit.Material; ++import org.bukkit.NamespacedKey; + import org.bukkit.configuration.serialization.DelegateDeserialization; + import org.bukkit.craftbukkit.inventory.CraftMetaItem.SerializableMeta; + import org.bukkit.craftbukkit.potion.CraftPotionUtil; +@@ -42,6 +43,7 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + static final ItemMetaKey POTION_COLOR = new ItemMetaKey("CustomPotionColor", "custom-color"); + static final ItemMetaKey ID = new ItemMetaKey("Id", "potion-id"); + static final ItemMetaKey DEFAULT_POTION = new ItemMetaKey("Potion", "potion-type"); ++ static final ItemMetaKey KEY = new ItemMetaKey("Key", "namespacedkey"); // Purpur - add key + + // Having an initial "state" in ItemMeta seems bit dirty but the UNCRAFTABLE potion type + // is treated as the empty form of the meta because it represents an empty potion with no effect +@@ -92,7 +94,13 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + boolean ambient = effect.getBoolean(AMBIENT.NBT); + boolean particles = tag.contains(SHOW_PARTICLES.NBT, CraftMagicNumbers.NBT.TAG_BYTE) ? effect.getBoolean(SHOW_PARTICLES.NBT) : true; + boolean icon = tag.contains(SHOW_ICON.NBT, CraftMagicNumbers.NBT.TAG_BYTE) ? effect.getBoolean(SHOW_ICON.NBT) : particles; +- this.customEffects.add(new PotionEffect(type, duration, amp, ambient, particles, icon)); ++ // Purpur start ++ NamespacedKey key = null; ++ if (tag.contains(KEY.NBT)) { ++ key = NamespacedKey.fromString(effect.getString(KEY.NBT)); ++ } ++ this.customEffects.add(new PotionEffect(type, duration, amp, ambient, particles, icon, key)); ++ // Purpur end + } + } + } +@@ -141,6 +149,11 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + effectData.putBoolean(AMBIENT.NBT, effect.isAmbient()); + effectData.putBoolean(SHOW_PARTICLES.NBT, effect.hasParticles()); + effectData.putBoolean(SHOW_ICON.NBT, effect.hasIcon()); ++ // Purpur start ++ if (effect.hasKey()) { ++ effectData.putString(KEY.NBT, effect.getKey().toString()); ++ } ++ // Purpur end + effectList.add(effectData); + } + } +@@ -202,7 +215,7 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + if (index != -1) { + if (overwrite) { + PotionEffect old = this.customEffects.get(index); +- if (old.getAmplifier() == effect.getAmplifier() && old.getDuration() == effect.getDuration() && old.isAmbient() == effect.isAmbient()) { ++ if (old.getAmplifier() == effect.getAmplifier() && old.getDuration() == effect.getDuration() && old.isAmbient() == effect.isAmbient() && old.getKey() == effect.getKey()) { // Purpur - add key + return false; + } + this.customEffects.set(index, effect); +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +index 8563fcf77eef0e1e354857b9a4256d78a523a8d0..b94c964790433cb7bd88ad16c5d82d1a8e39312d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +@@ -29,6 +29,7 @@ public interface CraftRecipe extends Recipe { + } else if (bukkit instanceof RecipeChoice.ExactChoice) { + stack = new Ingredient(((RecipeChoice.ExactChoice) bukkit).getChoices().stream().map((mat) -> new net.minecraft.world.item.crafting.Ingredient.ItemValue(CraftItemStack.asNMSCopy(mat)))); + stack.exact = true; ++ stack.predicate = ((RecipeChoice.ExactChoice) bukkit).getPredicate(); // Purpur + } else { + throw new IllegalArgumentException("Unknown recipe stack instance " + bukkit); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java +index 110503062b3043cffa082a1cda6b8d57152869aa..3e7e06bd5e9e4ed45c9e3452eb04e946fac817d8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java ++++ b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java +@@ -256,6 +256,7 @@ public final class CraftLegacy { + } + + static { ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressInitLegacyMaterialError) // Purpur + LOGGER.warn("Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug!"); // Paper - doesn't need to be an error + if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isDebugging()) { + new Exception().printStackTrace(); +diff --git a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java +index acb69821a99aa69bce6d127e10976089c85be223..c5abd73981c5f4b41605eba0d44e6573dfd2a77a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java ++++ b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java +@@ -101,7 +101,7 @@ public class CraftPotionUtil { + + public static MobEffectInstance fromBukkit(PotionEffect effect) { + MobEffect type = MobEffect.byId(effect.getType().getId()); +- return new MobEffectInstance(type, effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles()); ++ return new MobEffectInstance(type, effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles(), effect.getKey()); // Purpur - add key + } + + public static PotionEffect toBukkit(MobEffectInstance effect) { +@@ -110,7 +110,7 @@ public class CraftPotionUtil { + int duration = effect.getDuration(); + boolean ambient = effect.isAmbient(); + boolean particles = effect.isVisible(); +- return new PotionEffect(type, duration, amp, ambient, particles); ++ return new PotionEffect(type, duration, amp, ambient, particles, effect.getKey()); // Purpur - add key + } + + public static boolean equals(MobEffect mobEffect, PotionEffectType type) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index cdefb2025eedea7e204d70d568adaf1c1ec4c03c..5402098dce0d64d3dceea51f248d7d366850a74f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -504,7 +504,7 @@ public class CraftScheduler implements BukkitScheduler { + this.parsePending(); + } else { + //this.debugTail = this.debugTail.setNext(new CraftAsyncDebugger(currentTick + CraftScheduler.RECENT_TICKS, task.getOwner(), task.getTaskClass())); // Paper +- task.getOwner().getLogger().log(Level.SEVERE, "Unexpected Async Task in the Sync Scheduler. Report this to Paper"); // Paper ++ task.getOwner().getLogger().log(Level.SEVERE, "Unexpected Async Task in the Sync Scheduler. Report this to Purpur"); // Paper // Purpur + // We don't need to parse pending + // (async tasks must live with race-conditions if they attempt to cancel between these few lines of code) + } +@@ -516,10 +516,10 @@ public class CraftScheduler implements BukkitScheduler { + this.runners.remove(task.getTaskId()); + } + } +- MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper ++ //MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper // Purpur + this.pending.addAll(temp); + temp.clear(); +- MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper ++ //MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper // Purpur + //this.debugHead = this.debugHead.getNextHead(currentTick); // Paper + } + +@@ -563,7 +563,7 @@ public class CraftScheduler implements BukkitScheduler { + } + + void parsePending() { // Paper +- if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); // Paper ++ //if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); // Paper // Purpur + CraftTask head = this.head; + CraftTask task = head.getNext(); + CraftTask lastTask = head; +@@ -582,7 +582,7 @@ public class CraftScheduler implements BukkitScheduler { + task.setNext(null); + } + this.head = lastTask; +- if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); // Paper ++ //if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); // Paper // Purpur + } + + private boolean isReady(final int currentTick) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +index 3f45bab0e9f7b3697e6d9d1092a1e6e579f7066f..4f1cf281c4bf68c37982d390da8779dea78dab18 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +@@ -96,13 +96,13 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + + @Override + public void run() { +- try (Timing ignored = timings.startTiming()) { // Paper ++ //try (Timing ignored = timings.startTiming()) { // Paper // Purpur + if (this.rTask != null) { + this.rTask.run(); + } else { + this.cTask.accept(this); + } +- } // Paper ++ //} // Paper // Purpur + } + + long getCreatedAt() { +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +index 138407c2d4b0bc55ddb9aac5d2aa3edadda090fb..a6e9e503a496c18e2501b03ec84f4600c134a50c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +@@ -115,7 +115,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + public void getScoreboardScores(ObjectiveCriteria criteria, String name, Consumer consumer) { + // Paper start - add timings for scoreboard search + // plugins leaking scoreboards will make this very expensive, let server owners debug it easily +- co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); ++ //co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); // Purpur + try { + // Paper end - add timings for scoreboard search + for (CraftScoreboard scoreboard : this.scoreboards) { +@@ -123,7 +123,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + board.forAllObjectives(criteria, name, (score) -> consumer.accept(score)); + } + } finally { // Paper start - add timings for scoreboard search +- co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); ++ //co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); // Purpur + } + // Paper end - add timings for scoreboard search + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 6aa2121e286dd6d43201a38722ea0cdd205baaa7..aa65fb409f52cf79b2f532e5cd1fb11bef753180 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -468,7 +468,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + + @Override + public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { +- return new gg.pufferfish.pufferfish.PufferfishVersionFetcher(); // Pufferfish ++ return new com.destroystokyo.paper.PaperVersionFetcher(); // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index 80553face9c70c2a3d897681e7761df85b22d464..fb87620c742ff7912f5e8ccd2a7930dd605576d9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +@@ -11,7 +11,7 @@ public final class Versioning { + public static String getBukkitVersion() { + String result = "Unknown-Version"; + +- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/gg.pufferfish.pufferfish/pufferfish-api/pom.properties"); // Pufferfish ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/org.purpurmc.purpur/purpur-api/pom.properties"); // Purpur + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +index 0f8c5fad3c999da15c5c22b4baed275cf396a5d2..31ef9cd39ce5d9f0a6cec261dfbc4ff3274d8e03 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +@@ -23,7 +23,15 @@ public final class CommandPermissions { + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "kick", "Allows the user to kick players", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "stop", "Allows the user to stop the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "list", "Allows the user to list all online players", PermissionDefault.OP, commands); +- DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "gamemode", "Allows the user to change the gamemode of another player", PermissionDefault.OP, commands); ++ // Purpur start ++ Permission gamemodeVanilla = DefaultPermissions.registerPermission(PREFIX + "gamemode", "Allows the user to change the gamemode", PermissionDefault.OP, commands); ++ for (net.minecraft.world.level.GameType gametype : net.minecraft.world.level.GameType.values()) { ++ Permission gamemodeSelf = DefaultPermissions.registerPermission(PREFIX + "gamemode." + gametype.getName(), "Allows the user to set " + gametype.getName() + " gamemode for self", PermissionDefault.OP); ++ Permission gamemodeOther = DefaultPermissions.registerPermission(PREFIX + "gamemode." + gametype.getName() + ".other", "Allows the user to set " + gametype.getName() + " gamemode for other players", PermissionDefault.OP); ++ gamemodeSelf.addParent(gamemodeOther, true); ++ gamemodeVanilla.addParent(gamemodeSelf, true); ++ } ++ // Purpur end + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "xp", "Allows the user to give themselves or others arbitrary values of experience", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "defaultgamemode", "Allows the user to change the default gamemode of the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "seed", "Allows the user to view the seed of the world", PermissionDefault.OP, commands); +diff --git a/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c7c0ed8dfe58c841faf684a1fe228eeda6cd57b7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +@@ -0,0 +1,633 @@ ++package org.purpurmc.purpur; ++ ++import co.aikar.timings.TimingsManager; ++import com.google.common.base.Throwables; ++import com.google.common.collect.ImmutableMap; ++import com.mojang.datafixers.util.Pair; ++import net.kyori.adventure.bossbar.BossBar; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.effect.MobEffect; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.entity.EntityDimensions; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.food.FoodProperties; ++import net.minecraft.world.food.Foods; ++import net.minecraft.world.item.enchantment.Enchantment; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import org.bukkit.Bukkit; ++import org.bukkit.command.Command; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.InvalidConfigurationException; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.purpurmc.purpur.command.PurpurCommand; ++import org.purpurmc.purpur.task.TPSBarTask; ++ ++import java.io.File; ++import java.io.IOException; ++import java.lang.reflect.InvocationTargetException; ++import java.lang.reflect.Method; ++import java.lang.reflect.Modifier; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.logging.Level; ++ ++@SuppressWarnings("unused") ++public class PurpurConfig { ++ private static final String HEADER = "This is the main configuration file for Purpur.\n" ++ + "As you can see, there's tons to configure. Some options may impact gameplay, so use\n" ++ + "with caution, and make sure you know what each option does before configuring.\n" ++ + "\n" ++ + "If you need help with the configuration or have any questions related to Purpur,\n" ++ + "join us in our Discord guild.\n" ++ + "\n" ++ + "Website: https://purpurmc.org \n" ++ + "Docs: https://purpurmc.org/docs \n"; ++ private static File CONFIG_FILE; ++ public static YamlConfiguration config; ++ ++ private static Map commands; ++ ++ public static int version; ++ static boolean verbose; ++ ++ public static void init(File configFile) { ++ CONFIG_FILE = configFile; ++ config = new YamlConfiguration(); ++ try { ++ config.load(CONFIG_FILE); ++ } catch (IOException ignore) { ++ } catch (InvalidConfigurationException ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not load purpur.yml, please correct your syntax errors", ex); ++ throw Throwables.propagate(ex); ++ } ++ config.options().header(HEADER); ++ config.options().copyDefaults(true); ++ verbose = getBoolean("verbose", false); ++ ++ commands = new HashMap<>(); ++ commands.put("purpur", new PurpurCommand("purpur")); ++ ++ version = getInt("config-version", 31); ++ set("config-version", 31); ++ ++ readConfig(PurpurConfig.class, null); ++ ++ Blocks.rebuildCache(); ++ } ++ ++ protected static void log(String s) { ++ if (verbose) { ++ log(Level.INFO, s); ++ } ++ } ++ ++ protected static void log(Level level, String s) { ++ Bukkit.getLogger().log(level, s); ++ } ++ ++ public static void registerCommands() { ++ for (Map.Entry entry : commands.entrySet()) { ++ MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Purpur", entry.getValue()); ++ } ++ } ++ ++ static void readConfig(Class clazz, Object instance) { ++ for (Method method : clazz.getDeclaredMethods()) { ++ if (Modifier.isPrivate(method.getModifiers())) { ++ if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) { ++ try { ++ method.setAccessible(true); ++ method.invoke(instance); ++ } catch (InvocationTargetException ex) { ++ throw Throwables.propagate(ex.getCause()); ++ } catch (Exception ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Error invoking " + method, ex); ++ } ++ } ++ } ++ } ++ ++ try { ++ config.save(CONFIG_FILE); ++ } catch (IOException ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not save " + CONFIG_FILE, ex); ++ } ++ } ++ ++ private static void set(String path, Object val) { ++ config.addDefault(path, val); ++ config.set(path, val); ++ } ++ ++ private static String getString(String path, String def) { ++ config.addDefault(path, def); ++ return config.getString(path, config.getString(path)); ++ } ++ ++ private static boolean getBoolean(String path, boolean def) { ++ config.addDefault(path, def); ++ return config.getBoolean(path, config.getBoolean(path)); ++ } ++ ++ private static double getDouble(String path, double def) { ++ config.addDefault(path, def); ++ return config.getDouble(path, config.getDouble(path)); ++ } ++ ++ private static int getInt(String path, int def) { ++ config.addDefault(path, def); ++ return config.getInt(path, config.getInt(path)); ++ } ++ ++ private static List getList(String path, T def) { ++ config.addDefault(path, def); ++ return config.getList(path, config.getList(path)); ++ } ++ ++ static Map getMap(String path, Map def) { ++ if (def != null && config.getConfigurationSection(path) == null) { ++ config.addDefault(path, def); ++ return def; ++ } ++ return toMap(config.getConfigurationSection(path)); ++ } ++ ++ private static Map toMap(ConfigurationSection section) { ++ ImmutableMap.Builder builder = ImmutableMap.builder(); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ Object obj = section.get(key); ++ if (obj != null) { ++ builder.put(key, obj instanceof ConfigurationSection val ? toMap(val) : obj); ++ } ++ } ++ } ++ return builder.build(); ++ } ++ ++ public static String cannotRideMob = "You cannot mount that mob"; ++ public static String afkBroadcastAway = "%s is now AFK"; ++ public static String afkBroadcastBack = "%s is no longer AFK"; ++ public static String afkTabListPrefix = "[AFK] "; ++ public static String afkTabListSuffix = ""; ++ public static String creditsCommandOutput = "%s has been shown the end credits"; ++ public static String demoCommandOutput = "%s has been shown the demo screen"; ++ public static String pingCommandOutput = "%s's ping is %sms"; ++ public static String ramCommandOutput = "Ram Usage: / ()"; ++ public static String rambarCommandOutput = "Rambar toggled for "; ++ public static String tpsbarCommandOutput = "Tpsbar toggled for "; ++ public static String dontRunWithScissors = "Don't run with scissors!"; ++ public static String uptimeCommandOutput = "Server uptime is "; ++ public static String unverifiedUsername = "default"; ++ public static String sleepSkippingNight = "default"; ++ public static String sleepingPlayersPercent = "default"; ++ private static void messages() { ++ cannotRideMob = getString("settings.messages.cannot-ride-mob", cannotRideMob); ++ afkBroadcastAway = getString("settings.messages.afk-broadcast-away", afkBroadcastAway); ++ afkBroadcastBack = getString("settings.messages.afk-broadcast-back", afkBroadcastBack); ++ afkTabListPrefix = getString("settings.messages.afk-tab-list-prefix", afkTabListPrefix); ++ afkTabListSuffix = getString("settings.messages.afk-tab-list-suffix", afkTabListSuffix); ++ creditsCommandOutput = getString("settings.messages.credits-command-output", creditsCommandOutput); ++ demoCommandOutput = getString("settings.messages.demo-command-output", demoCommandOutput); ++ pingCommandOutput = getString("settings.messages.ping-command-output", pingCommandOutput); ++ ramCommandOutput = getString("settings.messages.ram-command-output", ramCommandOutput); ++ rambarCommandOutput = getString("settings.messages.rambar-command-output", rambarCommandOutput); ++ tpsbarCommandOutput = getString("settings.messages.tpsbar-command-output", tpsbarCommandOutput); ++ dontRunWithScissors = getString("settings.messages.dont-run-with-scissors", dontRunWithScissors); ++ uptimeCommandOutput = getString("settings.messages.uptime-command-output", uptimeCommandOutput); ++ unverifiedUsername = getString("settings.messages.unverified-username", unverifiedUsername); ++ sleepSkippingNight = getString("settings.messages.sleep-skipping-night", sleepSkippingNight); ++ sleepingPlayersPercent = getString("settings.messages.sleeping-players-percent", sleepingPlayersPercent); ++ } ++ ++ public static String deathMsgRunWithScissors = " slipped and fell on their shears"; ++ public static String deathMsgStonecutter = " has sawed themself in half"; ++ private static void deathMessages() { ++ deathMsgRunWithScissors = getString("settings.messages.death-message.run-with-scissors", deathMsgRunWithScissors); ++ deathMsgStonecutter = getString("settings.messages.death-message.stonecutter", deathMsgStonecutter); ++ } ++ ++ public static boolean advancementOnlyBroadcastToAffectedPlayer = false; ++ public static boolean deathMessageOnlyBroadcastToAffectedPlayer = false; ++ private static void broadcastSettings() { ++ if (version < 13) { ++ boolean oldValue = getBoolean("settings.advancement.only-broadcast-to-affected-player", false); ++ set("settings.broadcasts.advancement.only-broadcast-to-affected-player", oldValue); ++ set("settings.advancement.only-broadcast-to-affected-player", null); ++ } ++ advancementOnlyBroadcastToAffectedPlayer = getBoolean("settings.broadcasts.advancement.only-broadcast-to-affected-player", advancementOnlyBroadcastToAffectedPlayer); ++ deathMessageOnlyBroadcastToAffectedPlayer = getBoolean("settings.broadcasts.death.only-broadcast-to-affected-player", deathMessageOnlyBroadcastToAffectedPlayer); ++ } ++ ++ public static String serverModName = "Purpur"; ++ private static void serverModName() { ++ serverModName = getString("settings.server-mod-name", serverModName); ++ } ++ ++ public static double laggingThreshold = 19.0D; ++ private static void tickLoopSettings() { ++ laggingThreshold = getDouble("settings.lagging-threshold", laggingThreshold); ++ } ++ ++ public static boolean useAlternateKeepAlive = false; ++ private static void useAlternateKeepAlive() { ++ useAlternateKeepAlive = getBoolean("settings.use-alternate-keepalive", useAlternateKeepAlive); ++ } ++ ++ public static boolean disableGiveCommandDrops = false; ++ private static void disableGiveCommandDrops() { ++ disableGiveCommandDrops = getBoolean("settings.disable-give-dropping", disableGiveCommandDrops); ++ } ++ ++ public static String commandRamBarTitle = "Ram: / ()"; ++ public static BossBar.Overlay commandRamBarProgressOverlay = BossBar.Overlay.NOTCHED_20; ++ public static BossBar.Color commandRamBarProgressColorGood = BossBar.Color.GREEN; ++ public static BossBar.Color commandRamBarProgressColorMedium = BossBar.Color.YELLOW; ++ public static BossBar.Color commandRamBarProgressColorLow = BossBar.Color.RED; ++ public static String commandRamBarTextColorGood = ""; ++ public static String commandRamBarTextColorMedium = ""; ++ public static String commandRamBarTextColorLow = ""; ++ public static int commandRamBarTickInterval = 20; ++ public static String commandTPSBarTitle = "TPS: MSPT: Ping: ms"; ++ public static BossBar.Overlay commandTPSBarProgressOverlay = BossBar.Overlay.NOTCHED_20; ++ public static TPSBarTask.FillMode commandTPSBarProgressFillMode = TPSBarTask.FillMode.MSPT; ++ public static BossBar.Color commandTPSBarProgressColorGood = BossBar.Color.GREEN; ++ public static BossBar.Color commandTPSBarProgressColorMedium = BossBar.Color.YELLOW; ++ public static BossBar.Color commandTPSBarProgressColorLow = BossBar.Color.RED; ++ public static String commandTPSBarTextColorGood = ""; ++ public static String commandTPSBarTextColorMedium = ""; ++ public static String commandTPSBarTextColorLow = ""; ++ public static int commandTPSBarTickInterval = 20; ++ public static String commandCompassBarTitle = "S \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 W \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 N \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 E \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 S \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 W \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 N \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 E \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 "; ++ public static BossBar.Overlay commandCompassBarProgressOverlay = BossBar.Overlay.PROGRESS; ++ public static BossBar.Color commandCompassBarProgressColor = BossBar.Color.BLUE; ++ public static float commandCompassBarProgressPercent = 1.0F; ++ public static int commandCompassBarTickInterval = 5; ++ public static boolean commandGamemodeRequiresPermission = false; ++ public static int commandFillMaxArea = 32768; ++ public static boolean hideHiddenPlayersFromEntitySelector = false; ++ public static String uptimeFormat = ""; ++ public static String uptimeDay = "%02d day, "; ++ public static String uptimeDays = "%02d days, "; ++ public static String uptimeHour = "%02d hour, "; ++ public static String uptimeHours = "%02d hours, "; ++ public static String uptimeMinute = "%02d minute, and "; ++ public static String uptimeMinutes = "%02d minutes, and "; ++ public static String uptimeSecond = "%02d second"; ++ public static String uptimeSeconds = "%02d seconds"; ++ private static void commandSettings() { ++ commandRamBarTitle = getString("settings.command.rambar.title", commandRamBarTitle); ++ commandRamBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.rambar.overlay", commandRamBarProgressOverlay.name())); ++ commandRamBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.good", commandRamBarProgressColorGood.name())); ++ commandRamBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.medium", commandRamBarProgressColorMedium.name())); ++ commandRamBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.low", commandRamBarProgressColorLow.name())); ++ commandRamBarTextColorGood = getString("settings.command.rambar.text-color.good", commandRamBarTextColorGood); ++ commandRamBarTextColorMedium = getString("settings.command.rambar.text-color.medium", commandRamBarTextColorMedium); ++ commandRamBarTextColorLow = getString("settings.command.rambar.text-color.low", commandRamBarTextColorLow); ++ commandRamBarTickInterval = getInt("settings.command.rambar.tick-interval", commandRamBarTickInterval); ++ ++ commandTPSBarTitle = getString("settings.command.tpsbar.title", commandTPSBarTitle); ++ commandTPSBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.tpsbar.overlay", commandTPSBarProgressOverlay.name())); ++ commandTPSBarProgressFillMode = TPSBarTask.FillMode.valueOf(getString("settings.command.tpsbar.fill-mode", commandTPSBarProgressFillMode.name())); ++ commandTPSBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.good", commandTPSBarProgressColorGood.name())); ++ commandTPSBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.medium", commandTPSBarProgressColorMedium.name())); ++ commandTPSBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.low", commandTPSBarProgressColorLow.name())); ++ commandTPSBarTextColorGood = getString("settings.command.tpsbar.text-color.good", commandTPSBarTextColorGood); ++ commandTPSBarTextColorMedium = getString("settings.command.tpsbar.text-color.medium", commandTPSBarTextColorMedium); ++ commandTPSBarTextColorLow = getString("settings.command.tpsbar.text-color.low", commandTPSBarTextColorLow); ++ commandTPSBarTickInterval = getInt("settings.command.tpsbar.tick-interval", commandTPSBarTickInterval); ++ ++ commandCompassBarTitle = getString("settings.command.compass.title", commandCompassBarTitle); ++ commandCompassBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.compass.overlay", commandCompassBarProgressOverlay.name())); ++ commandCompassBarProgressColor = BossBar.Color.valueOf(getString("settings.command.compass.progress-color", commandCompassBarProgressColor.name())); ++ commandCompassBarProgressPercent = (float) getDouble("settings.command.compass.percent", commandCompassBarProgressPercent); ++ commandCompassBarTickInterval = getInt("settings.command.compass.tick-interval", commandCompassBarTickInterval); ++ ++ commandGamemodeRequiresPermission = getBoolean("settings.command.gamemode.requires-specific-permission", commandGamemodeRequiresPermission); ++ commandFillMaxArea = getInt("settings.command.fill.max-area", commandFillMaxArea); ++ hideHiddenPlayersFromEntitySelector = getBoolean("settings.command.hide-hidden-players-from-entity-selector", hideHiddenPlayersFromEntitySelector); ++ uptimeFormat = getString("settings.command.uptime.format", uptimeFormat); ++ uptimeDay = getString("settings.command.uptime.day", uptimeDay); ++ uptimeDays = getString("settings.command.uptime.days", uptimeDays); ++ uptimeHour = getString("settings.command.uptime.hour", uptimeHour); ++ uptimeHours = getString("settings.command.uptime.hours", uptimeHours); ++ uptimeMinute = getString("settings.command.uptime.minute", uptimeMinute); ++ uptimeMinutes = getString("settings.command.uptime.minutes", uptimeMinutes); ++ uptimeSecond = getString("settings.command.uptime.second", uptimeSecond); ++ uptimeSeconds = getString("settings.command.uptime.seconds", uptimeSeconds); ++ } ++ ++ public static int barrelRows = 3; ++ public static boolean enderChestSixRows = false; ++ public static boolean enderChestPermissionRows = false; ++ public static boolean cryingObsidianValidForPortalFrame = false; ++ public static int beeInsideBeeHive = 3; ++ public static boolean anvilCumulativeCost = true; ++ public static int lightningRodRange = 128; ++ public static Set grindstoneIgnoredEnchants = new HashSet<>(); ++ public static boolean grindstoneRemoveAttributes = false; ++ public static boolean grindstoneRemoveDisplay = false; ++ public static int caveVinesMaxGrowthAge = 25; ++ public static int kelpMaxGrowthAge = 25; ++ public static int twistingVinesMaxGrowthAge = 25; ++ public static int weepingVinesMaxGrowthAge = 25; ++ private static void blockSettings() { ++ if (version < 3) { ++ boolean oldValue = getBoolean("settings.barrel.packed-barrels", true); ++ set("settings.blocks.barrel.six-rows", oldValue); ++ set("settings.packed-barrels", null); ++ oldValue = getBoolean("settings.large-ender-chests", true); ++ set("settings.blocks.ender_chest.six-rows", oldValue); ++ set("settings.large-ender-chests", null); ++ } ++ if (version < 20) { ++ boolean oldValue = getBoolean("settings.blocks.barrel.six-rows", false); ++ set("settings.blocks.barrel.rows", oldValue ? 6 : 3); ++ set("settings.blocks.barrel.six-rows", null); ++ } ++ barrelRows = getInt("settings.blocks.barrel.rows", barrelRows); ++ if (barrelRows < 1 || barrelRows > 6) { ++ Bukkit.getLogger().severe("settings.blocks.barrel.rows must be 1-6, resetting to default"); ++ barrelRows = 3; ++ } ++ org.bukkit.event.inventory.InventoryType.BARREL.setDefaultSize(switch (barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }); ++ enderChestSixRows = getBoolean("settings.blocks.ender_chest.six-rows", enderChestSixRows); ++ org.bukkit.event.inventory.InventoryType.ENDER_CHEST.setDefaultSize(enderChestSixRows ? 54 : 27); ++ enderChestPermissionRows = getBoolean("settings.blocks.ender_chest.use-permissions-for-rows", enderChestPermissionRows); ++ cryingObsidianValidForPortalFrame = getBoolean("settings.blocks.crying_obsidian.valid-for-portal-frame", cryingObsidianValidForPortalFrame); ++ beeInsideBeeHive = getInt("settings.blocks.beehive.max-bees-inside", beeInsideBeeHive); ++ anvilCumulativeCost = getBoolean("settings.blocks.anvil.cumulative-cost", anvilCumulativeCost); ++ lightningRodRange = getInt("settings.blocks.lightning_rod.range", lightningRodRange); ++ ArrayList defaultCurses = new ArrayList<>(){{ ++ add("minecraft:binding_curse"); ++ add("minecraft:vanishing_curse"); ++ }}; ++ if (version < 24 && !getBoolean("settings.blocks.grindstone.ignore-curses", true)) { ++ defaultCurses.clear(); ++ } ++ getList("settings.blocks.grindstone.ignored-enchants", defaultCurses).forEach(key -> { ++ Enchantment enchantment = BuiltInRegistries.ENCHANTMENT.get(new ResourceLocation(key.toString())); ++ grindstoneIgnoredEnchants.add(enchantment); ++ }); ++ grindstoneRemoveAttributes = getBoolean("settings.blocks.grindstone.remove-attributes", grindstoneRemoveAttributes); ++ grindstoneRemoveDisplay = getBoolean("settings.blocks.grindstone.remove-name-and-lore", grindstoneRemoveDisplay); ++ caveVinesMaxGrowthAge = getInt("settings.blocks.cave_vines.max-growth-age", caveVinesMaxGrowthAge); ++ if (caveVinesMaxGrowthAge > 25) { ++ caveVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.cave_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ kelpMaxGrowthAge = getInt("settings.blocks.kelp.max-growth-age", kelpMaxGrowthAge); ++ if (kelpMaxGrowthAge > 25) { ++ kelpMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.kelp.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ twistingVinesMaxGrowthAge = getInt("settings.blocks.twisting_vines.max-growth-age", twistingVinesMaxGrowthAge); ++ if (twistingVinesMaxGrowthAge > 25) { ++ twistingVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.twisting_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ weepingVinesMaxGrowthAge = getInt("settings.blocks.weeping_vines.max-growth-age", weepingVinesMaxGrowthAge); ++ if (weepingVinesMaxGrowthAge > 25) { ++ weepingVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.weeping_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ } ++ ++ public static boolean allowInfinityMending = false; ++ public static boolean allowCrossbowInfinity = false; ++ public static boolean allowShearsLooting = false; ++ public static boolean allowTransparentBlocksInEnchantmentBox = false; ++ public static boolean allowUnsafeEnchants = false; ++ public static boolean allowInapplicableEnchants = true; ++ public static boolean allowIncompatibleEnchants = true; ++ public static boolean allowHigherEnchantsLevels = true; ++ public static boolean allowUnsafeEnchantCommand = false; ++ public static boolean clampEnchantLevels = true; ++ private static void enchantmentSettings() { ++ if (version < 5) { ++ boolean oldValue = getBoolean("settings.enchantment.allow-infinite-and-mending-together", false); ++ set("settings.enchantment.allow-infinity-and-mending-together", oldValue); ++ set("settings.enchantment.allow-infinite-and-mending-together", null); ++ } ++ if (version < 30) { ++ boolean oldValue = getBoolean("settings.enchantment.allow-unsafe-enchants", false); ++ set("settings.enchantment.anvil.allow-unsafe-enchants", oldValue); ++ set("settings.enchantment.anvil.allow-inapplicable-enchants", true); ++ set("settings.enchantment.anvil.allow-incompatible-enchants", true); ++ set("settings.enchantment.anvil.allow-higher-enchants-levels", true); ++ set("settings.enchantment.allow-unsafe-enchants", null); ++ } ++ allowInfinityMending = getBoolean("settings.enchantment.allow-infinity-and-mending-together", allowInfinityMending); ++ allowCrossbowInfinity = getBoolean("settings.enchantment.allow-infinity-on-crossbow", allowCrossbowInfinity); ++ allowShearsLooting = getBoolean("settings.enchantment.allow-looting-on-shears", allowShearsLooting); ++ allowTransparentBlocksInEnchantmentBox = getBoolean("settings.enchantment.allow-transparent-blocks-in-enchantment-box", allowTransparentBlocksInEnchantmentBox); ++ allowUnsafeEnchants = getBoolean("settings.enchantment.anvil.allow-unsafe-enchants", allowUnsafeEnchants); ++ allowInapplicableEnchants = getBoolean("settings.enchantment.anvil.allow-inapplicable-enchants", allowInapplicableEnchants); ++ allowIncompatibleEnchants = getBoolean("settings.enchantment.anvil.allow-incompatible-enchants", allowIncompatibleEnchants); ++ allowHigherEnchantsLevels = getBoolean("settings.enchantment.anvil.allow-higher-enchants-levels", allowHigherEnchantsLevels); ++ allowUnsafeEnchantCommand = getBoolean("settings.enchantment.allow-unsafe-enchant-command", allowUnsafeEnchants); // allowUnsafeEnchants as default for backwards compatability ++ clampEnchantLevels = getBoolean("settings.enchantment.clamp-levels", clampEnchantLevels); ++ } ++ ++ public static boolean endermanShortHeight = false; ++ private static void entitySettings() { ++ endermanShortHeight = getBoolean("settings.entity.enderman.short-height", endermanShortHeight); ++ if (endermanShortHeight) EntityType.ENDERMAN.setDimensions(EntityDimensions.scalable(0.6F, 1.9F)); ++ } ++ ++ public static boolean allowWaterPlacementInTheEnd = true; ++ private static void allowWaterPlacementInEnd() { ++ allowWaterPlacementInTheEnd = getBoolean("settings.allow-water-placement-in-the-end", allowWaterPlacementInTheEnd); ++ } ++ ++ public static boolean disableMushroomBlockUpdates = false; ++ public static boolean disableNoteBlockUpdates = false; ++ public static boolean disableChorusPlantUpdates = false; ++ private static void blockUpdatesSettings() { ++ disableMushroomBlockUpdates = getBoolean("settings.blocks.disable-mushroom-updates", disableMushroomBlockUpdates); ++ disableNoteBlockUpdates = getBoolean("settings.blocks.disable-note-block-updates", disableNoteBlockUpdates); ++ disableChorusPlantUpdates = getBoolean("settings.blocks.disable-chorus-plant-updates", disableChorusPlantUpdates); ++ } ++ ++ public static boolean loggerSuppressInitLegacyMaterialError = false; ++ public static boolean loggerSuppressIgnoredAdvancementWarnings = false; ++ public static boolean loggerSuppressUnrecognizedRecipeErrors = false; ++ public static boolean loggerSuppressSetBlockFarChunk = false; ++ public static boolean loggerSuppressLibraryLoader = false; ++ private static void loggerSettings() { ++ loggerSuppressInitLegacyMaterialError = getBoolean("settings.logger.suppress-init-legacy-material-errors", loggerSuppressInitLegacyMaterialError); ++ loggerSuppressIgnoredAdvancementWarnings = getBoolean("settings.logger.suppress-ignored-advancement-warnings", loggerSuppressIgnoredAdvancementWarnings); ++ loggerSuppressUnrecognizedRecipeErrors = getBoolean("settings.logger.suppress-unrecognized-recipe-errors", loggerSuppressUnrecognizedRecipeErrors); ++ loggerSuppressSetBlockFarChunk = getBoolean("settings.logger.suppress-setblock-in-far-chunk-errors", loggerSuppressSetBlockFarChunk); ++ loggerSuppressLibraryLoader = getBoolean("settings.logger.suppress-library-loader", loggerSuppressLibraryLoader); ++ org.bukkit.plugin.java.JavaPluginLoader.SuppressLibraryLoaderLogger = loggerSuppressLibraryLoader; ++ } ++ ++ public static boolean tpsCatchup = true; ++ private static void tpsCatchup() { ++ tpsCatchup = getBoolean("settings.tps-catchup", tpsCatchup); ++ } ++ ++ public static boolean useUPnP = false; ++ public static boolean maxJoinsPerSecond = false; ++ public static boolean kickForOutOfOrderChat = true; ++ private static void networkSettings() { ++ useUPnP = getBoolean("settings.network.upnp-port-forwarding", useUPnP); ++ maxJoinsPerSecond = getBoolean("settings.network.max-joins-per-second", maxJoinsPerSecond); ++ kickForOutOfOrderChat = getBoolean("settings.network.kick-for-out-of-order-chat", kickForOutOfOrderChat); ++ } ++ ++ public static java.util.regex.Pattern usernameValidCharactersPattern; ++ private static void usernameValidationSettings() { ++ String defaultPattern = "^[a-zA-Z0-9_.]*$"; ++ String setPattern = getString("settings.username-valid-characters", defaultPattern); ++ usernameValidCharactersPattern = java.util.regex.Pattern.compile(setPattern == null || setPattern.isBlank() ? defaultPattern : setPattern); ++ } ++ ++ private static void foodSettings() { ++ ConfigurationSection properties = config.getConfigurationSection("settings.food-properties"); ++ if (properties == null) { ++ config.addDefault("settings.food-properties", new HashMap<>()); ++ return; ++ } ++ properties.getKeys(false).forEach(foodKey -> { ++ FoodProperties food = Foods.ALL_PROPERTIES.get(foodKey); ++ if (food == null) { ++ PurpurConfig.log(Level.SEVERE, "Invalid food property: " + foodKey); ++ return; ++ } ++ FoodProperties foodDefaults = Foods.DEFAULT_PROPERTIES.get(foodKey); ++ food.setNutrition(properties.getInt(foodKey + ".nutrition", foodDefaults.getNutrition())); ++ food.setSaturationModifier((float) properties.getDouble(foodKey + ".saturation-modifier", foodDefaults.getSaturationModifier())); ++ food.setIsMeat(properties.getBoolean(foodKey + ".is-meat", foodDefaults.isMeat())); ++ food.setCanAlwaysEat(properties.getBoolean(foodKey + ".can-always-eat", foodDefaults.canAlwaysEat())); ++ food.setFastFood(properties.getBoolean(foodKey + ".fast-food", foodDefaults.isFastFood())); ++ ConfigurationSection effects = properties.getConfigurationSection(foodKey + ".effects"); ++ if (effects != null) { ++ Map effectDefaults = new HashMap<>(); ++ foodDefaults.getEffects().forEach(pair -> { ++ effectDefaults.put("chance", pair.getSecond()); ++ MobEffectInstance effect = pair.getFirst(); ++ effectDefaults.put("duration", effect.getDuration()); ++ effectDefaults.put("amplifier", effect.getAmplifier()); ++ effectDefaults.put("ambient", effect.isAmbient()); ++ effectDefaults.put("visible", effect.isVisible()); ++ effectDefaults.put("show-icon", effect.showIcon()); ++ }); ++ effects.getKeys(false).forEach(effectKey -> { ++ MobEffect effect = BuiltInRegistries.MOB_EFFECT.get(new ResourceLocation(effectKey)); ++ if (effect == null) { ++ PurpurConfig.log(Level.SEVERE, "Invalid food property effect for " + foodKey + ": " + effectKey); ++ return; ++ } ++ food.getEffects().removeIf(pair -> pair.getFirst().getEffect() == effect); ++ float chance = (float) effects.getDouble(effectKey + ".chance", ((Float) effectDefaults.get("chance")).doubleValue()); ++ int duration = effects.getInt(effectKey + ".duration", (int) effectDefaults.get("duration")); ++ if (chance <= 0.0F || duration < 0) { ++ return; ++ } ++ int amplifier = effects.getInt(effectKey + ".amplifier", (int) effectDefaults.get("amplifier")); ++ boolean ambient = effects.getBoolean(effectKey + ".ambient", (boolean) effectDefaults.get("ambient")); ++ boolean visible = effects.getBoolean(effectKey + ".visible", (boolean) effectDefaults.get("visible")); ++ boolean showIcon = effects.getBoolean(effectKey + ".show-icon", (boolean) effectDefaults.get("show-icon")); ++ food.getEffects().add(Pair.of(new MobEffectInstance(effect, duration, amplifier, ambient, visible, showIcon), chance)); ++ }); ++ } ++ }); ++ } ++ ++ public static boolean fixProjectileLootingTransfer = false; ++ private static void fixProjectileLootingTransfer() { ++ fixProjectileLootingTransfer = getBoolean("settings.fix-projectile-looting-transfer", fixProjectileLootingTransfer); ++ } ++ ++ public static boolean clampAttributes = true; ++ private static void clampAttributes() { ++ clampAttributes = getBoolean("settings.clamp-attributes", clampAttributes); ++ } ++ ++ public static boolean limitArmor = true; ++ private static void limitArmor() { ++ limitArmor = getBoolean("settings.limit-armor", limitArmor); ++ } ++ ++ private static void blastResistanceSettings() { ++ getMap("settings.blast-resistance-overrides", Collections.emptyMap()).forEach((blockId, value) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { ++ log(Level.SEVERE, "Invalid block for `settings.blast-resistance-overrides`: " + blockId); ++ return; ++ } ++ if (!(value instanceof Number blastResistance)) { ++ log(Level.SEVERE, "Invalid blast resistance for `settings.blast-resistance-overrides." + blockId + "`: " + value); ++ return; ++ } ++ block.explosionResistance = blastResistance.floatValue(); ++ }); ++ } ++ private static void blockFallMultiplierSettings() { ++ getMap("settings.block-fall-multipliers", Map.ofEntries( ++ Map.entry("minecraft:hay_block", Map.of("damage", 0.2F)), ++ Map.entry("minecraft:white_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:light_gray_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:gray_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:black_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:brown_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:pink_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:red_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:orange_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:yellow_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:green_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:lime_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:cyan_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:light_blue_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:blue_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:purple_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:magenta_bed", Map.of("distance", 0.5F)) ++ )).forEach((blockId, value) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { ++ log(Level.SEVERE, "Invalid block for `settings.block-fall-multipliers`: " + blockId); ++ return; ++ } ++ if (!(value instanceof Map map)) { ++ log(Level.SEVERE, "Invalid fall multiplier for `settings.block-fall-multipliers." + blockId + "`: " + value ++ + ", expected a map with keys `damage` and `distance` to floats."); ++ return; ++ } ++ Object rawFallDamageMultiplier = map.get("damage"); ++ if (rawFallDamageMultiplier == null) rawFallDamageMultiplier = 1F; ++ if (!(rawFallDamageMultiplier instanceof Number fallDamageMultiplier)) { ++ log(Level.SEVERE, "Invalid multiplier for `settings.block-fall-multipliers." + blockId + ".damage`: " + map.get("damage")); ++ return; ++ } ++ Object rawFallDistanceMultiplier = map.get("distance"); ++ if (rawFallDistanceMultiplier == null) rawFallDistanceMultiplier = 1F; ++ if (!(rawFallDistanceMultiplier instanceof Number fallDistanceMultiplier)) { ++ log(Level.SEVERE, "Invalid multiplier for `settings.block-fall-multipliers." + blockId + ".distance`: " + map.get("distance")); ++ return; ++ } ++ block.fallDamageMultiplier = fallDamageMultiplier.floatValue(); ++ block.fallDistanceMultiplier = fallDistanceMultiplier.floatValue(); ++ }); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0c4742027217d5ae27843989ad18be93608496a +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +@@ -0,0 +1,3179 @@ ++package org.purpurmc.purpur; ++ ++//import gg.pufferfish.pufferfish.PufferfishConfig; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.item.DyeColor; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.Explosion; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.properties.Tilt; ++import org.purpurmc.purpur.entity.GlowSquidColor; ++import org.purpurmc.purpur.tool.Strippable; ++import org.purpurmc.purpur.tool.Tillable; ++import org.purpurmc.purpur.tool.Waxable; ++import org.purpurmc.purpur.tool.Weatherable; ++import org.apache.commons.lang.BooleanUtils; ++import org.bukkit.ChatColor; ++import org.bukkit.World; ++import org.bukkit.configuration.ConfigurationSection; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.function.Predicate; ++import java.util.logging.Level; ++import static org.purpurmc.purpur.PurpurConfig.log; ++ ++@SuppressWarnings("unused") ++public class PurpurWorldConfig { ++ ++ private final String worldName; ++ private final World.Environment environment; ++ ++ public PurpurWorldConfig(String worldName, World.Environment environment) { ++ this.worldName = worldName; ++ this.environment = environment; ++ init(); ++ } ++ ++ public void init() { ++ log("-------- World Settings For [" + worldName + "] --------"); ++ PurpurConfig.readConfig(PurpurWorldConfig.class, this); ++ } ++ ++ private void set(String path, Object val) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, val); ++ PurpurConfig.config.set("world-settings.default." + path, val); ++ if (PurpurConfig.config.get("world-settings." + worldName + "." + path) != null) { ++ PurpurConfig.config.addDefault("world-settings." + worldName + "." + path, val); ++ PurpurConfig.config.set("world-settings." + worldName + "." + path, val); ++ } ++ } ++ ++ private ConfigurationSection getConfigurationSection(String path) { ++ ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + "." + path); ++ return section != null ? section : PurpurConfig.config.getConfigurationSection("world-settings.default." + path); ++ } ++ ++ private String getString(String path, String def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getString("world-settings." + worldName + "." + path, PurpurConfig.config.getString("world-settings.default." + path)); ++ } ++ ++ private boolean getBoolean(String path, boolean def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getBoolean("world-settings." + worldName + "." + path, PurpurConfig.config.getBoolean("world-settings.default." + path)); ++ } ++ ++ private boolean getBoolean(String path, Predicate predicate) { ++ String val = getString(path, "default").toLowerCase(); ++ Boolean bool = BooleanUtils.toBooleanObject(val, "true", "false", "default"); ++ return predicate.test(bool); ++ } ++ ++ private double getDouble(String path, double def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getDouble("world-settings." + worldName + "." + path, PurpurConfig.config.getDouble("world-settings.default." + path)); ++ } ++ ++ private int getInt(String path, int def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getInt("world-settings." + worldName + "." + path, PurpurConfig.config.getInt("world-settings.default." + path)); ++ } ++ ++ private List getList(String path, T def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getList("world-settings." + worldName + "." + path, PurpurConfig.config.getList("world-settings.default." + path)); ++ } ++ ++ private Map getMap(String path, Map def) { ++ final Map fallback = PurpurConfig.getMap("world-settings.default." + path, def); ++ final Map value = PurpurConfig.getMap("world-settings." + worldName + "." + path, null); ++ return value.isEmpty() ? fallback : value; ++ } ++ ++ public float armorstandStepHeight = 0.0F; ++ public boolean armorstandSetNameVisible = false; ++ public boolean armorstandFixNametags = false; ++ public boolean armorstandMovement = true; ++ public boolean armorstandWaterMovement = true; ++ public boolean armorstandWaterFence = true; ++ public boolean armorstandPlaceWithArms = false; ++ private void armorstandSettings() { ++ armorstandStepHeight = (float) getDouble("gameplay-mechanics.armorstand.step-height", armorstandStepHeight); ++ armorstandSetNameVisible = getBoolean("gameplay-mechanics.armorstand.set-name-visible-when-placing-with-custom-name", armorstandSetNameVisible); ++ armorstandFixNametags = getBoolean("gameplay-mechanics.armorstand.fix-nametags", armorstandFixNametags); ++ armorstandMovement = getBoolean("gameplay-mechanics.armorstand.can-movement-tick", armorstandMovement); ++ armorstandWaterMovement = getBoolean("gameplay-mechanics.armorstand.can-move-in-water", armorstandWaterMovement); ++ armorstandWaterFence = getBoolean("gameplay-mechanics.armorstand.can-move-in-water-over-fence", armorstandWaterFence); ++ armorstandPlaceWithArms = getBoolean("gameplay-mechanics.armorstand.place-with-arms-visible", armorstandPlaceWithArms); ++ } ++ ++ public boolean arrowMovementResetsDespawnCounter = true; ++ private void arrowSettings() { ++ arrowMovementResetsDespawnCounter = getBoolean("gameplay-mechanics.arrow.movement-resets-despawn-counter", arrowMovementResetsDespawnCounter); ++ } ++ ++ public boolean useBetterMending = false; ++ public boolean alwaysTameInCreative = false; ++ public boolean boatEjectPlayersOnLand = false; ++ public boolean boatsDoFallDamage = true; ++ public boolean disableDropsOnCrammingDeath = false; ++ public boolean entitiesCanUsePortals = true; ++ public boolean entitiesPickUpLootBypassMobGriefing = false; ++ public boolean fireballsBypassMobGriefing = false; ++ public boolean imposeTeleportRestrictionsOnGateways = false; ++ public boolean milkCuresBadOmen = true; ++ public boolean milkClearsBeneficialEffects = true; ++ public boolean noteBlockIgnoreAbove = false; ++ public boolean persistentDroppableEntityDisplayNames = false; ++ public boolean persistentTileEntityDisplayNames = false; ++ public boolean projectilesBypassMobGriefing = false; ++ public boolean tickFluids = true; ++ public double mobsBlindnessMultiplier = 1; ++ public double tridentLoyaltyVoidReturnHeight = 0.0D; ++ public double voidDamageHeight = -64.0D; ++ public double voidDamageDealt = 4.0D; ++ public int raidCooldownSeconds = 0; ++ public int animalBreedingCooldownSeconds = 0; ++ public boolean mobsIgnoreRails = false; ++ public boolean rainStopsAfterSleep = true; ++ public boolean thunderStopsAfterSleep = true; ++ public int mobLastHurtByPlayerTime = 100; ++ private void miscGameplayMechanicsSettings() { ++ useBetterMending = getBoolean("gameplay-mechanics.use-better-mending", useBetterMending); ++ alwaysTameInCreative = getBoolean("gameplay-mechanics.always-tame-in-creative", alwaysTameInCreative); ++ boatEjectPlayersOnLand = getBoolean("gameplay-mechanics.boat.eject-players-on-land", boatEjectPlayersOnLand); ++ boatsDoFallDamage = getBoolean("gameplay-mechanics.boat.do-fall-damage", boatsDoFallDamage); ++ disableDropsOnCrammingDeath = getBoolean("gameplay-mechanics.disable-drops-on-cramming-death", disableDropsOnCrammingDeath); ++ entitiesCanUsePortals = getBoolean("gameplay-mechanics.entities-can-use-portals", entitiesCanUsePortals); ++ entitiesPickUpLootBypassMobGriefing = getBoolean("gameplay-mechanics.entities-pick-up-loot-bypass-mob-griefing", entitiesPickUpLootBypassMobGriefing); ++ fireballsBypassMobGriefing = getBoolean("gameplay-mechanics.fireballs-bypass-mob-griefing", fireballsBypassMobGriefing); ++ imposeTeleportRestrictionsOnGateways = getBoolean("gameplay-mechanics.impose-teleport-restrictions-on-gateways", imposeTeleportRestrictionsOnGateways); ++ milkCuresBadOmen = getBoolean("gameplay-mechanics.milk-cures-bad-omen", milkCuresBadOmen); ++ milkClearsBeneficialEffects = getBoolean("gameplay-mechanics.milk-clears-beneficial-effects", milkClearsBeneficialEffects); ++ noteBlockIgnoreAbove = getBoolean("gameplay-mechanics.note-block-ignore-above", noteBlockIgnoreAbove); ++ persistentTileEntityDisplayNames = getBoolean("gameplay-mechanics.persistent-tileentity-display-names-and-lore", persistentTileEntityDisplayNames); ++ persistentDroppableEntityDisplayNames = getBoolean("gameplay-mechanics.persistent-droppable-entity-display-names", persistentDroppableEntityDisplayNames); ++ projectilesBypassMobGriefing = getBoolean("gameplay-mechanics.projectiles-bypass-mob-griefing", projectilesBypassMobGriefing); ++ tickFluids = getBoolean("gameplay-mechanics.tick-fluids", tickFluids); ++ mobsBlindnessMultiplier = getDouble("gameplay-mechanics.entity-blindness-multiplier", mobsBlindnessMultiplier); ++ tridentLoyaltyVoidReturnHeight = getDouble("gameplay-mechanics.trident-loyalty-void-return-height", tridentLoyaltyVoidReturnHeight); ++ voidDamageHeight = getDouble("gameplay-mechanics.void-damage-height", voidDamageHeight); ++ voidDamageDealt = getDouble("gameplay-mechanics.void-damage-dealt", voidDamageDealt); ++ raidCooldownSeconds = getInt("gameplay-mechanics.raid-cooldown-seconds", raidCooldownSeconds); ++ animalBreedingCooldownSeconds = getInt("gameplay-mechanics.animal-breeding-cooldown-seconds", animalBreedingCooldownSeconds); ++ mobsIgnoreRails = getBoolean("gameplay-mechanics.mobs-ignore-rails", mobsIgnoreRails); ++ rainStopsAfterSleep = getBoolean("gameplay-mechanics.rain-stops-after-sleep", rainStopsAfterSleep); ++ thunderStopsAfterSleep = getBoolean("gameplay-mechanics.thunder-stops-after-sleep", thunderStopsAfterSleep); ++ mobLastHurtByPlayerTime = getInt("gameplay-mechanics.mob-last-hurt-by-player-time", mobLastHurtByPlayerTime); ++ } ++ ++ public int daytimeTicks = 12000; ++ public int nighttimeTicks = 12000; ++ private void daytimeCycleSettings() { ++ daytimeTicks = getInt("gameplay-mechanics.daylight-cycle-ticks.daytime", daytimeTicks); ++ nighttimeTicks = getInt("gameplay-mechanics.daylight-cycle-ticks.nighttime", nighttimeTicks); ++ } ++ ++ public int drowningAirTicks = 300; ++ public int drowningDamageInterval = 20; ++ public double damageFromDrowning = 2.0F; ++ private void drowningSettings() { ++ drowningAirTicks = getInt("gameplay-mechanics.drowning.air-ticks", drowningAirTicks); ++ drowningDamageInterval = getInt("gameplay-mechanics.drowning.ticks-per-damage", drowningDamageInterval); ++ damageFromDrowning = getDouble("gameplay-mechanics.drowning.damage-from-drowning", damageFromDrowning); ++ } ++ ++ public int elytraDamagePerSecond = 1; ++ public double elytraDamageMultiplyBySpeed = 0; ++ public boolean elytraIgnoreUnbreaking = false; ++ public int elytraDamagePerFireworkBoost = 0; ++ public int elytraDamagePerTridentBoost = 0; ++ public boolean elytraKineticDamage = true; ++ private void elytraSettings() { ++ elytraDamagePerSecond = getInt("gameplay-mechanics.elytra.damage-per-second", elytraDamagePerSecond); ++ elytraDamageMultiplyBySpeed = getDouble("gameplay-mechanics.elytra.damage-multiplied-by-speed", elytraDamageMultiplyBySpeed); ++ elytraIgnoreUnbreaking = getBoolean("gameplay-mechanics.elytra.ignore-unbreaking", elytraIgnoreUnbreaking); ++ elytraDamagePerFireworkBoost = getInt("gameplay-mechanics.elytra.damage-per-boost.firework", elytraDamagePerFireworkBoost); ++ elytraDamagePerTridentBoost = getInt("gameplay-mechanics.elytra.damage-per-boost.trident", elytraDamagePerTridentBoost); ++ elytraKineticDamage = getBoolean("gameplay-mechanics.elytra.kinetic-damage", elytraKineticDamage); ++ } ++ ++ public int entityLifeSpan = 0; ++ public float entityLeftHandedChance = 0.05f; ++ public boolean entitySharedRandom = true; ++ private void entitySettings() { ++ entityLifeSpan = getInt("gameplay-mechanics.entity-lifespan", entityLifeSpan); ++ entityLeftHandedChance = (float) getDouble("gameplay-mechanics.entity-left-handed-chance", entityLeftHandedChance); ++ entitySharedRandom = getBoolean("settings.entity.shared-random", entitySharedRandom); ++ } ++ ++ public boolean explosionClampRadius = true; ++ private void explosionSettings() { ++ explosionClampRadius = getBoolean("gameplay-mechanics.clamp-explosion-radius", explosionClampRadius); ++ } ++ ++ public boolean infinityWorksWithoutArrows = false; ++ public boolean infinityWorksWithNormalArrows = true; ++ public boolean infinityWorksWithSpectralArrows = false; ++ public boolean infinityWorksWithTippedArrows = false; ++ private void infinityArrowsSettings() { ++ infinityWorksWithoutArrows = getBoolean("gameplay-mechanics.infinity-bow.works-without-arrows", infinityWorksWithoutArrows); ++ infinityWorksWithNormalArrows = getBoolean("gameplay-mechanics.infinity-bow.normal-arrows", infinityWorksWithNormalArrows); ++ infinityWorksWithSpectralArrows = getBoolean("gameplay-mechanics.infinity-bow.spectral-arrows", infinityWorksWithSpectralArrows); ++ infinityWorksWithTippedArrows = getBoolean("gameplay-mechanics.infinity-bow.tipped-arrows", infinityWorksWithTippedArrows); ++ } ++ ++ public List itemImmuneToCactus = new ArrayList<>(); ++ public List itemImmuneToExplosion = new ArrayList<>(); ++ public List itemImmuneToFire = new ArrayList<>(); ++ public List itemImmuneToLightning = new ArrayList<>(); ++ public boolean dontRunWithScissors = false; ++ public double scissorsRunningDamage = 1D; ++ public float enderPearlDamage = 5.0F; ++ public int enderPearlCooldown = 20; ++ public int enderPearlCooldownCreative = 20; ++ public float enderPearlEndermiteChance = 0.05F; ++ public int glowBerriesEatGlowDuration = 0; ++ public boolean shulkerBoxItemDropContentsWhenDestroyed = true; ++ public boolean compassItemShowsBossBar = false; ++ public boolean snowballExtinguishesFire = false; ++ public boolean snowballExtinguishesCandles = false; ++ public boolean snowballExtinguishesCampfires = false; ++ private void itemSettings() { ++ itemImmuneToCactus.clear(); ++ getList("gameplay-mechanics.item.immune.cactus", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToCactus.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToCactus.add(item); ++ }); ++ itemImmuneToExplosion.clear(); ++ getList("gameplay-mechanics.item.immune.explosion", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToExplosion.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToExplosion.add(item); ++ }); ++ itemImmuneToFire.clear(); ++ getList("gameplay-mechanics.item.immune.fire", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToFire.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToFire.add(item); ++ }); ++ itemImmuneToLightning.clear(); ++ getList("gameplay-mechanics.item.immune.lightning", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToLightning.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToLightning.add(item); ++ }); ++ dontRunWithScissors = getBoolean("gameplay-mechanics.item.shears.damage-if-sprinting", dontRunWithScissors); ++ scissorsRunningDamage = getDouble("gameplay-mechanics.item.shears.sprinting-damage", scissorsRunningDamage); ++ enderPearlDamage = (float) getDouble("gameplay-mechanics.item.ender-pearl.damage", enderPearlDamage); ++ enderPearlCooldown = getInt("gameplay-mechanics.item.ender-pearl.cooldown", enderPearlCooldown); ++ enderPearlCooldownCreative = getInt("gameplay-mechanics.item.ender-pearl.creative-cooldown", enderPearlCooldownCreative); ++ enderPearlEndermiteChance = (float) getDouble("gameplay-mechanics.item.ender-pearl.endermite-spawn-chance", enderPearlEndermiteChance); ++ glowBerriesEatGlowDuration = getInt("gameplay-mechanics.item.glow_berries.eat-glow-duration", glowBerriesEatGlowDuration); ++ shulkerBoxItemDropContentsWhenDestroyed = getBoolean("gameplay-mechanics.item.shulker_box.drop-contents-when-destroyed", shulkerBoxItemDropContentsWhenDestroyed); ++ compassItemShowsBossBar = getBoolean("gameplay-mechanics.item.compass.holding-shows-bossbar", compassItemShowsBossBar); ++ snowballExtinguishesFire = getBoolean("gameplay-mechanics.item.snowball.extinguish.fire", snowballExtinguishesFire); ++ snowballExtinguishesCandles = getBoolean("gameplay-mechanics.item.snowball.extinguish.candles", snowballExtinguishesCandles); ++ snowballExtinguishesCampfires = getBoolean("gameplay-mechanics.item.snowball.extinguish.campfires", snowballExtinguishesCampfires); ++ } ++ ++ public double minecartMaxSpeed = 0.4D; ++ public boolean minecartPlaceAnywhere = false; ++ public boolean minecartControllable = false; ++ public float minecartControllableStepHeight = 1.0F; ++ public double minecartControllableHopBoost = 0.5D; ++ public boolean minecartControllableFallDamage = true; ++ public double minecartControllableBaseSpeed = 0.1D; ++ public Map minecartControllableBlockSpeeds = new HashMap<>(); ++ public double poweredRailBoostModifier = 0.06; ++ private void minecartSettings() { ++ if (PurpurConfig.version < 12) { ++ boolean oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.place-anywhere", minecartPlaceAnywhere); ++ set("gameplay-mechanics.controllable-minecarts.place-anywhere", null); ++ set("gameplay-mechanics.minecart.place-anywhere", oldBool); ++ oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.enabled", minecartControllable); ++ set("gameplay-mechanics.controllable-minecarts.enabled", null); ++ set("gameplay-mechanics.minecart.controllable.enabled", oldBool); ++ double oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.step-height", minecartControllableStepHeight); ++ set("gameplay-mechanics.controllable-minecarts.step-height", null); ++ set("gameplay-mechanics.minecart.controllable.step-height", oldDouble); ++ oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.hop-boost", minecartControllableHopBoost); ++ set("gameplay-mechanics.controllable-minecarts.hop-boost", null); ++ set("gameplay-mechanics.minecart.controllable.hop-boost", oldDouble); ++ oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.fall-damage", minecartControllableFallDamage); ++ set("gameplay-mechanics.controllable-minecarts.fall-damage", null); ++ set("gameplay-mechanics.minecart.controllable.fall-damage", oldBool); ++ oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.base-speed", minecartControllableBaseSpeed); ++ set("gameplay-mechanics.controllable-minecarts.base-speed", null); ++ set("gameplay-mechanics.minecart.controllable.base-speed", oldDouble); ++ ConfigurationSection section = getConfigurationSection("gameplay-mechanics.controllable-minecarts.block-speed"); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ if ("grass-block".equals(key)) key = "grass_block"; // oopsie ++ oldDouble = section.getDouble(key, minecartControllableBaseSpeed); ++ set("gameplay-mechanics.controllable-minecarts.block-speed." + key, null); ++ set("gameplay-mechanics.minecart.controllable.block-speed." + key, oldDouble); ++ } ++ set("gameplay-mechanics.controllable-minecarts.block-speed", null); ++ } ++ set("gameplay-mechanics.controllable-minecarts", null); ++ } ++ ++ minecartMaxSpeed = getDouble("gameplay-mechanics.minecart.max-speed", minecartMaxSpeed); ++ minecartPlaceAnywhere = getBoolean("gameplay-mechanics.minecart.place-anywhere", minecartPlaceAnywhere); ++ minecartControllable = getBoolean("gameplay-mechanics.minecart.controllable.enabled", minecartControllable); ++ minecartControllableStepHeight = (float) getDouble("gameplay-mechanics.minecart.controllable.step-height", minecartControllableStepHeight); ++ minecartControllableHopBoost = getDouble("gameplay-mechanics.minecart.controllable.hop-boost", minecartControllableHopBoost); ++ minecartControllableFallDamage = getBoolean("gameplay-mechanics.minecart.controllable.fall-damage", minecartControllableFallDamage); ++ minecartControllableBaseSpeed = getDouble("gameplay-mechanics.minecart.controllable.base-speed", minecartControllableBaseSpeed); ++ ConfigurationSection section = getConfigurationSection("gameplay-mechanics.minecart.controllable.block-speed"); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key)); ++ if (block != Blocks.AIR) { ++ minecartControllableBlockSpeeds.put(block, section.getDouble(key, minecartControllableBaseSpeed)); ++ } ++ } ++ } else { ++ set("gameplay-mechanics.minecart.controllable.block-speed.grass_block", 0.3D); ++ set("gameplay-mechanics.minecart.controllable.block-speed.stone", 0.5D); ++ } ++ poweredRailBoostModifier = getDouble("gameplay-mechanics.minecart.powered-rail.boost-modifier", poweredRailBoostModifier); ++ } ++ ++ public float entityHealthRegenAmount = 1.0F; ++ public float entityMinimalHealthPoison = 1.0F; ++ public float entityPoisonDegenerationAmount = 1.0F; ++ public float entityWitherDegenerationAmount = 1.0F; ++ public float humanHungerExhaustionAmount = 0.005F; ++ public float humanSaturationRegenAmount = 1.0F; ++ private void mobEffectSettings() { ++ entityHealthRegenAmount = (float) getDouble("gameplay-mechanics.mob-effects.health-regen-amount", entityHealthRegenAmount); ++ entityMinimalHealthPoison = (float) getDouble("gameplay-mechanics.mob-effects.minimal-health-poison-amount", entityMinimalHealthPoison); ++ entityPoisonDegenerationAmount = (float) getDouble("gameplay-mechanics.mob-effects.poison-degeneration-amount", entityPoisonDegenerationAmount); ++ entityWitherDegenerationAmount = (float) getDouble("gameplay-mechanics.mob-effects.wither-degeneration-amount", entityWitherDegenerationAmount); ++ humanHungerExhaustionAmount = (float) getDouble("gameplay-mechanics.mob-effects.hunger-exhaustion-amount", humanHungerExhaustionAmount); ++ humanSaturationRegenAmount = (float) getDouble("gameplay-mechanics.mob-effects.saturation-regen-amount", humanSaturationRegenAmount); ++ } ++ ++ public boolean catSpawning; ++ public boolean patrolSpawning; ++ public boolean phantomSpawning; ++ public boolean villagerTraderSpawning; ++ public boolean villageSiegeSpawning; ++ public boolean mobSpawningIgnoreCreativePlayers = false; ++ private void mobSpawnerSettings() { ++ // values of "default" or null will default to true only if the world environment is normal (aka overworld) ++ Predicate predicate = (bool) -> (bool != null && bool) || (bool == null && environment == World.Environment.NORMAL); ++ catSpawning = getBoolean("gameplay-mechanics.mob-spawning.village-cats", predicate); ++ patrolSpawning = getBoolean("gameplay-mechanics.mob-spawning.raid-patrols", predicate); ++ phantomSpawning = getBoolean("gameplay-mechanics.mob-spawning.phantoms", predicate); ++ villagerTraderSpawning = getBoolean("gameplay-mechanics.mob-spawning.wandering-traders", predicate); ++ villageSiegeSpawning = getBoolean("gameplay-mechanics.mob-spawning.village-sieges", predicate); ++ mobSpawningIgnoreCreativePlayers = getBoolean("gameplay-mechanics.mob-spawning.ignore-creative-players", mobSpawningIgnoreCreativePlayers); ++ } ++ ++ public boolean disableObserverClocks = false; ++ private void observerSettings() { ++ disableObserverClocks = getBoolean("blocks.observer.disable-clock", disableObserverClocks); ++ } ++ ++ public int playerNetheriteFireResistanceDuration = 0; ++ public int playerNetheriteFireResistanceAmplifier = 0; ++ public boolean playerNetheriteFireResistanceAmbient = false; ++ public boolean playerNetheriteFireResistanceShowParticles = false; ++ public boolean playerNetheriteFireResistanceShowIcon = true; ++ private void playerNetheriteFireResistance() { ++ playerNetheriteFireResistanceDuration = getInt("gameplay-mechanics.player.netherite-fire-resistance.duration", playerNetheriteFireResistanceDuration); ++ playerNetheriteFireResistanceAmplifier = getInt("gameplay-mechanics.player.netherite-fire-resistance.amplifier", playerNetheriteFireResistanceAmplifier); ++ playerNetheriteFireResistanceAmbient = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.ambient", playerNetheriteFireResistanceAmbient); ++ playerNetheriteFireResistanceShowParticles = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.show-particles", playerNetheriteFireResistanceShowParticles); ++ playerNetheriteFireResistanceShowIcon = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.show-icon", playerNetheriteFireResistanceShowIcon); ++ } ++ ++ public boolean idleTimeoutKick = true; ++ public boolean idleTimeoutTickNearbyEntities = true; ++ public boolean idleTimeoutCountAsSleeping = false; ++ public boolean idleTimeoutUpdateTabList = false; ++ public boolean idleTimeoutTargetPlayer = true; ++ public int playerSpawnInvulnerableTicks = 60; ++ public boolean playerInvulnerableWhileAcceptingResourcePack = false; ++ public String playerDeathExpDropEquation = "expLevel * 7"; ++ public int playerDeathExpDropMax = 100; ++ public boolean teleportIfOutsideBorder = false; ++ public boolean teleportOnNetherCeilingDamage = false; ++ public boolean totemOfUndyingWorksInInventory = false; ++ public boolean playerFixStuckPortal = false; ++ public boolean creativeOnePunch = false; ++ public boolean playerSleepNearMonsters = false; ++ public boolean playersSkipNight = true; ++ public double playerCriticalDamageMultiplier = 1.5D; ++ public int playerBurpDelay = 10; ++ public boolean playerBurpWhenFull = false; ++ public boolean playerArmorSwapping = false; ++ public boolean playerArmorSwappingCreativeMakesCopy = true; ++ public boolean playerRidableInWater = false; ++ public boolean playerRemoveBindingWithWeakness = false; ++ public int shiftRightClickRepairsMendingPoints = 0; ++ public int playerExpPickupDelay = 2; ++ public boolean playerVoidTrading = false; ++ private void playerSettings() { ++ if (PurpurConfig.version < 19) { ++ boolean oldVal = getBoolean("gameplay-mechanics.player.idle-timeout.mods-target", idleTimeoutTargetPlayer); ++ set("gameplay-mechanics.player.idle-timeout.mods-target", null); ++ set("gameplay-mechanics.player.idle-timeout.mobs-target", oldVal); ++ } ++ idleTimeoutKick = getBoolean("gameplay-mechanics.player.idle-timeout.kick-if-idle", idleTimeoutKick); ++ idleTimeoutTickNearbyEntities = getBoolean("gameplay-mechanics.player.idle-timeout.tick-nearby-entities", idleTimeoutTickNearbyEntities); ++ idleTimeoutCountAsSleeping = getBoolean("gameplay-mechanics.player.idle-timeout.count-as-sleeping", idleTimeoutCountAsSleeping); ++ idleTimeoutUpdateTabList = getBoolean("gameplay-mechanics.player.idle-timeout.update-tab-list", idleTimeoutUpdateTabList); ++ idleTimeoutTargetPlayer = getBoolean("gameplay-mechanics.player.idle-timeout.mobs-target", idleTimeoutTargetPlayer); ++ playerSpawnInvulnerableTicks = getInt("gameplay-mechanics.player.spawn-invulnerable-ticks", playerSpawnInvulnerableTicks); ++ playerInvulnerableWhileAcceptingResourcePack = getBoolean("gameplay-mechanics.player.invulnerable-while-accepting-resource-pack", playerInvulnerableWhileAcceptingResourcePack); ++ playerDeathExpDropEquation = getString("gameplay-mechanics.player.exp-dropped-on-death.equation", playerDeathExpDropEquation); ++ playerDeathExpDropMax = getInt("gameplay-mechanics.player.exp-dropped-on-death.maximum", playerDeathExpDropMax); ++ teleportIfOutsideBorder = getBoolean("gameplay-mechanics.player.teleport-if-outside-border", teleportIfOutsideBorder); ++ teleportOnNetherCeilingDamage = getBoolean("gameplay-mechanics.player.teleport-on-nether-ceiling-damage", teleportOnNetherCeilingDamage); ++ totemOfUndyingWorksInInventory = getBoolean("gameplay-mechanics.player.totem-of-undying-works-in-inventory", totemOfUndyingWorksInInventory); ++ playerFixStuckPortal = getBoolean("gameplay-mechanics.player.fix-stuck-in-portal", playerFixStuckPortal); ++ creativeOnePunch = getBoolean("gameplay-mechanics.player.one-punch-in-creative", creativeOnePunch); ++ playerSleepNearMonsters = getBoolean("gameplay-mechanics.player.sleep-ignore-nearby-mobs", playerSleepNearMonsters); ++ playersSkipNight = getBoolean("gameplay-mechanics.player.can-skip-night", playersSkipNight); ++ playerCriticalDamageMultiplier = getDouble("gameplay-mechanics.player.critical-damage-multiplier", playerCriticalDamageMultiplier); ++ playerBurpDelay = getInt("gameplay-mechanics.player.burp-delay", playerBurpDelay); ++ playerBurpWhenFull = getBoolean("gameplay-mechanics.player.burp-when-full", playerBurpWhenFull); ++ playerArmorSwapping = getBoolean("gameplay-mechanics.player.armor-click-equip.allow-hot-swapping", playerArmorSwapping); ++ playerArmorSwappingCreativeMakesCopy = getBoolean("gameplay-mechanics.player.armor-click-equip.creative-makes-copy", playerArmorSwappingCreativeMakesCopy); ++ playerRidableInWater = getBoolean("gameplay-mechanics.player.ridable-in-water", playerRidableInWater); ++ playerRemoveBindingWithWeakness = getBoolean("gameplay-mechanics.player.curse-of-binding.remove-with-weakness", playerRemoveBindingWithWeakness); ++ shiftRightClickRepairsMendingPoints = getInt("gameplay-mechanics.player.shift-right-click-repairs-mending-points", shiftRightClickRepairsMendingPoints); ++ playerExpPickupDelay = getInt("gameplay-mechanics.player.exp-pickup-delay-ticks", playerExpPickupDelay); ++ playerVoidTrading = getBoolean("gameplay-mechanics.player.allow-void-trading", playerVoidTrading); ++ } ++ ++ private static boolean projectileDespawnRateSettingsMigrated = false; ++ private void projectileDespawnRateSettings() { ++ if (PurpurConfig.version < 28 && !projectileDespawnRateSettingsMigrated) { ++ migrateProjectileDespawnRateSettings(EntityType.DRAGON_FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.EGG); ++ migrateProjectileDespawnRateSettings(EntityType.ENDER_PEARL); ++ migrateProjectileDespawnRateSettings(EntityType.EXPERIENCE_BOTTLE); ++ migrateProjectileDespawnRateSettings(EntityType.FIREWORK_ROCKET); ++ migrateProjectileDespawnRateSettings(EntityType.FISHING_BOBBER); ++ migrateProjectileDespawnRateSettings(EntityType.FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.LLAMA_SPIT); ++ migrateProjectileDespawnRateSettings(EntityType.POTION); ++ migrateProjectileDespawnRateSettings(EntityType.SHULKER_BULLET); ++ migrateProjectileDespawnRateSettings(EntityType.SMALL_FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.SNOWBALL); ++ migrateProjectileDespawnRateSettings(EntityType.WITHER_SKULL); ++ //PufferfishConfig.save(); ++ set("gameplay-mechanics.projectile-despawn-rates", null); ++ // pufferfish's entity_timeout is a global config ++ // we only want to migrate values from the ++ // default world (first world loaded) ++ projectileDespawnRateSettingsMigrated = true; ++ } ++ } ++ private void migrateProjectileDespawnRateSettings(EntityType type) { ++ //String pufferName = "entity_timeouts." + type.id.toUpperCase(Locale.ROOT); ++ //int value = getInt("gameplay-mechanics.projectile-despawn-rates." + type.id, -1); ++ //if (value != -1 && PufferfishConfig.getRawInt(pufferName, -1) == -1) { ++ // PufferfishConfig.setInt(pufferName, value); ++ // type.ttl = value; ++ //} ++ } ++ ++ public double bowProjectileOffset = 1.0D; ++ public double crossbowProjectileOffset = 1.0D; ++ public double eggProjectileOffset = 1.0D; ++ public double enderPearlProjectileOffset = 1.0D; ++ public double throwablePotionProjectileOffset = 1.0D; ++ public double tridentProjectileOffset = 1.0D; ++ public double snowballProjectileOffset = 1.0D; ++ private void projectileOffsetSettings() { ++ bowProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.bow", bowProjectileOffset); ++ crossbowProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.crossbow", crossbowProjectileOffset); ++ eggProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.egg", eggProjectileOffset); ++ enderPearlProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.ender-pearl", enderPearlProjectileOffset); ++ throwablePotionProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.throwable-potion", throwablePotionProjectileOffset); ++ tridentProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.trident", tridentProjectileOffset); ++ snowballProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.snowball", snowballProjectileOffset); ++ } ++ ++ public int snowballDamage = -1; ++ private void snowballSettings() { ++ snowballDamage = getInt("gameplay-mechanics.projectile-damage.snowball", snowballDamage); ++ } ++ ++ public List shovelTurnsBlockToGrassPath = new ArrayList<>(); ++ private void shovelSettings() { ++ getList("gameplay-mechanics.shovel-turns-block-to-grass-path", new ArrayList(){{ ++ add("minecraft:coarse_dirt"); ++ add("minecraft:dirt"); ++ add("minecraft:grass_block"); ++ add("minecraft:mycelium"); ++ add("minecraft:podzol"); ++ add("minecraft:rooted_dirt"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (block != Blocks.AIR) shovelTurnsBlockToGrassPath.add(block); ++ }); ++ } ++ ++ public boolean silkTouchEnabled = false; ++ public String silkTouchSpawnerName = "Monster Spawner"; ++ public List silkTouchSpawnerLore = new ArrayList<>(); ++ public List silkTouchTools = new ArrayList<>(); ++ public int minimumSilkTouchSpawnerRequire = 1; ++ private void silkTouchSettings() { ++ if (PurpurConfig.version < 21) { ++ String oldName = getString("gameplay-mechanics.silk-touch.spawner-name", silkTouchSpawnerName); ++ set("gameplay-mechanics.silk-touch.spawner-name", "" + ChatColor.toMM(oldName.replace("{mob}", ""))); ++ List list = new ArrayList<>(); ++ getList("gameplay-mechanics.silk-touch.spawner-lore", List.of("Spawns a ")) ++ .forEach(line -> list.add("" + ChatColor.toMM(line.toString().replace("{mob}", "")))); ++ set("gameplay-mechanics.silk-touch.spawner-lore", list); ++ } ++ silkTouchEnabled = getBoolean("gameplay-mechanics.silk-touch.enabled", silkTouchEnabled); ++ silkTouchSpawnerName = getString("gameplay-mechanics.silk-touch.spawner-name", silkTouchSpawnerName); ++ minimumSilkTouchSpawnerRequire = getInt("gameplay-mechanics.silk-touch.minimal-level", minimumSilkTouchSpawnerRequire); ++ silkTouchSpawnerLore.clear(); ++ getList("gameplay-mechanics.silk-touch.spawner-lore", List.of("Spawns a ")) ++ .forEach(line -> silkTouchSpawnerLore.add(line.toString())); ++ silkTouchTools.clear(); ++ getList("gameplay-mechanics.silk-touch.tools", List.of( ++ "minecraft:iron_pickaxe", ++ "minecraft:golden_pickaxe", ++ "minecraft:diamond_pickaxe", ++ "minecraft:netherite_pickaxe" ++ )).forEach(key -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) silkTouchTools.add(item); ++ }); ++ } ++ ++ public Map axeStrippables = new HashMap<>(); ++ public Map axeWaxables = new HashMap<>(); ++ public Map axeWeatherables = new HashMap<>(); ++ public Map hoeTillables = new HashMap<>(); ++ public boolean hoeReplantsCrops = false; ++ public boolean hoeReplantsNetherWarts = false; ++ private void toolSettings() { ++ axeStrippables.clear(); ++ axeWaxables.clear(); ++ axeWeatherables.clear(); ++ hoeTillables.clear(); ++ if (PurpurConfig.version < 18) { ++ ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + ".tools.hoe.tilling"); ++ if (section != null) { ++ PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tillables", section); ++ PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tilling", null); ++ } ++ section = PurpurConfig.config.getConfigurationSection("world-settings.default.tools.hoe.tilling"); ++ if (section != null) { ++ PurpurConfig.config.set("world-settings.default.tools.hoe.tillables", section); ++ PurpurConfig.config.set("world-settings.default.tools.hoe.tilling", null); ++ } ++ } ++ if (PurpurConfig.version < 29) { ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:mangrove_log", Map.of("into", "minecraft:stripped_mangrove_log", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:mangrove_wood", Map.of("into", "minecraft:stripped_mangrove_wood", "drops", new HashMap())); ++ } ++ getMap("tools.axe.strippables", Map.ofEntries( ++ Map.entry("minecraft:oak_wood", Map.of("into", "minecraft:stripped_oak_wood", "drops", new HashMap())), ++ Map.entry("minecraft:oak_log", Map.of("into", "minecraft:stripped_oak_log", "drops", new HashMap())), ++ Map.entry("minecraft:dark_oak_wood", Map.of("into", "minecraft:stripped_dark_oak_wood", "drops", new HashMap())), ++ Map.entry("minecraft:dark_oak_log", Map.of("into", "minecraft:stripped_dark_oak_log", "drops", new HashMap())), ++ Map.entry("minecraft:acacia_wood", Map.of("into", "minecraft:stripped_acacia_wood", "drops", new HashMap())), ++ Map.entry("minecraft:acacia_log", Map.of("into", "minecraft:stripped_acacia_log", "drops", new HashMap())), ++ Map.entry("minecraft:birch_wood", Map.of("into", "minecraft:stripped_birch_wood", "drops", new HashMap())), ++ Map.entry("minecraft:birch_log", Map.of("into", "minecraft:stripped_birch_log", "drops", new HashMap())), ++ Map.entry("minecraft:jungle_wood", Map.of("into", "minecraft:stripped_jungle_wood", "drops", new HashMap())), ++ Map.entry("minecraft:jungle_log", Map.of("into", "minecraft:stripped_jungle_log", "drops", new HashMap())), ++ Map.entry("minecraft:spruce_wood", Map.of("into", "minecraft:stripped_spruce_wood", "drops", new HashMap())), ++ Map.entry("minecraft:spruce_log", Map.of("into", "minecraft:stripped_spruce_log", "drops", new HashMap())), ++ Map.entry("minecraft:mangrove_wood", Map.of("into", "minecraft:stripped_mangrove_wood", "drops", new HashMap())), ++ Map.entry("minecraft:mangrove_log", Map.of("into", "minecraft:stripped_mangrove_log", "drops", new HashMap())), ++ Map.entry("minecraft:warped_stem", Map.of("into", "minecraft:stripped_warped_stem", "drops", new HashMap())), ++ Map.entry("minecraft:warped_hyphae", Map.of("into", "minecraft:stripped_warped_hyphae", "drops", new HashMap())), ++ Map.entry("minecraft:crimson_stem", Map.of("into", "minecraft:stripped_crimson_stem", "drops", new HashMap())), ++ Map.entry("minecraft:crimson_hyphae", Map.of("into", "minecraft:stripped_crimson_hyphae", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.strippables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeStrippables.put(block, new Strippable(into, drops)); ++ }); ++ getMap("tools.axe.waxables", Map.ofEntries( ++ Map.entry("minecraft:waxed_copper_block", Map.of("into", "minecraft:copper_block", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper", Map.of("into", "minecraft:oxidized_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper", Map.of("into", "minecraft:oxidized_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper_slab", Map.of("into", "minecraft:oxidized_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper_stairs", Map.of("into", "minecraft:oxidized_cut_copper_stairs", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.waxables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeWaxables.put(block, new Waxable(into, drops)); ++ }); ++ getMap("tools.axe.weatherables", Map.ofEntries( ++ Map.entry("minecraft:exposed_copper", Map.of("into", "minecraft:copper_block", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.weatherables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeWeatherables.put(block, new Weatherable(into, drops)); ++ }); ++ getMap("tools.hoe.tillables", Map.ofEntries( ++ Map.entry("minecraft:grass_block", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:dirt_path", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:dirt", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:coarse_dirt", Map.of("condition", "air_above", "into", "minecraft:dirt", "drops", new HashMap())), ++ Map.entry("minecraft:rooted_dirt", Map.of("condition", "always", "into", "minecraft:dirt", "drops", Map.of("minecraft:hanging_roots", 1.0D)))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tillables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tillables." + blockId + "`"); return; } ++ String conditionId = (String) map.get("condition"); ++ Tillable.Condition condition = Tillable.Condition.get(conditionId); ++ if (condition == null) { PurpurConfig.log(Level.SEVERE, "Invalid condition for `tools.hoe.tillables." + blockId + ".condition`: " + conditionId); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tillables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tillables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.hoe.tillables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ hoeTillables.put(block, new Tillable(condition, into, drops)); ++ }); ++ hoeReplantsCrops = getBoolean("tools.hoe.replant-crops", hoeReplantsCrops); ++ hoeReplantsNetherWarts = getBoolean("tools.hoe.replant-nether-warts", hoeReplantsNetherWarts); ++ } ++ ++ public boolean anvilAllowColors = false; ++ public boolean anvilColorsUseMiniMessage; ++ public int anvilRepairIngotsAmount = 0; ++ public int anvilDamageObsidianAmount = 0; ++ private void anvilSettings() { ++ anvilAllowColors = getBoolean("blocks.anvil.allow-colors", anvilAllowColors); ++ anvilColorsUseMiniMessage = getBoolean("blocks.anvil.use-mini-message", anvilColorsUseMiniMessage); ++ anvilRepairIngotsAmount = getInt("blocks.anvil.iron-ingots-used-for-repair", anvilRepairIngotsAmount); ++ anvilDamageObsidianAmount = getInt("blocks.anvil.obsidian-used-for-damage", anvilDamageObsidianAmount); ++ } ++ ++ public double azaleaGrowthChance = 0.0D; ++ private void azaleaSettings() { ++ azaleaGrowthChance = getDouble("blocks.azalea.growth-chance", azaleaGrowthChance); ++ } ++ ++ public int beaconLevelOne = 20; ++ public int beaconLevelTwo = 30; ++ public int beaconLevelThree = 40; ++ public int beaconLevelFour = 50; ++ public boolean beaconAllowEffectsWithTintedGlass = false; ++ private void beaconSettings() { ++ beaconLevelOne = getInt("blocks.beacon.effect-range.level-1", beaconLevelOne); ++ beaconLevelTwo = getInt("blocks.beacon.effect-range.level-2", beaconLevelTwo); ++ beaconLevelThree = getInt("blocks.beacon.effect-range.level-3", beaconLevelThree); ++ beaconLevelFour = getInt("blocks.beacon.effect-range.level-4", beaconLevelFour); ++ beaconAllowEffectsWithTintedGlass = getBoolean("blocks.beacon.allow-effects-with-tinted-glass", beaconAllowEffectsWithTintedGlass); ++ } ++ ++ public boolean bedExplode = true; ++ public boolean bedExplodeOnVillagerSleep = false; ++ public double bedExplosionPower = 5.0D; ++ public boolean bedExplosionFire = true; ++ public net.minecraft.world.level.Level.ExplosionInteraction bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ private void bedSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.bed.explosion-effect", bedExplosionEffect.name()))) { ++ set("blocks.bed.explosion-effect", "BLOCK"); ++ } ++ } ++ bedExplode = getBoolean("blocks.bed.explode", bedExplode); ++ bedExplodeOnVillagerSleep = getBoolean("blocks.bed.explode-on-villager-sleep", bedExplodeOnVillagerSleep); ++ bedExplosionPower = getDouble("blocks.bed.explosion-power", bedExplosionPower); ++ bedExplosionFire = getBoolean("blocks.bed.explosion-fire", bedExplosionFire); ++ try { ++ bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.bed.explosion-effect", bedExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.bed.explosion-effect`! Using default of `BLOCK`"); ++ bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ } ++ ++ public Map bigDripleafTiltDelay = new HashMap<>(); ++ private void bigDripleafSettings() { ++ bigDripleafTiltDelay.clear(); ++ getMap("blocks.big_dripleaf.tilt-delay", Map.ofEntries( ++ Map.entry("UNSTABLE", 10), ++ Map.entry("PARTIAL", 10), ++ Map.entry("FULL", 100)) ++ ).forEach((tilt, delay) -> { ++ try { ++ bigDripleafTiltDelay.put(Tilt.valueOf(tilt), (int) delay); ++ } catch (IllegalArgumentException e) { ++ PurpurConfig.log(Level.SEVERE, "Invalid big_dripleaf tilt key: " + tilt); ++ } ++ }); ++ } ++ ++ public boolean buddingAmethystSilkTouch = false; ++ private void buddingAmethystSettings() { ++ buddingAmethystSilkTouch = getBoolean("blocks.budding_amethyst.silk-touch", buddingAmethystSilkTouch); ++ } ++ ++ public boolean cactusBreaksFromSolidNeighbors = true; ++ public boolean cactusAffectedByBonemeal = false; ++ private void cactusSettings() { ++ cactusBreaksFromSolidNeighbors = getBoolean("blocks.cactus.breaks-from-solid-neighbors", cactusBreaksFromSolidNeighbors); ++ cactusAffectedByBonemeal = getBoolean("blocks.cactus.affected-by-bonemeal", cactusAffectedByBonemeal); ++ } ++ ++ public boolean sugarCanAffectedByBonemeal = false; ++ private void sugarCaneSettings() { ++ sugarCanAffectedByBonemeal = getBoolean("blocks.sugar_cane.affected-by-bonemeal", sugarCanAffectedByBonemeal); ++ } ++ ++ public boolean netherWartAffectedByBonemeal = false; ++ private void netherWartSettings() { ++ netherWartAffectedByBonemeal = getBoolean("blocks.nether_wart.affected-by-bonemeal", netherWartAffectedByBonemeal); ++ } ++ ++ public boolean campFireLitWhenPlaced = true; ++ private void campFireSettings() { ++ campFireLitWhenPlaced = getBoolean("blocks.campfire.lit-when-placed", campFireLitWhenPlaced); ++ } ++ ++ public boolean chestOpenWithBlockOnTop = false; ++ private void chestSettings() { ++ chestOpenWithBlockOnTop = getBoolean("blocks.chest.open-with-solid-block-on-top", chestOpenWithBlockOnTop); ++ } ++ ++ public boolean composterBulkProcess = false; ++ private void composterSettings() { ++ composterBulkProcess = getBoolean("blocks.composter.sneak-to-bulk-process", composterBulkProcess); ++ } ++ ++ public boolean coralDieOutsideWater = true; ++ private void coralSettings() { ++ coralDieOutsideWater = getBoolean("blocks.coral.die-outside-water", coralDieOutsideWater); ++ } ++ ++ public boolean dispenserApplyCursedArmor = true; ++ public boolean dispenserPlaceAnvils = false; ++ private void dispenserSettings() { ++ dispenserApplyCursedArmor = getBoolean("blocks.dispenser.apply-cursed-to-armor-slots", dispenserApplyCursedArmor); ++ dispenserPlaceAnvils = getBoolean("blocks.dispenser.place-anvils", dispenserPlaceAnvils); ++ } ++ ++ public List doorRequiresRedstone = new ArrayList<>(); ++ private void doorSettings() { ++ getList("blocks.door.requires-redstone", new ArrayList()).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ doorRequiresRedstone.add(block); ++ } ++ }); ++ } ++ ++ public boolean dragonEggTeleport = true; ++ private void dragonEggSettings() { ++ dragonEggTeleport = getBoolean("blocks.dragon_egg.teleport", dragonEggTeleport); ++ } ++ ++ public boolean baselessEndCrystalExplode = true; ++ public double baselessEndCrystalExplosionPower = 6.0D; ++ public boolean baselessEndCrystalExplosionFire = false; ++ public net.minecraft.world.level.Level.ExplosionInteraction baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ public boolean basedEndCrystalExplode = true; ++ public double basedEndCrystalExplosionPower = 6.0D; ++ public boolean basedEndCrystalExplosionFire = false; ++ public net.minecraft.world.level.Level.ExplosionInteraction basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ public int endCrystalCramming = 0; ++ private void endCrystalSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.end-crystal.baseless.explosion-effect", baselessEndCrystalExplosionEffect.name()))) { ++ set("blocks.end-crystal.baseless.explosion-effect", "BLOCK"); ++ } ++ if ("DESTROY".equals(getString("blocks.end-crystal.base.explosion-effect", basedEndCrystalExplosionEffect.name()))) { ++ set("blocks.end-crystal.base.explosion-effect", "BLOCK"); ++ } ++ } ++ baselessEndCrystalExplode = getBoolean("blocks.end-crystal.baseless.explode", baselessEndCrystalExplode); ++ baselessEndCrystalExplosionPower = getDouble("blocks.end-crystal.baseless.explosion-power", baselessEndCrystalExplosionPower); ++ baselessEndCrystalExplosionFire = getBoolean("blocks.end-crystal.baseless.explosion-fire", baselessEndCrystalExplosionFire); ++ try { ++ baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.end-crystal.baseless.explosion-effect", baselessEndCrystalExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.end-crystal.baseless.explosion-effect`! Using default of `BLOCK`"); ++ baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ basedEndCrystalExplode = getBoolean("blocks.end-crystal.base.explode", basedEndCrystalExplode); ++ basedEndCrystalExplosionPower = getDouble("blocks.end-crystal.base.explosion-power", basedEndCrystalExplosionPower); ++ basedEndCrystalExplosionFire = getBoolean("blocks.end-crystal.base.explosion-fire", basedEndCrystalExplosionFire); ++ try { ++ basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.end-crystal.base.explosion-effect", basedEndCrystalExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.end-crystal.base.explosion-effect`! Using default of `BLOCK`"); ++ basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ endCrystalCramming = getInt("blocks.end-crystal.cramming-amount", endCrystalCramming); ++ } ++ ++ public boolean farmlandBypassMobGriefing = false; ++ public boolean farmlandGetsMoistFromBelow = false; ++ public boolean farmlandAlpha = false; ++ public boolean farmlandTramplingDisabled = false; ++ public boolean farmlandTramplingOnlyPlayers = false; ++ public boolean farmlandTramplingFeatherFalling = false; ++ public double farmlandTrampleHeight = -1D; ++ private void farmlandSettings() { ++ farmlandBypassMobGriefing = getBoolean("blocks.farmland.bypass-mob-griefing", farmlandBypassMobGriefing); ++ farmlandGetsMoistFromBelow = getBoolean("blocks.farmland.gets-moist-from-below", farmlandGetsMoistFromBelow); ++ farmlandAlpha = getBoolean("blocks.farmland.use-alpha-farmland", farmlandAlpha); ++ farmlandTramplingDisabled = getBoolean("blocks.farmland.disable-trampling", farmlandTramplingDisabled); ++ farmlandTramplingOnlyPlayers = getBoolean("blocks.farmland.only-players-trample", farmlandTramplingOnlyPlayers); ++ farmlandTramplingFeatherFalling = getBoolean("blocks.farmland.feather-fall-distance-affects-trampling", farmlandTramplingFeatherFalling); ++ farmlandTrampleHeight = getDouble("blocks.farmland.trample-height", farmlandTrampleHeight); ++ } ++ ++ public double floweringAzaleaGrowthChance = 0.0D; ++ private void floweringAzaleaSettings() { ++ floweringAzaleaGrowthChance = getDouble("blocks.flowering_azalea.growth-chance", floweringAzaleaGrowthChance); ++ } ++ ++ public boolean furnaceUseLavaFromUnderneath = false; ++ private void furnaceSettings() { ++ if (PurpurConfig.version < 17) { ++ furnaceUseLavaFromUnderneath = getBoolean("blocks.furnace.infinite-fuel", furnaceUseLavaFromUnderneath); ++ boolean oldValue = getBoolean("blocks.furnace.infinite-fuel", furnaceUseLavaFromUnderneath); ++ set("blocks.furnace.infinite-fuel", null); ++ set("blocks.furnace.use-lava-from-underneath", oldValue); ++ } ++ furnaceUseLavaFromUnderneath = getBoolean("blocks.furnace.use-lava-from-underneath", furnaceUseLavaFromUnderneath); ++ } ++ ++ public boolean endPortalSafeTeleporting = true; ++ private void endPortalSettings() { ++ endPortalSafeTeleporting = getBoolean("blocks.end_portal.safe-teleporting", endPortalSafeTeleporting); ++ } ++ ++ public boolean mobsSpawnOnPackedIce = true; ++ public boolean mobsSpawnOnBlueIce = true; ++ public boolean snowOnBlueIce = true; ++ private void iceSettings() { ++ mobsSpawnOnPackedIce = getBoolean("blocks.packed_ice.allow-mob-spawns", mobsSpawnOnPackedIce); ++ mobsSpawnOnBlueIce = getBoolean("blocks.blue_ice.allow-mob-spawns", mobsSpawnOnBlueIce); ++ snowOnBlueIce = getBoolean("blocks.blue_ice.allow-snow-formation", snowOnBlueIce); ++ } ++ ++ public boolean lavaInfinite = false; ++ public int lavaInfiniteRequiredSources = 2; ++ public int lavaSpeedNether = 10; ++ public int lavaSpeedNotNether = 30; ++ private void lavaSettings() { ++ lavaInfinite = getBoolean("blocks.lava.infinite-source", lavaInfinite); ++ lavaInfiniteRequiredSources = getInt("blocks.lava.infinite-required-sources", lavaInfiniteRequiredSources); ++ lavaSpeedNether = getInt("blocks.lava.speed.nether", lavaSpeedNether); ++ lavaSpeedNotNether = getInt("blocks.lava.speed.not-nether", lavaSpeedNotNether); ++ } ++ ++ public int pistonBlockPushLimit = 12; ++ private void pistonSettings() { ++ pistonBlockPushLimit = getInt("blocks.piston.block-push-limit", pistonBlockPushLimit); ++ } ++ ++ public boolean magmaBlockDamageWhenSneaking = false; ++ public boolean magmaBlockDamageWithFrostWalker = false; ++ private void magmaBlockSettings() { ++ magmaBlockDamageWhenSneaking = getBoolean("blocks.magma-block.damage-when-sneaking", magmaBlockDamageWhenSneaking); ++ magmaBlockDamageWithFrostWalker = getBoolean("blocks.magma-block.damage-with-frost-walker", magmaBlockDamageWithFrostWalker); ++ } ++ ++ public boolean powderSnowBypassMobGriefing = false; ++ private void powderSnowSettings() { ++ powderSnowBypassMobGriefing = getBoolean("blocks.powder_snow.bypass-mob-griefing", powderSnowBypassMobGriefing); ++ } ++ ++ public int railActivationRange = 8; ++ private void railSettings() { ++ railActivationRange = getInt("blocks.powered-rail.activation-range", railActivationRange); ++ } ++ ++ public boolean respawnAnchorExplode = true; ++ public double respawnAnchorExplosionPower = 5.0D; ++ public boolean respawnAnchorExplosionFire = true; ++ public net.minecraft.world.level.Level.ExplosionInteraction respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ private void respawnAnchorSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.respawn_anchor.explosion-effect", respawnAnchorExplosionEffect.name()))) { ++ set("blocks.respawn_anchor.explosion-effect", "BLOCK"); ++ } ++ } ++ respawnAnchorExplode = getBoolean("blocks.respawn_anchor.explode", respawnAnchorExplode); ++ respawnAnchorExplosionPower = getDouble("blocks.respawn_anchor.explosion-power", respawnAnchorExplosionPower); ++ respawnAnchorExplosionFire = getBoolean("blocks.respawn_anchor.explosion-fire", respawnAnchorExplosionFire); ++ try { ++ respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.respawn_anchor.explosion-effect", respawnAnchorExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.respawn_anchor.explosion-effect`! Using default of `BLOCK`"); ++ respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ } ++ ++ public boolean fixSandDuping = true; ++ private void sandSettings() { ++ fixSandDuping = getBoolean("blocks.sand.fix-duping", fixSandDuping); ++ } ++ ++ public boolean sculkShriekerCanSummonDefault = false; ++ private void sculkShriekerSettings() { ++ sculkShriekerCanSummonDefault = getBoolean("blocks.sculk_shrieker.can-summon-default", sculkShriekerCanSummonDefault); ++ } ++ ++ public boolean shulkerBoxAllowOversizedStacks = false; ++ private void shulkerBoxSettings() { ++ shulkerBoxAllowOversizedStacks = getBoolean("blocks.shulker_box.allow-oversized-stacks", shulkerBoxAllowOversizedStacks); ++ } ++ ++ public boolean signRightClickEdit = false; ++ public boolean signAllowColors = false; ++ private void signSettings() { ++ signRightClickEdit = getBoolean("blocks.sign.right-click-edit", signRightClickEdit); ++ signAllowColors = getBoolean("blocks.sign.allow-colors", signAllowColors); ++ } ++ ++ public boolean slabHalfBreak = false; ++ private void slabSettings() { ++ slabHalfBreak = getBoolean("blocks.slab.break-individual-slabs-when-sneaking", slabHalfBreak); ++ } ++ ++ public boolean spawnerDeactivateByRedstone = false; ++ public boolean spawnerFixMC238526 = false; ++ private void spawnerSettings() { ++ spawnerDeactivateByRedstone = getBoolean("blocks.spawner.deactivate-by-redstone", spawnerDeactivateByRedstone); ++ spawnerFixMC238526 = getBoolean("blocks.spawner.fix-mc-238526", spawnerFixMC238526); ++ } ++ ++ public int spongeAbsorptionArea = 64; ++ public int spongeAbsorptionRadius = 6; ++ public boolean spongeAbsorbsLava = false; ++ private void spongeSettings() { ++ spongeAbsorptionArea = getInt("blocks.sponge.absorption.area", spongeAbsorptionArea); ++ spongeAbsorptionRadius = getInt("blocks.sponge.absorption.radius", spongeAbsorptionRadius); ++ spongeAbsorbsLava = getBoolean("blocks.sponge.absorbs-lava", spongeAbsorbsLava); ++ } ++ ++ public float stonecutterDamage = 0.0F; ++ private void stonecutterSettings() { ++ stonecutterDamage = (float) getDouble("blocks.stonecutter.damage", stonecutterDamage); ++ } ++ ++ public boolean turtleEggsBreakFromExpOrbs = true; ++ public boolean turtleEggsBreakFromItems = true; ++ public boolean turtleEggsBreakFromMinecarts = true; ++ public boolean turtleEggsBypassMobGriefing = false; ++ public int turtleEggsRandomTickCrackChance = 500; ++ public boolean turtleEggsTramplingFeatherFalling = false; ++ private void turtleEggSettings() { ++ turtleEggsBreakFromExpOrbs = getBoolean("blocks.turtle_egg.break-from-exp-orbs", turtleEggsBreakFromExpOrbs); ++ turtleEggsBreakFromItems = getBoolean("blocks.turtle_egg.break-from-items", turtleEggsBreakFromItems); ++ turtleEggsBreakFromMinecarts = getBoolean("blocks.turtle_egg.break-from-minecarts", turtleEggsBreakFromMinecarts); ++ turtleEggsBypassMobGriefing = getBoolean("blocks.turtle_egg.bypass-mob-griefing", turtleEggsBypassMobGriefing); ++ turtleEggsRandomTickCrackChance = getInt("blocks.turtle_egg.random-tick-crack-chance", turtleEggsRandomTickCrackChance); ++ turtleEggsTramplingFeatherFalling = getBoolean("blocks.turtle_egg.feather-fall-distance-affects-trampling", turtleEggsTramplingFeatherFalling); ++ } ++ ++ public boolean waterInfinite = true; ++ public int waterInfiniteRequiredSources = 2; ++ private void waterSources() { ++ waterInfinite = getBoolean("blocks.water.infinite-source", waterInfinite); ++ waterInfiniteRequiredSources = getInt("blocks.water.infinite-required-sources", waterInfiniteRequiredSources); ++ } ++ ++ public boolean babiesAreRidable = true; ++ public boolean untamedTamablesAreRidable = true; ++ public boolean useNightVisionWhenRiding = false; ++ private void ridableSettings() { ++ babiesAreRidable = getBoolean("ridable-settings.babies-are-ridable", babiesAreRidable); ++ untamedTamablesAreRidable = getBoolean("ridable-settings.untamed-tamables-are-ridable", untamedTamablesAreRidable); ++ useNightVisionWhenRiding = getBoolean("ridable-settings.use-night-vision", useNightVisionWhenRiding); ++ } ++ ++ public boolean allayRidable = false; ++ public boolean allayRidableInWater = false; ++ public boolean allayControllable = true; ++ public List allayRespectNBT = new ArrayList<>(); ++ private void allaySettings() { ++ allayRidable = getBoolean("mobs.allay.ridable", allayRidable); ++ allayRidableInWater = getBoolean("mobs.allay.ridable-in-water", allayRidableInWater); ++ allayControllable = getBoolean("mobs.allay.controllable", allayControllable); ++ allayRespectNBT.clear(); ++ getList("mobs.allay.respect-nbt", new ArrayList<>()).forEach(key -> allayRespectNBT.add(key.toString())); ++ } ++ ++ public boolean axolotlRidable = false; ++ public boolean axolotlControllable = true; ++ public double axolotlMaxHealth = 14.0D; ++ public int axolotlBreedingTicks = 6000; ++ public boolean axolotlTakeDamageFromWater = false; ++ public boolean axolotlAlwaysDropExp = false; ++ private void axolotlSettings() { ++ axolotlRidable = getBoolean("mobs.axolotl.ridable", axolotlRidable); ++ axolotlControllable = getBoolean("mobs.axolotl.controllable", axolotlControllable); ++ axolotlMaxHealth = getDouble("mobs.axolotl.attributes.max_health", axolotlMaxHealth); ++ axolotlBreedingTicks = getInt("mobs.axolotl.breeding-delay-ticks", axolotlBreedingTicks); ++ axolotlTakeDamageFromWater = getBoolean("mobs.axolotl.takes-damage-from-water", axolotlTakeDamageFromWater); ++ axolotlAlwaysDropExp = getBoolean("mobs.axolotl.always-drop-exp", axolotlAlwaysDropExp); ++ } ++ ++ public boolean batRidable = false; ++ public boolean batRidableInWater = false; ++ public boolean batControllable = true; ++ public double batMaxY = 320D; ++ public double batMaxHealth = 6.0D; ++ public double batFollowRange = 16.0D; ++ public double batKnockbackResistance = 0.0D; ++ public double batMovementSpeed = 0.6D; ++ public double batFlyingSpeed = 0.6D; ++ public double batArmor = 0.0D; ++ public double batArmorToughness = 0.0D; ++ public double batAttackKnockback = 0.0D; ++ public boolean batTakeDamageFromWater = false; ++ public boolean batAlwaysDropExp = false; ++ private void batSettings() { ++ batRidable = getBoolean("mobs.bat.ridable", batRidable); ++ batRidableInWater = getBoolean("mobs.bat.ridable-in-water", batRidableInWater); ++ batControllable = getBoolean("mobs.bat.controllable", batControllable); ++ batMaxY = getDouble("mobs.bat.ridable-max-y", batMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.bat.attributes.max-health", batMaxHealth); ++ set("mobs.bat.attributes.max-health", null); ++ set("mobs.bat.attributes.max_health", oldValue); ++ } ++ batMaxHealth = getDouble("mobs.bat.attributes.max_health", batMaxHealth); ++ batTakeDamageFromWater = getBoolean("mobs.bat.takes-damage-from-water", batTakeDamageFromWater); ++ batAlwaysDropExp = getBoolean("mobs.bat.always-drop-exp", batAlwaysDropExp); ++ } ++ ++ public boolean beeRidable = false; ++ public boolean beeRidableInWater = false; ++ public boolean beeControllable = true; ++ public double beeMaxY = 320D; ++ public double beeMaxHealth = 10.0D; ++ public int beeBreedingTicks = 6000; ++ public boolean beeTakeDamageFromWater = false; ++ public boolean beeCanWorkAtNight = false; ++ public boolean beeCanWorkInRain = false; ++ public boolean beeAlwaysDropExp = false; ++ public boolean beeDiesAfterSting = true; ++ private void beeSettings() { ++ beeRidable = getBoolean("mobs.bee.ridable", beeRidable); ++ beeRidableInWater = getBoolean("mobs.bee.ridable-in-water", beeRidableInWater); ++ beeControllable = getBoolean("mobs.bee.controllable", beeControllable); ++ beeMaxY = getDouble("mobs.bee.ridable-max-y", beeMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.bee.attributes.max-health", beeMaxHealth); ++ set("mobs.bee.attributes.max-health", null); ++ set("mobs.bee.attributes.max_health", oldValue); ++ } ++ beeMaxHealth = getDouble("mobs.bee.attributes.max_health", beeMaxHealth); ++ beeBreedingTicks = getInt("mobs.bee.breeding-delay-ticks", beeBreedingTicks); ++ beeTakeDamageFromWater = getBoolean("mobs.bee.takes-damage-from-water", beeTakeDamageFromWater); ++ beeCanWorkAtNight = getBoolean("mobs.bee.can-work-at-night", beeCanWorkAtNight); ++ beeCanWorkInRain = getBoolean("mobs.bee.can-work-in-rain", beeCanWorkInRain); ++ beeAlwaysDropExp = getBoolean("mobs.bee.always-drop-exp", beeAlwaysDropExp); ++ beeDiesAfterSting = getBoolean("mobs.bee.dies-after-sting", beeDiesAfterSting); ++ } ++ ++ public boolean blazeRidable = false; ++ public boolean blazeRidableInWater = false; ++ public boolean blazeControllable = true; ++ public double blazeMaxY = 320D; ++ public double blazeMaxHealth = 20.0D; ++ public boolean blazeTakeDamageFromWater = true; ++ public boolean blazeAlwaysDropExp = false; ++ private void blazeSettings() { ++ blazeRidable = getBoolean("mobs.blaze.ridable", blazeRidable); ++ blazeRidableInWater = getBoolean("mobs.blaze.ridable-in-water", blazeRidableInWater); ++ blazeControllable = getBoolean("mobs.blaze.controllable", blazeControllable); ++ blazeMaxY = getDouble("mobs.blaze.ridable-max-y", blazeMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.blaze.attributes.max-health", blazeMaxHealth); ++ set("mobs.blaze.attributes.max-health", null); ++ set("mobs.blaze.attributes.max_health", oldValue); ++ } ++ blazeMaxHealth = getDouble("mobs.blaze.attributes.max_health", blazeMaxHealth); ++ blazeTakeDamageFromWater = getBoolean("mobs.blaze.takes-damage-from-water", blazeTakeDamageFromWater); ++ blazeAlwaysDropExp = getBoolean("mobs.blaze.always-drop-exp", blazeAlwaysDropExp); ++ } ++ ++ public int camelBreedingTicks = 6000; ++ public double camelMaxHealthMin = 32.0D; ++ public double camelMaxHealthMax = 32.0D; ++ public double camelJumpStrengthMin = 0.42D; ++ public double camelJumpStrengthMax = 0.42D; ++ public double camelMovementSpeedMin = 0.09D; ++ public double camelMovementSpeedMax = 0.09D; ++ private void camelSettings() { ++ camelMaxHealthMin = getDouble("mobs.camel.attributes.max_health.min", camelMaxHealthMin); ++ camelMaxHealthMax = getDouble("mobs.camel.attributes.max_health.max", camelMaxHealthMax); ++ camelJumpStrengthMin = getDouble("mobs.camel.attributes.jump_strength.min", camelJumpStrengthMin); ++ camelJumpStrengthMax = getDouble("mobs.camel.attributes.jump_strength.max", camelJumpStrengthMax); ++ camelMovementSpeedMin = getDouble("mobs.camel.attributes.movement_speed.min", camelMovementSpeedMin); ++ camelMovementSpeedMax = getDouble("mobs.camel.attributes.movement_speed.max", camelMovementSpeedMax); ++ camelBreedingTicks = getInt("mobs.camel.breeding-delay-ticks", camelBreedingTicks); ++ } ++ ++ public boolean catRidable = false; ++ public boolean catRidableInWater = false; ++ public boolean catControllable = true; ++ public double catMaxHealth = 10.0D; ++ public int catSpawnDelay = 1200; ++ public int catSpawnSwampHutScanRange = 16; ++ public int catSpawnVillageScanRange = 48; ++ public int catBreedingTicks = 6000; ++ public DyeColor catDefaultCollarColor = DyeColor.RED; ++ public boolean catTakeDamageFromWater = false; ++ public boolean catAlwaysDropExp = false; ++ private void catSettings() { ++ catRidable = getBoolean("mobs.cat.ridable", catRidable); ++ catRidableInWater = getBoolean("mobs.cat.ridable-in-water", catRidableInWater); ++ catControllable = getBoolean("mobs.cat.controllable", catControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cat.attributes.max-health", catMaxHealth); ++ set("mobs.cat.attributes.max-health", null); ++ set("mobs.cat.attributes.max_health", oldValue); ++ } ++ catMaxHealth = getDouble("mobs.cat.attributes.max_health", catMaxHealth); ++ catSpawnDelay = getInt("mobs.cat.spawn-delay", catSpawnDelay); ++ catSpawnSwampHutScanRange = getInt("mobs.cat.scan-range-for-other-cats.swamp-hut", catSpawnSwampHutScanRange); ++ catSpawnVillageScanRange = getInt("mobs.cat.scan-range-for-other-cats.village", catSpawnVillageScanRange); ++ catBreedingTicks = getInt("mobs.cat.breeding-delay-ticks", catBreedingTicks); ++ try { ++ catDefaultCollarColor = DyeColor.valueOf(getString("mobs.cat.default-collar-color", wolfDefaultCollarColor.name())); ++ } catch (IllegalArgumentException ignore) { ++ catDefaultCollarColor = DyeColor.RED; ++ } ++ catTakeDamageFromWater = getBoolean("mobs.cat.takes-damage-from-water", catTakeDamageFromWater); ++ catAlwaysDropExp = getBoolean("mobs.cat.always-drop-exp", catAlwaysDropExp); ++ } ++ ++ public boolean caveSpiderRidable = false; ++ public boolean caveSpiderRidableInWater = false; ++ public boolean caveSpiderControllable = true; ++ public double caveSpiderMaxHealth = 12.0D; ++ public boolean caveSpiderTakeDamageFromWater = false; ++ public boolean caveSpiderAlwaysDropExp = false; ++ private void caveSpiderSettings() { ++ caveSpiderRidable = getBoolean("mobs.cave_spider.ridable", caveSpiderRidable); ++ caveSpiderRidableInWater = getBoolean("mobs.cave_spider.ridable-in-water", caveSpiderRidableInWater); ++ caveSpiderControllable = getBoolean("mobs.cave_spider.controllable", caveSpiderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cave_spider.attributes.max-health", caveSpiderMaxHealth); ++ set("mobs.cave_spider.attributes.max-health", null); ++ set("mobs.cave_spider.attributes.max_health", oldValue); ++ } ++ caveSpiderMaxHealth = getDouble("mobs.cave_spider.attributes.max_health", caveSpiderMaxHealth); ++ caveSpiderTakeDamageFromWater = getBoolean("mobs.cave_spider.takes-damage-from-water", caveSpiderTakeDamageFromWater); ++ caveSpiderAlwaysDropExp = getBoolean("mobs.cave_spider.always-drop-exp", caveSpiderAlwaysDropExp); ++ } ++ ++ public boolean chickenRidable = false; ++ public boolean chickenRidableInWater = false; ++ public boolean chickenControllable = true; ++ public double chickenMaxHealth = 4.0D; ++ public boolean chickenRetaliate = false; ++ public int chickenBreedingTicks = 6000; ++ public boolean chickenTakeDamageFromWater = false; ++ public boolean chickenAlwaysDropExp = false; ++ private void chickenSettings() { ++ chickenRidable = getBoolean("mobs.chicken.ridable", chickenRidable); ++ chickenRidableInWater = getBoolean("mobs.chicken.ridable-in-water", chickenRidableInWater); ++ chickenControllable = getBoolean("mobs.chicken.controllable", chickenControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.chicken.attributes.max-health", chickenMaxHealth); ++ set("mobs.chicken.attributes.max-health", null); ++ set("mobs.chicken.attributes.max_health", oldValue); ++ } ++ chickenMaxHealth = getDouble("mobs.chicken.attributes.max_health", chickenMaxHealth); ++ chickenRetaliate = getBoolean("mobs.chicken.retaliate", chickenRetaliate); ++ chickenBreedingTicks = getInt("mobs.chicken.breeding-delay-ticks", chickenBreedingTicks); ++ chickenTakeDamageFromWater = getBoolean("mobs.chicken.takes-damage-from-water", chickenTakeDamageFromWater); ++ chickenAlwaysDropExp = getBoolean("mobs.chicken.always-drop-exp", chickenAlwaysDropExp); ++ } ++ ++ public boolean codRidable = false; ++ public boolean codControllable = true; ++ public double codMaxHealth = 3.0D; ++ public boolean codTakeDamageFromWater = false; ++ public boolean codAlwaysDropExp = false; ++ private void codSettings() { ++ codRidable = getBoolean("mobs.cod.ridable", codRidable); ++ codControllable = getBoolean("mobs.cod.controllable", codControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cod.attributes.max-health", codMaxHealth); ++ set("mobs.cod.attributes.max-health", null); ++ set("mobs.cod.attributes.max_health", oldValue); ++ } ++ codMaxHealth = getDouble("mobs.cod.attributes.max_health", codMaxHealth); ++ codTakeDamageFromWater = getBoolean("mobs.cod.takes-damage-from-water", codTakeDamageFromWater); ++ codAlwaysDropExp = getBoolean("mobs.cod.always-drop-exp", codAlwaysDropExp); ++ } ++ ++ public boolean cowRidable = false; ++ public boolean cowRidableInWater = false; ++ public boolean cowControllable = true; ++ public double cowMaxHealth = 10.0D; ++ public int cowFeedMushrooms = 0; ++ public int cowBreedingTicks = 6000; ++ public boolean cowTakeDamageFromWater = false; ++ public double cowNaturallyAggressiveToPlayersChance = 0.0D; ++ public double cowNaturallyAggressiveToPlayersDamage = 2.0D; ++ public boolean cowAlwaysDropExp = false; ++ private void cowSettings() { ++ if (PurpurConfig.version < 22) { ++ double oldValue = getDouble("mobs.cow.naturally-aggressive-to-players-chance", cowNaturallyAggressiveToPlayersChance); ++ set("mobs.cow.naturally-aggressive-to-players-chance", null); ++ set("mobs.cow.naturally-aggressive-to-players.chance", oldValue); ++ } ++ cowRidable = getBoolean("mobs.cow.ridable", cowRidable); ++ cowRidableInWater = getBoolean("mobs.cow.ridable-in-water", cowRidableInWater); ++ cowControllable = getBoolean("mobs.cow.controllable", cowControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cow.attributes.max-health", cowMaxHealth); ++ set("mobs.cow.attributes.max-health", null); ++ set("mobs.cow.attributes.max_health", oldValue); ++ } ++ cowMaxHealth = getDouble("mobs.cow.attributes.max_health", cowMaxHealth); ++ cowFeedMushrooms = getInt("mobs.cow.feed-mushrooms-for-mooshroom", cowFeedMushrooms); ++ cowBreedingTicks = getInt("mobs.cow.breeding-delay-ticks", cowBreedingTicks); ++ cowTakeDamageFromWater = getBoolean("mobs.cow.takes-damage-from-water", cowTakeDamageFromWater); ++ cowNaturallyAggressiveToPlayersChance = getDouble("mobs.cow.naturally-aggressive-to-players.chance", cowNaturallyAggressiveToPlayersChance); ++ cowNaturallyAggressiveToPlayersDamage = getDouble("mobs.cow.naturally-aggressive-to-players.damage", cowNaturallyAggressiveToPlayersDamage); ++ cowAlwaysDropExp = getBoolean("mobs.cow.always-drop-exp", cowAlwaysDropExp); ++ } ++ ++ public boolean creeperRidable = false; ++ public boolean creeperRidableInWater = false; ++ public boolean creeperControllable = true; ++ public double creeperMaxHealth = 20.0D; ++ public double creeperChargedChance = 0.0D; ++ public boolean creeperAllowGriefing = true; ++ public boolean creeperBypassMobGriefing = false; ++ public boolean creeperTakeDamageFromWater = false; ++ public boolean creeperExplodeWhenKilled = false; ++ public boolean creeperHealthRadius = false; ++ public boolean creeperAlwaysDropExp = false; ++ public double creeperHeadVisibilityPercent = 0.5D; ++ public boolean creeperEncircleTarget = false; ++ private void creeperSettings() { ++ creeperRidable = getBoolean("mobs.creeper.ridable", creeperRidable); ++ creeperRidableInWater = getBoolean("mobs.creeper.ridable-in-water", creeperRidableInWater); ++ creeperControllable = getBoolean("mobs.creeper.controllable", creeperControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.creeper.attributes.max-health", creeperMaxHealth); ++ set("mobs.creeper.attributes.max-health", null); ++ set("mobs.creeper.attributes.max_health", oldValue); ++ } ++ creeperMaxHealth = getDouble("mobs.creeper.attributes.max_health", creeperMaxHealth); ++ creeperChargedChance = getDouble("mobs.creeper.naturally-charged-chance", creeperChargedChance); ++ creeperAllowGriefing = getBoolean("mobs.creeper.allow-griefing", creeperAllowGriefing); ++ creeperBypassMobGriefing = getBoolean("mobs.creeper.bypass-mob-griefing", creeperBypassMobGriefing); ++ creeperTakeDamageFromWater = getBoolean("mobs.creeper.takes-damage-from-water", creeperTakeDamageFromWater); ++ creeperExplodeWhenKilled = getBoolean("mobs.creeper.explode-when-killed", creeperExplodeWhenKilled); ++ creeperHealthRadius = getBoolean("mobs.creeper.health-impacts-explosion", creeperHealthRadius); ++ creeperAlwaysDropExp = getBoolean("mobs.creeper.always-drop-exp", creeperAlwaysDropExp); ++ creeperHeadVisibilityPercent = getDouble("mobs.creeper.head-visibility-percent", creeperHeadVisibilityPercent); ++ creeperEncircleTarget = getBoolean("mobs.creeper.encircle-target", creeperEncircleTarget); ++ } ++ ++ public boolean dolphinRidable = false; ++ public boolean dolphinControllable = true; ++ public int dolphinSpitCooldown = 20; ++ public float dolphinSpitSpeed = 1.0F; ++ public float dolphinSpitDamage = 2.0F; ++ public double dolphinMaxHealth = 10.0D; ++ public boolean dolphinDisableTreasureSearching = false; ++ public boolean dolphinTakeDamageFromWater = false; ++ public double dolphinNaturallyAggressiveToPlayersChance = 0.0D; ++ public boolean dolphinAlwaysDropExp = false; ++ private void dolphinSettings() { ++ dolphinRidable = getBoolean("mobs.dolphin.ridable", dolphinRidable); ++ dolphinControllable = getBoolean("mobs.dolphin.controllable", dolphinControllable); ++ dolphinSpitCooldown = getInt("mobs.dolphin.spit.cooldown", dolphinSpitCooldown); ++ dolphinSpitSpeed = (float) getDouble("mobs.dolphin.spit.speed", dolphinSpitSpeed); ++ dolphinSpitDamage = (float) getDouble("mobs.dolphin.spit.damage", dolphinSpitDamage); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.dolphin.attributes.max-health", dolphinMaxHealth); ++ set("mobs.dolphin.attributes.max-health", null); ++ set("mobs.dolphin.attributes.max_health", oldValue); ++ } ++ dolphinMaxHealth = getDouble("mobs.dolphin.attributes.max_health", dolphinMaxHealth); ++ dolphinDisableTreasureSearching = getBoolean("mobs.dolphin.disable-treasure-searching", dolphinDisableTreasureSearching); ++ dolphinTakeDamageFromWater = getBoolean("mobs.dolphin.takes-damage-from-water", dolphinTakeDamageFromWater); ++ dolphinNaturallyAggressiveToPlayersChance = getDouble("mobs.dolphin.naturally-aggressive-to-players-chance", dolphinNaturallyAggressiveToPlayersChance); ++ dolphinAlwaysDropExp = getBoolean("mobs.dolphin.always-drop-exp", dolphinAlwaysDropExp); ++ } ++ ++ public boolean donkeyRidableInWater = false; ++ public double donkeyMaxHealthMin = 15.0D; ++ public double donkeyMaxHealthMax = 30.0D; ++ public double donkeyJumpStrengthMin = 0.5D; ++ public double donkeyJumpStrengthMax = 0.5D; ++ public double donkeyMovementSpeedMin = 0.175D; ++ public double donkeyMovementSpeedMax = 0.175D; ++ public int donkeyBreedingTicks = 6000; ++ public boolean donkeyTakeDamageFromWater = false; ++ public boolean donkeyAlwaysDropExp = false; ++ private void donkeySettings() { ++ donkeyRidableInWater = getBoolean("mobs.donkey.ridable-in-water", donkeyRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.donkey.attributes.max-health.min", donkeyMaxHealthMin); ++ double oldMax = getDouble("mobs.donkey.attributes.max-health.max", donkeyMaxHealthMax); ++ set("mobs.donkey.attributes.max-health", null); ++ set("mobs.donkey.attributes.max_health.min", oldMin); ++ set("mobs.donkey.attributes.max_health.max", oldMax); ++ } ++ donkeyMaxHealthMin = getDouble("mobs.donkey.attributes.max_health.min", donkeyMaxHealthMin); ++ donkeyMaxHealthMax = getDouble("mobs.donkey.attributes.max_health.max", donkeyMaxHealthMax); ++ donkeyJumpStrengthMin = getDouble("mobs.donkey.attributes.jump_strength.min", donkeyJumpStrengthMin); ++ donkeyJumpStrengthMax = getDouble("mobs.donkey.attributes.jump_strength.max", donkeyJumpStrengthMax); ++ donkeyMovementSpeedMin = getDouble("mobs.donkey.attributes.movement_speed.min", donkeyMovementSpeedMin); ++ donkeyMovementSpeedMax = getDouble("mobs.donkey.attributes.movement_speed.max", donkeyMovementSpeedMax); ++ donkeyBreedingTicks = getInt("mobs.donkey.breeding-delay-ticks", donkeyBreedingTicks); ++ donkeyTakeDamageFromWater = getBoolean("mobs.donkey.takes-damage-from-water", donkeyTakeDamageFromWater); ++ donkeyAlwaysDropExp = getBoolean("mobs.donkey.always-drop-exp", donkeyAlwaysDropExp); ++ } ++ ++ public boolean drownedRidable = false; ++ public boolean drownedRidableInWater = false; ++ public boolean drownedControllable = true; ++ public double drownedMaxHealth = 20.0D; ++ public double drownedSpawnReinforcements = 0.1D; ++ public boolean drownedJockeyOnlyBaby = true; ++ public double drownedJockeyChance = 0.05D; ++ public boolean drownedJockeyTryExistingChickens = true; ++ public boolean drownedTakeDamageFromWater = false; ++ public boolean drownedBreakDoors = false; ++ public boolean drownedAlwaysDropExp = false; ++ private void drownedSettings() { ++ drownedRidable = getBoolean("mobs.drowned.ridable", drownedRidable); ++ drownedRidableInWater = getBoolean("mobs.drowned.ridable-in-water", drownedRidableInWater); ++ drownedControllable = getBoolean("mobs.drowned.controllable", drownedControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.drowned.attributes.max-health", drownedMaxHealth); ++ set("mobs.drowned.attributes.max-health", null); ++ set("mobs.drowned.attributes.max_health", oldValue); ++ } ++ drownedMaxHealth = getDouble("mobs.drowned.attributes.max_health", drownedMaxHealth); ++ drownedSpawnReinforcements = getDouble("mobs.drowned.attributes.spawn_reinforcements", drownedSpawnReinforcements); ++ drownedJockeyOnlyBaby = getBoolean("mobs.drowned.jockey.only-babies", drownedJockeyOnlyBaby); ++ drownedJockeyChance = getDouble("mobs.drowned.jockey.chance", drownedJockeyChance); ++ drownedJockeyTryExistingChickens = getBoolean("mobs.drowned.jockey.try-existing-chickens", drownedJockeyTryExistingChickens); ++ drownedTakeDamageFromWater = getBoolean("mobs.drowned.takes-damage-from-water", drownedTakeDamageFromWater); ++ drownedBreakDoors = getBoolean("mobs.drowned.can-break-doors", drownedBreakDoors); ++ drownedAlwaysDropExp = getBoolean("mobs.drowned.always-drop-exp", drownedAlwaysDropExp); ++ } ++ ++ public boolean elderGuardianRidable = false; ++ public boolean elderGuardianControllable = true; ++ public double elderGuardianMaxHealth = 80.0D; ++ public boolean elderGuardianTakeDamageFromWater = false; ++ public boolean elderGuardianAlwaysDropExp = false; ++ private void elderGuardianSettings() { ++ elderGuardianRidable = getBoolean("mobs.elder_guardian.ridable", elderGuardianRidable); ++ elderGuardianControllable = getBoolean("mobs.elder_guardian.controllable", elderGuardianControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.elder_guardian.attributes.max-health", elderGuardianMaxHealth); ++ set("mobs.elder_guardian.attributes.max-health", null); ++ set("mobs.elder_guardian.attributes.max_health", oldValue); ++ } ++ elderGuardianMaxHealth = getDouble("mobs.elder_guardian.attributes.max_health", elderGuardianMaxHealth); ++ elderGuardianTakeDamageFromWater = getBoolean("mobs.elder_guardian.takes-damage-from-water", elderGuardianTakeDamageFromWater); ++ elderGuardianAlwaysDropExp = getBoolean("mobs.elder_guardian.always-drop-exp", elderGuardianAlwaysDropExp); ++ } ++ ++ public boolean enchantmentTableLapisPersists = false; ++ private void enchantmentTableSettings() { ++ enchantmentTableLapisPersists = getBoolean("blocks.enchantment-table.lapis-persists", enchantmentTableLapisPersists); ++ } ++ ++ public boolean enderDragonRidable = false; ++ public boolean enderDragonRidableInWater = false; ++ public boolean enderDragonControllable = true; ++ public double enderDragonMaxY = 320D; ++ public double enderDragonMaxHealth = 200.0D; ++ public boolean enderDragonAlwaysDropsFullExp = false; ++ public boolean enderDragonBypassMobGriefing = false; ++ public boolean enderDragonTakeDamageFromWater = false; ++ public boolean enderDragonCanRideVehicles = false; ++ private void enderDragonSettings() { ++ enderDragonRidable = getBoolean("mobs.ender_dragon.ridable", enderDragonRidable); ++ enderDragonRidableInWater = getBoolean("mobs.ender_dragon.ridable-in-water", enderDragonRidableInWater); ++ enderDragonControllable = getBoolean("mobs.ender_dragon.controllable", enderDragonControllable); ++ enderDragonMaxY = getDouble("mobs.ender_dragon.ridable-max-y", enderDragonMaxY); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.ender_dragon.max-health", enderDragonMaxHealth); ++ set("mobs.ender_dragon.max-health", null); ++ set("mobs.ender_dragon.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ender_dragon.attributes.max-health", enderDragonMaxHealth); ++ set("mobs.ender_dragon.attributes.max-health", null); ++ set("mobs.ender_dragon.attributes.max_health", oldValue); ++ } ++ enderDragonMaxHealth = getDouble("mobs.ender_dragon.attributes.max_health", enderDragonMaxHealth); ++ enderDragonAlwaysDropsFullExp = getBoolean("mobs.ender_dragon.always-drop-full-exp", enderDragonAlwaysDropsFullExp); ++ enderDragonBypassMobGriefing = getBoolean("mobs.ender_dragon.bypass-mob-griefing", enderDragonBypassMobGriefing); ++ enderDragonTakeDamageFromWater = getBoolean("mobs.ender_dragon.takes-damage-from-water", enderDragonTakeDamageFromWater); ++ enderDragonCanRideVehicles = getBoolean("mobs.ender_dragon.can-ride-vehicles", enderDragonCanRideVehicles); ++ } ++ ++ public boolean endermanRidable = false; ++ public boolean endermanRidableInWater = false; ++ public boolean endermanControllable = true; ++ public double endermanMaxHealth = 40.0D; ++ public boolean endermanAllowGriefing = true; ++ public boolean endermanDespawnEvenWithBlock = false; ++ public boolean endermanBypassMobGriefing = false; ++ public boolean endermanTakeDamageFromWater = true; ++ public boolean endermanAggroEndermites = true; ++ public boolean endermanAggroEndermitesOnlyIfPlayerSpawned = false; ++ public boolean endermanIgnorePlayerDragonHead = false; ++ public boolean endermanDisableStareAggro = false; ++ public boolean endermanIgnoreProjectiles = false; ++ public boolean endermanAlwaysDropExp = false; ++ private void endermanSettings() { ++ endermanRidable = getBoolean("mobs.enderman.ridable", endermanRidable); ++ endermanRidableInWater = getBoolean("mobs.enderman.ridable-in-water", endermanRidableInWater); ++ endermanControllable = getBoolean("mobs.enderman.controllable", endermanControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.enderman.attributes.max-health", endermanMaxHealth); ++ set("mobs.enderman.attributes.max-health", null); ++ set("mobs.enderman.attributes.max_health", oldValue); ++ } ++ if (PurpurConfig.version < 15) { ++ // remove old option ++ set("mobs.enderman.aggressive-towards-spawned-endermites", null); ++ } ++ endermanMaxHealth = getDouble("mobs.enderman.attributes.max_health", endermanMaxHealth); ++ endermanAllowGriefing = getBoolean("mobs.enderman.allow-griefing", endermanAllowGriefing); ++ endermanDespawnEvenWithBlock = getBoolean("mobs.enderman.can-despawn-with-held-block", endermanDespawnEvenWithBlock); ++ endermanBypassMobGriefing = getBoolean("mobs.enderman.bypass-mob-griefing", endermanBypassMobGriefing); ++ endermanTakeDamageFromWater = getBoolean("mobs.enderman.takes-damage-from-water", endermanTakeDamageFromWater); ++ endermanAggroEndermites = getBoolean("mobs.enderman.aggressive-towards-endermites", endermanAggroEndermites); ++ endermanAggroEndermitesOnlyIfPlayerSpawned = getBoolean("mobs.enderman.aggressive-towards-endermites-only-spawned-by-player-thrown-ender-pearls", endermanAggroEndermitesOnlyIfPlayerSpawned); ++ endermanIgnorePlayerDragonHead = getBoolean("mobs.enderman.ignore-players-wearing-dragon-head", endermanIgnorePlayerDragonHead); ++ endermanDisableStareAggro = getBoolean("mobs.enderman.disable-player-stare-aggression", endermanDisableStareAggro); ++ endermanIgnoreProjectiles = getBoolean("mobs.enderman.ignore-projectiles", endermanIgnoreProjectiles); ++ endermanAlwaysDropExp = getBoolean("mobs.enderman.always-drop-exp", endermanAlwaysDropExp); ++ } ++ ++ public boolean endermiteRidable = false; ++ public boolean endermiteRidableInWater = false; ++ public boolean endermiteControllable = true; ++ public double endermiteMaxHealth = 8.0D; ++ public boolean endermiteTakeDamageFromWater = false; ++ public boolean endermiteAlwaysDropExp = false; ++ private void endermiteSettings() { ++ endermiteRidable = getBoolean("mobs.endermite.ridable", endermiteRidable); ++ endermiteRidableInWater = getBoolean("mobs.endermite.ridable-in-water", endermiteRidableInWater); ++ endermiteControllable = getBoolean("mobs.endermite.controllable", endermiteControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.endermite.attributes.max-health", endermiteMaxHealth); ++ set("mobs.endermite.attributes.max-health", null); ++ set("mobs.endermite.attributes.max_health", oldValue); ++ } ++ endermiteMaxHealth = getDouble("mobs.endermite.attributes.max_health", endermiteMaxHealth); ++ endermiteTakeDamageFromWater = getBoolean("mobs.endermite.takes-damage-from-water", endermiteTakeDamageFromWater); ++ endermiteAlwaysDropExp = getBoolean("mobs.endermite.always-drop-exp", endermiteAlwaysDropExp); ++ } ++ ++ public boolean evokerRidable = false; ++ public boolean evokerRidableInWater = false; ++ public boolean evokerControllable = true; ++ public double evokerMaxHealth = 24.0D; ++ public boolean evokerBypassMobGriefing = false; ++ public boolean evokerTakeDamageFromWater = false; ++ public boolean evokerAlwaysDropExp = false; ++ private void evokerSettings() { ++ evokerRidable = getBoolean("mobs.evoker.ridable", evokerRidable); ++ evokerRidableInWater = getBoolean("mobs.evoker.ridable-in-water", evokerRidableInWater); ++ evokerControllable = getBoolean("mobs.evoker.controllable", evokerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.evoker.attributes.max-health", evokerMaxHealth); ++ set("mobs.evoker.attributes.max-health", null); ++ set("mobs.evoker.attributes.max_health", oldValue); ++ } ++ evokerMaxHealth = getDouble("mobs.evoker.attributes.max_health", evokerMaxHealth); ++ evokerBypassMobGriefing = getBoolean("mobs.evoker.bypass-mob-griefing", evokerBypassMobGriefing); ++ evokerTakeDamageFromWater = getBoolean("mobs.evoker.takes-damage-from-water", evokerTakeDamageFromWater); ++ evokerAlwaysDropExp = getBoolean("mobs.evoker.always-drop-exp", evokerAlwaysDropExp); ++ } ++ ++ public boolean foxRidable = false; ++ public boolean foxRidableInWater = false; ++ public boolean foxControllable = true; ++ public double foxMaxHealth = 10.0D; ++ public boolean foxTypeChangesWithTulips = false; ++ public int foxBreedingTicks = 6000; ++ public boolean foxBypassMobGriefing = false; ++ public boolean foxTakeDamageFromWater = false; ++ public boolean foxAlwaysDropExp = false; ++ private void foxSettings() { ++ foxRidable = getBoolean("mobs.fox.ridable", foxRidable); ++ foxRidableInWater = getBoolean("mobs.fox.ridable-in-water", foxRidableInWater); ++ foxControllable = getBoolean("mobs.fox.controllable", foxControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.fox.attributes.max-health", foxMaxHealth); ++ set("mobs.fox.attributes.max-health", null); ++ set("mobs.fox.attributes.max_health", oldValue); ++ } ++ foxMaxHealth = getDouble("mobs.fox.attributes.max_health", foxMaxHealth); ++ foxTypeChangesWithTulips = getBoolean("mobs.fox.tulips-change-type", foxTypeChangesWithTulips); ++ foxBreedingTicks = getInt("mobs.fox.breeding-delay-ticks", foxBreedingTicks); ++ foxBypassMobGriefing = getBoolean("mobs.fox.bypass-mob-griefing", foxBypassMobGriefing); ++ foxTakeDamageFromWater = getBoolean("mobs.fox.takes-damage-from-water", foxTakeDamageFromWater); ++ foxAlwaysDropExp = getBoolean("mobs.fox.always-drop-exp", foxAlwaysDropExp); ++ } ++ ++ public boolean frogRidable = false; ++ public boolean frogRidableInWater = false; ++ public boolean frogControllable = true; ++ public float frogRidableJumpHeight = 0.65F; ++ public int frogBreedingTicks = 6000; ++ private void frogSettings() { ++ frogRidable = getBoolean("mobs.frog.ridable", frogRidable); ++ frogRidableInWater = getBoolean("mobs.frog.ridable-in-water", frogRidableInWater); ++ frogControllable = getBoolean("mobs.frog.controllable", frogControllable); ++ frogRidableJumpHeight = (float) getDouble("mobs.frog.ridable-jump-height", frogRidableJumpHeight); ++ frogBreedingTicks = getInt("mobs.frog.breeding-delay-ticks", frogBreedingTicks); ++ } ++ ++ public boolean ghastRidable = false; ++ public boolean ghastRidableInWater = false; ++ public boolean ghastControllable = true; ++ public double ghastMaxY = 320D; ++ public double ghastMaxHealth = 10.0D; ++ public boolean ghastTakeDamageFromWater = false; ++ public boolean ghastAlwaysDropExp = false; ++ private void ghastSettings() { ++ ghastRidable = getBoolean("mobs.ghast.ridable", ghastRidable); ++ ghastRidableInWater = getBoolean("mobs.ghast.ridable-in-water", ghastRidableInWater); ++ ghastControllable = getBoolean("mobs.ghast.controllable", ghastControllable); ++ ghastMaxY = getDouble("mobs.ghast.ridable-max-y", ghastMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ghast.attributes.max-health", ghastMaxHealth); ++ set("mobs.ghast.attributes.max-health", null); ++ set("mobs.ghast.attributes.max_health", oldValue); ++ } ++ ghastMaxHealth = getDouble("mobs.ghast.attributes.max_health", ghastMaxHealth); ++ ghastTakeDamageFromWater = getBoolean("mobs.ghast.takes-damage-from-water", ghastTakeDamageFromWater); ++ ghastAlwaysDropExp = getBoolean("mobs.ghast.always-drop-exp", ghastAlwaysDropExp); ++ } ++ ++ public boolean giantRidable = false; ++ public boolean giantRidableInWater = false; ++ public boolean giantControllable = true; ++ public double giantMovementSpeed = 0.5D; ++ public double giantAttackDamage = 50.0D; ++ public double giantMaxHealth = 100.0D; ++ public float giantStepHeight = 2.0F; ++ public float giantJumpHeight = 1.0F; ++ public boolean giantHaveAI = false; ++ public boolean giantHaveHostileAI = false; ++ public boolean giantTakeDamageFromWater = false; ++ public boolean giantAlwaysDropExp = false; ++ private void giantSettings() { ++ giantRidable = getBoolean("mobs.giant.ridable", giantRidable); ++ giantRidableInWater = getBoolean("mobs.giant.ridable-in-water", giantRidableInWater); ++ giantControllable = getBoolean("mobs.giant.controllable", giantControllable); ++ giantMovementSpeed = getDouble("mobs.giant.movement-speed", giantMovementSpeed); ++ giantAttackDamage = getDouble("mobs.giant.attack-damage", giantAttackDamage); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.giant.max-health", giantMaxHealth); ++ set("mobs.giant.max-health", null); ++ set("mobs.giant.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.giant.attributes.max-health", giantMaxHealth); ++ set("mobs.giant.attributes.max-health", null); ++ set("mobs.giant.attributes.max_health", oldValue); ++ } ++ giantMaxHealth = getDouble("mobs.giant.attributes.max_health", giantMaxHealth); ++ giantStepHeight = (float) getDouble("mobs.giant.step-height", giantStepHeight); ++ giantJumpHeight = (float) getDouble("mobs.giant.jump-height", giantJumpHeight); ++ giantHaveAI = getBoolean("mobs.giant.have-ai", giantHaveAI); ++ giantHaveHostileAI = getBoolean("mobs.giant.have-hostile-ai", giantHaveHostileAI); ++ giantTakeDamageFromWater = getBoolean("mobs.giant.takes-damage-from-water", giantTakeDamageFromWater); ++ giantAlwaysDropExp = getBoolean("mobs.giant.always-drop-exp", giantAlwaysDropExp); ++ } ++ ++ public boolean glowSquidRidable = false; ++ public boolean glowSquidControllable = true; ++ public double glowSquidMaxHealth = 10.0D; ++ public boolean glowSquidsCanFly = false; ++ public boolean glowSquidTakeDamageFromWater = false; ++ public boolean glowSquidAlwaysDropExp = false; ++ public GlowSquidColor.Mode glowSquidColorMode = GlowSquidColor.Mode.RAINBOW; ++ private void glowSquidSettings() { ++ glowSquidRidable = getBoolean("mobs.glow_squid.ridable", glowSquidRidable); ++ glowSquidControllable = getBoolean("mobs.glow_squid.controllable", glowSquidControllable); ++ glowSquidMaxHealth = getDouble("mobs.glow_squid.attributes.max_health", glowSquidMaxHealth); ++ glowSquidsCanFly = getBoolean("mobs.glow_squid.can-fly", glowSquidsCanFly); ++ glowSquidTakeDamageFromWater = getBoolean("mobs.glow_squid.takes-damage-from-water", glowSquidTakeDamageFromWater); ++ glowSquidAlwaysDropExp = getBoolean("mobs.glow_squid.always-drop-exp", glowSquidAlwaysDropExp); ++ glowSquidColorMode = GlowSquidColor.Mode.get(getString("mobs.glow_squid.rainglow-mode", glowSquidColorMode.toString())); ++ } ++ ++ public boolean goatRidable = false; ++ public boolean goatRidableInWater = false; ++ public boolean goatControllable = true; ++ public double goatMaxHealth = 10.0D; ++ public int goatBreedingTicks = 6000; ++ public boolean goatTakeDamageFromWater = false; ++ public boolean goatAlwaysDropExp = false; ++ private void goatSettings() { ++ goatRidable = getBoolean("mobs.goat.ridable", goatRidable); ++ goatRidableInWater = getBoolean("mobs.goat.ridable-in-water", goatRidableInWater); ++ goatControllable = getBoolean("mobs.goat.controllable", goatControllable); ++ goatMaxHealth = getDouble("mobs.goat.attributes.max_health", goatMaxHealth); ++ goatBreedingTicks = getInt("mobs.goat.breeding-delay-ticks", goatBreedingTicks); ++ goatTakeDamageFromWater = getBoolean("mobs.goat.takes-damage-from-water", goatTakeDamageFromWater); ++ goatAlwaysDropExp = getBoolean("mobs.goat.always-drop-exp", goatAlwaysDropExp); ++ } ++ ++ public boolean guardianRidable = false; ++ public boolean guardianControllable = true; ++ public double guardianMaxHealth = 30.0D; ++ public boolean guardianTakeDamageFromWater = false; ++ public boolean guardianAlwaysDropExp = false; ++ private void guardianSettings() { ++ guardianRidable = getBoolean("mobs.guardian.ridable", guardianRidable); ++ guardianControllable = getBoolean("mobs.guardian.controllable", guardianControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.guardian.attributes.max-health", guardianMaxHealth); ++ set("mobs.guardian.attributes.max-health", null); ++ set("mobs.guardian.attributes.max_health", oldValue); ++ } ++ guardianMaxHealth = getDouble("mobs.guardian.attributes.max_health", guardianMaxHealth); ++ guardianTakeDamageFromWater = getBoolean("mobs.guardian.takes-damage-from-water", guardianTakeDamageFromWater); ++ guardianAlwaysDropExp = getBoolean("mobs.guardian.always-drop-exp", guardianAlwaysDropExp); ++ } ++ ++ public boolean forceHalloweenSeason = false; ++ public float chanceHeadHalloweenOnEntity = 0.25F; ++ private void halloweenSetting() { ++ forceHalloweenSeason = getBoolean("gameplay-mechanics.halloween.force", forceHalloweenSeason); ++ chanceHeadHalloweenOnEntity = (float) getDouble("gameplay-mechanics.halloween.head-chance", chanceHeadHalloweenOnEntity); ++ } ++ ++ public boolean hoglinRidable = false; ++ public boolean hoglinRidableInWater = false; ++ public boolean hoglinControllable = true; ++ public double hoglinMaxHealth = 40.0D; ++ public int hoglinBreedingTicks = 6000; ++ public boolean hoglinTakeDamageFromWater = false; ++ public boolean hoglinAlwaysDropExp = false; ++ private void hoglinSettings() { ++ hoglinRidable = getBoolean("mobs.hoglin.ridable", hoglinRidable); ++ hoglinRidableInWater = getBoolean("mobs.hoglin.ridable-in-water", hoglinRidableInWater); ++ hoglinControllable = getBoolean("mobs.hoglin.controllable", hoglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.hoglin.attributes.max-health", hoglinMaxHealth); ++ set("mobs.hoglin.attributes.max-health", null); ++ set("mobs.hoglin.attributes.max_health", oldValue); ++ } ++ hoglinMaxHealth = getDouble("mobs.hoglin.attributes.max_health", hoglinMaxHealth); ++ hoglinBreedingTicks = getInt("mobs.hoglin.breeding-delay-ticks", hoglinBreedingTicks); ++ hoglinTakeDamageFromWater = getBoolean("mobs.hoglin.takes-damage-from-water", hoglinTakeDamageFromWater); ++ hoglinAlwaysDropExp = getBoolean("mobs.hoglin.always-drop-exp", hoglinAlwaysDropExp); ++ } ++ ++ public boolean horseRidableInWater = false; ++ public double horseMaxHealthMin = 15.0D; ++ public double horseMaxHealthMax = 30.0D; ++ public double horseJumpStrengthMin = 0.4D; ++ public double horseJumpStrengthMax = 1.0D; ++ public double horseMovementSpeedMin = 0.1125D; ++ public double horseMovementSpeedMax = 0.3375D; ++ public int horseBreedingTicks = 6000; ++ public boolean horseTakeDamageFromWater = false; ++ public boolean horseAlwaysDropExp = false; ++ private void horseSettings() { ++ horseRidableInWater = getBoolean("mobs.horse.ridable-in-water", horseRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.horse.attributes.max-health.min", horseMaxHealthMin); ++ double oldMax = getDouble("mobs.horse.attributes.max-health.max", horseMaxHealthMax); ++ set("mobs.horse.attributes.max-health", null); ++ set("mobs.horse.attributes.max_health.min", oldMin); ++ set("mobs.horse.attributes.max_health.max", oldMax); ++ } ++ horseMaxHealthMin = getDouble("mobs.horse.attributes.max_health.min", horseMaxHealthMin); ++ horseMaxHealthMax = getDouble("mobs.horse.attributes.max_health.max", horseMaxHealthMax); ++ horseJumpStrengthMin = getDouble("mobs.horse.attributes.jump_strength.min", horseJumpStrengthMin); ++ horseJumpStrengthMax = getDouble("mobs.horse.attributes.jump_strength.max", horseJumpStrengthMax); ++ horseMovementSpeedMin = getDouble("mobs.horse.attributes.movement_speed.min", horseMovementSpeedMin); ++ horseMovementSpeedMax = getDouble("mobs.horse.attributes.movement_speed.max", horseMovementSpeedMax); ++ horseBreedingTicks = getInt("mobs.horse.breeding-delay-ticks", horseBreedingTicks); ++ horseTakeDamageFromWater = getBoolean("mobs.horse.takes-damage-from-water", horseTakeDamageFromWater); ++ horseAlwaysDropExp = getBoolean("mobs.horse.always-drop-exp", horseAlwaysDropExp); ++ } ++ ++ public boolean huskRidable = false; ++ public boolean huskRidableInWater = false; ++ public boolean huskControllable = true; ++ public double huskMaxHealth = 20.0D; ++ public double huskSpawnReinforcements = 0.1D; ++ public boolean huskJockeyOnlyBaby = true; ++ public double huskJockeyChance = 0.05D; ++ public boolean huskJockeyTryExistingChickens = true; ++ public boolean huskTakeDamageFromWater = false; ++ public boolean huskAlwaysDropExp = false; ++ private void huskSettings() { ++ huskRidable = getBoolean("mobs.husk.ridable", huskRidable); ++ huskRidableInWater = getBoolean("mobs.husk.ridable-in-water", huskRidableInWater); ++ huskControllable = getBoolean("mobs.husk.controllable", huskControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.husk.attributes.max-health", huskMaxHealth); ++ set("mobs.husk.attributes.max-health", null); ++ set("mobs.husk.attributes.max_health", oldValue); ++ } ++ huskMaxHealth = getDouble("mobs.husk.attributes.max_health", huskMaxHealth); ++ huskSpawnReinforcements = getDouble("mobs.husk.attributes.spawn_reinforcements", huskSpawnReinforcements); ++ huskJockeyOnlyBaby = getBoolean("mobs.husk.jockey.only-babies", huskJockeyOnlyBaby); ++ huskJockeyChance = getDouble("mobs.husk.jockey.chance", huskJockeyChance); ++ huskJockeyTryExistingChickens = getBoolean("mobs.husk.jockey.try-existing-chickens", huskJockeyTryExistingChickens); ++ huskTakeDamageFromWater = getBoolean("mobs.husk.takes-damage-from-water", huskTakeDamageFromWater); ++ huskAlwaysDropExp = getBoolean("mobs.husk.always-drop-exp", huskAlwaysDropExp); ++ } ++ ++ public boolean illusionerRidable = false; ++ public boolean illusionerRidableInWater = false; ++ public boolean illusionerControllable = true; ++ public double illusionerMovementSpeed = 0.5D; ++ public double illusionerFollowRange = 18.0D; ++ public double illusionerMaxHealth = 32.0D; ++ public boolean illusionerTakeDamageFromWater = false; ++ public boolean illusionerAlwaysDropExp = false; ++ private void illusionerSettings() { ++ illusionerRidable = getBoolean("mobs.illusioner.ridable", illusionerRidable); ++ illusionerRidableInWater = getBoolean("mobs.illusioner.ridable-in-water", illusionerRidableInWater); ++ illusionerControllable = getBoolean("mobs.illusioner.controllable", illusionerControllable); ++ illusionerMovementSpeed = getDouble("mobs.illusioner.movement-speed", illusionerMovementSpeed); ++ illusionerFollowRange = getDouble("mobs.illusioner.follow-range", illusionerFollowRange); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.illusioner.max-health", illusionerMaxHealth); ++ set("mobs.illusioner.max-health", null); ++ set("mobs.illusioner.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.illusioner.attributes.max-health", illusionerMaxHealth); ++ set("mobs.illusioner.attributes.max-health", null); ++ set("mobs.illusioner.attributes.max_health", oldValue); ++ } ++ illusionerMaxHealth = getDouble("mobs.illusioner.attributes.max_health", illusionerMaxHealth); ++ illusionerTakeDamageFromWater = getBoolean("mobs.illusioner.takes-damage-from-water", illusionerTakeDamageFromWater); ++ illusionerAlwaysDropExp = getBoolean("mobs.illusioner.always-drop-exp", illusionerAlwaysDropExp); ++ } ++ ++ public boolean ironGolemRidable = false; ++ public boolean ironGolemRidableInWater = false; ++ public boolean ironGolemControllable = true; ++ public boolean ironGolemCanSwim = false; ++ public double ironGolemMaxHealth = 100.0D; ++ public boolean ironGolemTakeDamageFromWater = false; ++ public boolean ironGolemPoppyCalm = false; ++ public boolean ironGolemHealCalm = false; ++ public boolean ironGolemAlwaysDropExp = false; ++ private void ironGolemSettings() { ++ ironGolemRidable = getBoolean("mobs.iron_golem.ridable", ironGolemRidable); ++ ironGolemRidableInWater = getBoolean("mobs.iron_golem.ridable-in-water", ironGolemRidableInWater); ++ ironGolemControllable = getBoolean("mobs.iron_golem.controllable", ironGolemControllable); ++ ironGolemCanSwim = getBoolean("mobs.iron_golem.can-swim", ironGolemCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.iron_golem.attributes.max-health", ironGolemMaxHealth); ++ set("mobs.iron_golem.attributes.max-health", null); ++ set("mobs.iron_golem.attributes.max_health", oldValue); ++ } ++ ironGolemMaxHealth = getDouble("mobs.iron_golem.attributes.max_health", ironGolemMaxHealth); ++ ironGolemTakeDamageFromWater = getBoolean("mobs.iron_golem.takes-damage-from-water", ironGolemTakeDamageFromWater); ++ ironGolemPoppyCalm = getBoolean("mobs.iron_golem.poppy-calms-anger", ironGolemPoppyCalm); ++ ironGolemHealCalm = getBoolean("mobs.iron_golem.healing-calms-anger", ironGolemHealCalm); ++ ironGolemAlwaysDropExp = getBoolean("mobs.iron_golem.always-drop-exp", ironGolemAlwaysDropExp); ++ } ++ ++ public boolean llamaRidable = false; ++ public boolean llamaRidableInWater = false; ++ public boolean llamaControllable = true; ++ public double llamaMaxHealthMin = 15.0D; ++ public double llamaMaxHealthMax = 30.0D; ++ public double llamaJumpStrengthMin = 0.5D; ++ public double llamaJumpStrengthMax = 0.5D; ++ public double llamaMovementSpeedMin = 0.175D; ++ public double llamaMovementSpeedMax = 0.175D; ++ public int llamaBreedingTicks = 6000; ++ public boolean llamaTakeDamageFromWater = false; ++ public boolean llamaJoinCaravans = true; ++ public boolean llamaAlwaysDropExp = false; ++ private void llamaSettings() { ++ llamaRidable = getBoolean("mobs.llama.ridable", llamaRidable); ++ llamaRidableInWater = getBoolean("mobs.llama.ridable-in-water", llamaRidableInWater); ++ llamaControllable = getBoolean("mobs.llama.controllable", llamaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.llama.attributes.max-health.min", llamaMaxHealthMin); ++ double oldMax = getDouble("mobs.llama.attributes.max-health.max", llamaMaxHealthMax); ++ set("mobs.llama.attributes.max-health", null); ++ set("mobs.llama.attributes.max_health.min", oldMin); ++ set("mobs.llama.attributes.max_health.max", oldMax); ++ } ++ llamaMaxHealthMin = getDouble("mobs.llama.attributes.max_health.min", llamaMaxHealthMin); ++ llamaMaxHealthMax = getDouble("mobs.llama.attributes.max_health.max", llamaMaxHealthMax); ++ llamaJumpStrengthMin = getDouble("mobs.llama.attributes.jump_strength.min", llamaJumpStrengthMin); ++ llamaJumpStrengthMax = getDouble("mobs.llama.attributes.jump_strength.max", llamaJumpStrengthMax); ++ llamaMovementSpeedMin = getDouble("mobs.llama.attributes.movement_speed.min", llamaMovementSpeedMin); ++ llamaMovementSpeedMax = getDouble("mobs.llama.attributes.movement_speed.max", llamaMovementSpeedMax); ++ llamaBreedingTicks = getInt("mobs.llama.breeding-delay-ticks", llamaBreedingTicks); ++ llamaTakeDamageFromWater = getBoolean("mobs.llama.takes-damage-from-water", llamaTakeDamageFromWater); ++ llamaJoinCaravans = getBoolean("mobs.llama.join-caravans", llamaJoinCaravans); ++ llamaAlwaysDropExp = getBoolean("mobs.llama.always-drop-exp", llamaAlwaysDropExp); ++ } ++ ++ public boolean magmaCubeRidable = false; ++ public boolean magmaCubeRidableInWater = false; ++ public boolean magmaCubeControllable = true; ++ public String magmaCubeMaxHealth = "size * size"; ++ public String magmaCubeAttackDamage = "size"; ++ public Map magmaCubeMaxHealthCache = new HashMap<>(); ++ public Map magmaCubeAttackDamageCache = new HashMap<>(); ++ public boolean magmaCubeTakeDamageFromWater = false; ++ public boolean magmaCubeAlwaysDropExp = false; ++ private void magmaCubeSettings() { ++ magmaCubeRidable = getBoolean("mobs.magma_cube.ridable", magmaCubeRidable); ++ magmaCubeRidableInWater = getBoolean("mobs.magma_cube.ridable-in-water", magmaCubeRidableInWater); ++ magmaCubeControllable = getBoolean("mobs.magma_cube.controllable", magmaCubeControllable); ++ if (PurpurConfig.version < 10) { ++ String oldValue = getString("mobs.magma_cube.attributes.max-health", magmaCubeMaxHealth); ++ set("mobs.magma_cube.attributes.max-health", null); ++ set("mobs.magma_cube.attributes.max_health", oldValue); ++ } ++ magmaCubeMaxHealth = getString("mobs.magma_cube.attributes.max_health", magmaCubeMaxHealth); ++ magmaCubeAttackDamage = getString("mobs.magma_cube.attributes.attack_damage", magmaCubeAttackDamage); ++ magmaCubeMaxHealthCache.clear(); ++ magmaCubeAttackDamageCache.clear(); ++ magmaCubeTakeDamageFromWater = getBoolean("mobs.magma_cube.takes-damage-from-water", magmaCubeTakeDamageFromWater); ++ magmaCubeAlwaysDropExp = getBoolean("mobs.magma_cube.always-drop-exp", magmaCubeAlwaysDropExp); ++ } ++ ++ public boolean mooshroomRidable = false; ++ public boolean mooshroomRidableInWater = false; ++ public boolean mooshroomControllable = true; ++ public double mooshroomMaxHealth = 10.0D; ++ public int mooshroomBreedingTicks = 6000; ++ public boolean mooshroomTakeDamageFromWater = false; ++ public boolean mooshroomAlwaysDropExp = false; ++ private void mooshroomSettings() { ++ mooshroomRidable = getBoolean("mobs.mooshroom.ridable", mooshroomRidable); ++ mooshroomRidableInWater = getBoolean("mobs.mooshroom.ridable-in-water", mooshroomRidableInWater); ++ mooshroomControllable = getBoolean("mobs.mooshroom.controllable", mooshroomControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.mooshroom.attributes.max-health", mooshroomMaxHealth); ++ set("mobs.mooshroom.attributes.max-health", null); ++ set("mobs.mooshroom.attributes.max_health", oldValue); ++ } ++ mooshroomMaxHealth = getDouble("mobs.mooshroom.attributes.max_health", mooshroomMaxHealth); ++ mooshroomBreedingTicks = getInt("mobs.mooshroom.breeding-delay-ticks", mooshroomBreedingTicks); ++ mooshroomTakeDamageFromWater = getBoolean("mobs.mooshroom.takes-damage-from-water", mooshroomTakeDamageFromWater); ++ mooshroomAlwaysDropExp = getBoolean("mobs.mooshroom.always-drop-exp", mooshroomAlwaysDropExp); ++ } ++ ++ public boolean muleRidableInWater = false; ++ public double muleMaxHealthMin = 15.0D; ++ public double muleMaxHealthMax = 30.0D; ++ public double muleJumpStrengthMin = 0.5D; ++ public double muleJumpStrengthMax = 0.5D; ++ public double muleMovementSpeedMin = 0.175D; ++ public double muleMovementSpeedMax = 0.175D; ++ public int muleBreedingTicks = 6000; ++ public boolean muleTakeDamageFromWater = false; ++ public boolean muleAlwaysDropExp = false; ++ private void muleSettings() { ++ muleRidableInWater = getBoolean("mobs.mule.ridable-in-water", muleRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.mule.attributes.max-health.min", muleMaxHealthMin); ++ double oldMax = getDouble("mobs.mule.attributes.max-health.max", muleMaxHealthMax); ++ set("mobs.mule.attributes.max-health", null); ++ set("mobs.mule.attributes.max_health.min", oldMin); ++ set("mobs.mule.attributes.max_health.max", oldMax); ++ } ++ muleMaxHealthMin = getDouble("mobs.mule.attributes.max_health.min", muleMaxHealthMin); ++ muleMaxHealthMax = getDouble("mobs.mule.attributes.max_health.max", muleMaxHealthMax); ++ muleJumpStrengthMin = getDouble("mobs.mule.attributes.jump_strength.min", muleJumpStrengthMin); ++ muleJumpStrengthMax = getDouble("mobs.mule.attributes.jump_strength.max", muleJumpStrengthMax); ++ muleMovementSpeedMin = getDouble("mobs.mule.attributes.movement_speed.min", muleMovementSpeedMin); ++ muleMovementSpeedMax = getDouble("mobs.mule.attributes.movement_speed.max", muleMovementSpeedMax); ++ muleBreedingTicks = getInt("mobs.mule.breeding-delay-ticks", muleBreedingTicks); ++ muleTakeDamageFromWater = getBoolean("mobs.mule.takes-damage-from-water", muleTakeDamageFromWater); ++ muleAlwaysDropExp = getBoolean("mobs.mule.always-drop-exp", muleAlwaysDropExp); ++ } ++ ++ public boolean ocelotRidable = false; ++ public boolean ocelotRidableInWater = false; ++ public boolean ocelotControllable = true; ++ public double ocelotMaxHealth = 10.0D; ++ public int ocelotBreedingTicks = 6000; ++ public boolean ocelotTakeDamageFromWater = false; ++ public boolean ocelotAlwaysDropExp = false; ++ private void ocelotSettings() { ++ ocelotRidable = getBoolean("mobs.ocelot.ridable", ocelotRidable); ++ ocelotRidableInWater = getBoolean("mobs.ocelot.ridable-in-water", ocelotRidableInWater); ++ ocelotControllable = getBoolean("mobs.ocelot.controllable", ocelotControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ocelot.attributes.max-health", ocelotMaxHealth); ++ set("mobs.ocelot.attributes.max-health", null); ++ set("mobs.ocelot.attributes.max_health", oldValue); ++ } ++ ocelotMaxHealth = getDouble("mobs.ocelot.attributes.max_health", ocelotMaxHealth); ++ ocelotBreedingTicks = getInt("mobs.ocelot.breeding-delay-ticks", ocelotBreedingTicks); ++ ocelotTakeDamageFromWater = getBoolean("mobs.ocelot.takes-damage-from-water", ocelotTakeDamageFromWater); ++ ocelotAlwaysDropExp = getBoolean("mobs.ocelot.always-drop-exp", ocelotAlwaysDropExp); ++ } ++ ++ public boolean pandaRidable = false; ++ public boolean pandaRidableInWater = false; ++ public boolean pandaControllable = true; ++ public double pandaMaxHealth = 20.0D; ++ public int pandaBreedingTicks = 6000; ++ public boolean pandaTakeDamageFromWater = false; ++ public boolean pandaAlwaysDropExp = false; ++ private void pandaSettings() { ++ pandaRidable = getBoolean("mobs.panda.ridable", pandaRidable); ++ pandaRidableInWater = getBoolean("mobs.panda.ridable-in-water", pandaRidableInWater); ++ pandaControllable = getBoolean("mobs.panda.controllable", pandaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.panda.attributes.max-health", pandaMaxHealth); ++ set("mobs.panda.attributes.max-health", null); ++ set("mobs.panda.attributes.max_health", oldValue); ++ } ++ pandaMaxHealth = getDouble("mobs.panda.attributes.max_health", pandaMaxHealth); ++ pandaBreedingTicks = getInt("mobs.panda.breeding-delay-ticks", pandaBreedingTicks); ++ pandaTakeDamageFromWater = getBoolean("mobs.panda.takes-damage-from-water", pandaTakeDamageFromWater); ++ pandaAlwaysDropExp = getBoolean("mobs.panda.always-drop-exp", pandaAlwaysDropExp); ++ } ++ ++ public boolean parrotRidable = false; ++ public boolean parrotRidableInWater = false; ++ public boolean parrotControllable = true; ++ public double parrotMaxY = 320D; ++ public double parrotMaxHealth = 6.0D; ++ public boolean parrotTakeDamageFromWater = false; ++ public boolean parrotBreedable = false; ++ public boolean parrotAlwaysDropExp = false; ++ private void parrotSettings() { ++ parrotRidable = getBoolean("mobs.parrot.ridable", parrotRidable); ++ parrotRidableInWater = getBoolean("mobs.parrot.ridable-in-water", parrotRidableInWater); ++ parrotControllable = getBoolean("mobs.parrot.controllable", parrotControllable); ++ parrotMaxY = getDouble("mobs.parrot.ridable-max-y", parrotMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.parrot.attributes.max-health", parrotMaxHealth); ++ set("mobs.parrot.attributes.max-health", null); ++ set("mobs.parrot.attributes.max_health", oldValue); ++ } ++ parrotMaxHealth = getDouble("mobs.parrot.attributes.max_health", parrotMaxHealth); ++ parrotTakeDamageFromWater = getBoolean("mobs.parrot.takes-damage-from-water", parrotTakeDamageFromWater); ++ parrotBreedable = getBoolean("mobs.parrot.can-breed", parrotBreedable); ++ parrotAlwaysDropExp = getBoolean("mobs.parrot.always-drop-exp", parrotAlwaysDropExp); ++ } ++ ++ public boolean phantomRidable = false; ++ public boolean phantomRidableInWater = false; ++ public boolean phantomControllable = true; ++ public double phantomMaxY = 320D; ++ public float phantomFlameDamage = 1.0F; ++ public int phantomFlameFireTime = 8; ++ public boolean phantomAllowGriefing = false; ++ public String phantomMaxHealth = "20.0"; ++ public String phantomAttackDamage = "6 + size"; ++ public Map phantomMaxHealthCache = new HashMap<>(); ++ public Map phantomAttackDamageCache = new HashMap<>(); ++ public double phantomAttackedByCrystalRadius = 0.0D; ++ public float phantomAttackedByCrystalDamage = 1.0F; ++ public double phantomOrbitCrystalRadius = 0.0D; ++ public int phantomSpawnMinSkyDarkness = 5; ++ public boolean phantomSpawnOnlyAboveSeaLevel = true; ++ public boolean phantomSpawnOnlyWithVisibleSky = true; ++ public double phantomSpawnLocalDifficultyChance = 3.0D; ++ public int phantomSpawnMinPerAttempt = 1; ++ public int phantomSpawnMaxPerAttempt = -1; ++ public int phantomBurnInLight = 0; ++ public boolean phantomIgnorePlayersWithTorch = false; ++ public boolean phantomBurnInDaylight = true; ++ public boolean phantomFlamesOnSwoop = false; ++ public boolean phantomTakeDamageFromWater = false; ++ public boolean phantomAlwaysDropExp = false; ++ public int phantomMinSize = 0; ++ public int phantomMaxSize = 0; ++ private void phantomSettings() { ++ phantomRidable = getBoolean("mobs.phantom.ridable", phantomRidable); ++ phantomRidableInWater = getBoolean("mobs.phantom.ridable-in-water", phantomRidableInWater); ++ phantomControllable = getBoolean("mobs.phantom.controllable", phantomControllable); ++ phantomMaxY = getDouble("mobs.phantom.ridable-max-y", phantomMaxY); ++ phantomFlameDamage = (float) getDouble("mobs.phantom.flames.damage", phantomFlameDamage); ++ phantomFlameFireTime = getInt("mobs.phantom.flames.fire-time", phantomFlameFireTime); ++ phantomAllowGriefing = getBoolean("mobs.phantom.allow-griefing", phantomAllowGriefing); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.phantom.attributes.max-health", Double.parseDouble(phantomMaxHealth)); ++ set("mobs.phantom.attributes.max-health", null); ++ set("mobs.phantom.attributes.max_health", String.valueOf(oldValue)); ++ } ++ if (PurpurConfig.version < 25) { ++ double oldValue = getDouble("mobs.phantom.attributes.max_health", Double.parseDouble(phantomMaxHealth)); ++ set("mobs.phantom.attributes.max_health", String.valueOf(oldValue)); ++ } ++ phantomMaxHealth = getString("mobs.phantom.attributes.max_health", phantomMaxHealth); ++ phantomAttackDamage = getString("mobs.phantom.attributes.attack_damage", phantomAttackDamage); ++ phantomMaxHealthCache.clear(); ++ phantomAttackDamageCache.clear(); ++ phantomAttackedByCrystalRadius = getDouble("mobs.phantom.attacked-by-crystal-range", phantomAttackedByCrystalRadius); ++ phantomAttackedByCrystalDamage = (float) getDouble("mobs.phantom.attacked-by-crystal-damage", phantomAttackedByCrystalDamage); ++ phantomOrbitCrystalRadius = getDouble("mobs.phantom.orbit-crystal-radius", phantomOrbitCrystalRadius); ++ phantomSpawnMinSkyDarkness = getInt("mobs.phantom.spawn.min-sky-darkness", phantomSpawnMinSkyDarkness); ++ phantomSpawnOnlyAboveSeaLevel = getBoolean("mobs.phantom.spawn.only-above-sea-level", phantomSpawnOnlyAboveSeaLevel); ++ phantomSpawnOnlyWithVisibleSky = getBoolean("mobs.phantom.spawn.only-with-visible-sky", phantomSpawnOnlyWithVisibleSky); ++ phantomSpawnLocalDifficultyChance = getDouble("mobs.phantom.spawn.local-difficulty-chance", phantomSpawnLocalDifficultyChance); ++ phantomSpawnMinPerAttempt = getInt("mobs.phantom.spawn.per-attempt.min", phantomSpawnMinPerAttempt); ++ phantomSpawnMaxPerAttempt = getInt("mobs.phantom.spawn.per-attempt.max", phantomSpawnMaxPerAttempt); ++ phantomBurnInLight = getInt("mobs.phantom.burn-in-light", phantomBurnInLight); ++ phantomBurnInDaylight = getBoolean("mobs.phantom.burn-in-daylight", phantomBurnInDaylight); ++ phantomIgnorePlayersWithTorch = getBoolean("mobs.phantom.ignore-players-with-torch", phantomIgnorePlayersWithTorch); ++ phantomFlamesOnSwoop = getBoolean("mobs.phantom.flames-on-swoop", phantomFlamesOnSwoop); ++ phantomTakeDamageFromWater = getBoolean("mobs.phantom.takes-damage-from-water", phantomTakeDamageFromWater); ++ phantomAlwaysDropExp = getBoolean("mobs.phantom.always-drop-exp", phantomAlwaysDropExp); ++ phantomMinSize = Mth.clamp(getInt("mobs.phantom.size.min", phantomMinSize), 0, 64); ++ phantomMaxSize = Mth.clamp(getInt("mobs.phantom.size.max", phantomMaxSize), 0, 64); ++ if (phantomMinSize > phantomMaxSize) { ++ phantomMinSize = phantomMinSize ^ phantomMaxSize; ++ phantomMaxSize = phantomMinSize ^ phantomMaxSize; ++ phantomMinSize = phantomMinSize ^ phantomMaxSize; ++ } ++ } ++ ++ public boolean pigRidable = false; ++ public boolean pigRidableInWater = false; ++ public boolean pigControllable = true; ++ public double pigMaxHealth = 10.0D; ++ public boolean pigGiveSaddleBack = false; ++ public int pigBreedingTicks = 6000; ++ public boolean pigTakeDamageFromWater = false; ++ public boolean pigAlwaysDropExp = false; ++ private void pigSettings() { ++ pigRidable = getBoolean("mobs.pig.ridable", pigRidable); ++ pigRidableInWater = getBoolean("mobs.pig.ridable-in-water", pigRidableInWater); ++ pigControllable = getBoolean("mobs.pig.controllable", pigControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pig.attributes.max-health", pigMaxHealth); ++ set("mobs.pig.attributes.max-health", null); ++ set("mobs.pig.attributes.max_health", oldValue); ++ } ++ pigMaxHealth = getDouble("mobs.pig.attributes.max_health", pigMaxHealth); ++ pigGiveSaddleBack = getBoolean("mobs.pig.give-saddle-back", pigGiveSaddleBack); ++ pigBreedingTicks = getInt("mobs.pig.breeding-delay-ticks", pigBreedingTicks); ++ pigTakeDamageFromWater = getBoolean("mobs.pig.takes-damage-from-water", pigTakeDamageFromWater); ++ pigAlwaysDropExp = getBoolean("mobs.pig.always-drop-exp", pigAlwaysDropExp); ++ } ++ ++ public boolean piglinRidable = false; ++ public boolean piglinRidableInWater = false; ++ public boolean piglinControllable = true; ++ public double piglinMaxHealth = 16.0D; ++ public boolean piglinBypassMobGriefing = false; ++ public boolean piglinTakeDamageFromWater = false; ++ public int piglinPortalSpawnModifier = 2000; ++ public boolean piglinAlwaysDropExp = false; ++ public double piglinHeadVisibilityPercent = 0.5D; ++ private void piglinSettings() { ++ piglinRidable = getBoolean("mobs.piglin.ridable", piglinRidable); ++ piglinRidableInWater = getBoolean("mobs.piglin.ridable-in-water", piglinRidableInWater); ++ piglinControllable = getBoolean("mobs.piglin.controllable", piglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.piglin.attributes.max-health", piglinMaxHealth); ++ set("mobs.piglin.attributes.max-health", null); ++ set("mobs.piglin.attributes.max_health", oldValue); ++ } ++ piglinMaxHealth = getDouble("mobs.piglin.attributes.max_health", piglinMaxHealth); ++ piglinBypassMobGriefing = getBoolean("mobs.piglin.bypass-mob-griefing", piglinBypassMobGriefing); ++ piglinTakeDamageFromWater = getBoolean("mobs.piglin.takes-damage-from-water", piglinTakeDamageFromWater); ++ piglinPortalSpawnModifier = getInt("mobs.piglin.portal-spawn-modifier", piglinPortalSpawnModifier); ++ piglinAlwaysDropExp = getBoolean("mobs.piglin.always-drop-exp", piglinAlwaysDropExp); ++ piglinHeadVisibilityPercent = getDouble("mobs.piglin.head-visibility-percent", piglinHeadVisibilityPercent); ++ } ++ ++ public boolean piglinBruteRidable = false; ++ public boolean piglinBruteRidableInWater = false; ++ public boolean piglinBruteControllable = true; ++ public double piglinBruteMaxHealth = 50.0D; ++ public boolean piglinBruteTakeDamageFromWater = false; ++ public boolean piglinBruteAlwaysDropExp = false; ++ private void piglinBruteSettings() { ++ piglinBruteRidable = getBoolean("mobs.piglin_brute.ridable", piglinBruteRidable); ++ piglinBruteRidableInWater = getBoolean("mobs.piglin_brute.ridable-in-water", piglinBruteRidableInWater); ++ piglinBruteControllable = getBoolean("mobs.piglin_brute.controllable", piglinBruteControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.piglin_brute.attributes.max-health", piglinBruteMaxHealth); ++ set("mobs.piglin_brute.attributes.max-health", null); ++ set("mobs.piglin_brute.attributes.max_health", oldValue); ++ } ++ piglinBruteMaxHealth = getDouble("mobs.piglin_brute.attributes.max_health", piglinBruteMaxHealth); ++ piglinBruteTakeDamageFromWater = getBoolean("mobs.piglin_brute.takes-damage-from-water", piglinBruteTakeDamageFromWater); ++ piglinBruteAlwaysDropExp = getBoolean("mobs.piglin_brute.always-drop-exp", piglinBruteAlwaysDropExp); ++ } ++ ++ public boolean pillagerRidable = false; ++ public boolean pillagerRidableInWater = false; ++ public boolean pillagerControllable = true; ++ public double pillagerMaxHealth = 24.0D; ++ public boolean pillagerBypassMobGriefing = false; ++ public boolean pillagerTakeDamageFromWater = false; ++ public boolean pillagerAlwaysDropExp = false; ++ private void pillagerSettings() { ++ pillagerRidable = getBoolean("mobs.pillager.ridable", pillagerRidable); ++ pillagerRidableInWater = getBoolean("mobs.pillager.ridable-in-water", pillagerRidableInWater); ++ pillagerControllable = getBoolean("mobs.pillager.controllable", pillagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pillager.attributes.max-health", pillagerMaxHealth); ++ set("mobs.pillager.attributes.max-health", null); ++ set("mobs.pillager.attributes.max_health", oldValue); ++ } ++ pillagerMaxHealth = getDouble("mobs.pillager.attributes.max_health", pillagerMaxHealth); ++ pillagerBypassMobGriefing = getBoolean("mobs.pillager.bypass-mob-griefing", pillagerBypassMobGriefing); ++ pillagerTakeDamageFromWater = getBoolean("mobs.pillager.takes-damage-from-water", pillagerTakeDamageFromWater); ++ pillagerAlwaysDropExp = getBoolean("mobs.pillager.always-drop-exp", pillagerAlwaysDropExp); ++ } ++ ++ public boolean polarBearRidable = false; ++ public boolean polarBearRidableInWater = false; ++ public boolean polarBearControllable = true; ++ public double polarBearMaxHealth = 30.0D; ++ public String polarBearBreedableItemString = ""; ++ public Item polarBearBreedableItem = null; ++ public int polarBearBreedingTicks = 6000; ++ public boolean polarBearTakeDamageFromWater = false; ++ public boolean polarBearAlwaysDropExp = false; ++ private void polarBearSettings() { ++ polarBearRidable = getBoolean("mobs.polar_bear.ridable", polarBearRidable); ++ polarBearRidableInWater = getBoolean("mobs.polar_bear.ridable-in-water", polarBearRidableInWater); ++ polarBearControllable = getBoolean("mobs.polar_bear.controllable", polarBearControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.polar_bear.attributes.max-health", polarBearMaxHealth); ++ set("mobs.polar_bear.attributes.max-health", null); ++ set("mobs.polar_bear.attributes.max_health", oldValue); ++ } ++ polarBearMaxHealth = getDouble("mobs.polar_bear.attributes.max_health", polarBearMaxHealth); ++ polarBearBreedableItemString = getString("mobs.polar_bear.breedable-item", polarBearBreedableItemString); ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(polarBearBreedableItemString)); ++ if (item != Items.AIR) polarBearBreedableItem = item; ++ polarBearBreedingTicks = getInt("mobs.polar_bear.breeding-delay-ticks", polarBearBreedingTicks); ++ polarBearTakeDamageFromWater = getBoolean("mobs.polar_bear.takes-damage-from-water", polarBearTakeDamageFromWater); ++ polarBearAlwaysDropExp = getBoolean("mobs.polar_bear.always-drop-exp", polarBearAlwaysDropExp); ++ } ++ ++ public boolean pufferfishRidable = false; ++ public boolean pufferfishControllable = true; ++ public double pufferfishMaxHealth = 3.0D; ++ public boolean pufferfishTakeDamageFromWater = false; ++ public boolean pufferfishAlwaysDropExp = false; ++ private void pufferfishSettings() { ++ pufferfishRidable = getBoolean("mobs.pufferfish.ridable", pufferfishRidable); ++ pufferfishControllable = getBoolean("mobs.pufferfish.controllable", pufferfishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pufferfish.attributes.max-health", pufferfishMaxHealth); ++ set("mobs.pufferfish.attributes.max-health", null); ++ set("mobs.pufferfish.attributes.max_health", oldValue); ++ } ++ pufferfishMaxHealth = getDouble("mobs.pufferfish.attributes.max_health", pufferfishMaxHealth); ++ pufferfishTakeDamageFromWater = getBoolean("mobs.pufferfish.takes-damage-from-water", pufferfishTakeDamageFromWater); ++ pufferfishAlwaysDropExp = getBoolean("mobs.pufferfish.always-drop-exp", pufferfishAlwaysDropExp); ++ } ++ ++ public boolean rabbitRidable = false; ++ public boolean rabbitRidableInWater = false; ++ public boolean rabbitControllable = true; ++ public double rabbitMaxHealth = 3.0D; ++ public double rabbitNaturalToast = 0.0D; ++ public double rabbitNaturalKiller = 0.0D; ++ public int rabbitBreedingTicks = 6000; ++ public boolean rabbitBypassMobGriefing = false; ++ public boolean rabbitTakeDamageFromWater = false; ++ public boolean rabbitAlwaysDropExp = false; ++ private void rabbitSettings() { ++ rabbitRidable = getBoolean("mobs.rabbit.ridable", rabbitRidable); ++ rabbitRidableInWater = getBoolean("mobs.rabbit.ridable-in-water", rabbitRidableInWater); ++ rabbitControllable = getBoolean("mobs.rabbit.controllable", rabbitControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.rabbit.attributes.max-health", rabbitMaxHealth); ++ set("mobs.rabbit.attributes.max-health", null); ++ set("mobs.rabbit.attributes.max_health", oldValue); ++ } ++ rabbitMaxHealth = getDouble("mobs.rabbit.attributes.max_health", rabbitMaxHealth); ++ rabbitNaturalToast = getDouble("mobs.rabbit.spawn-toast-chance", rabbitNaturalToast); ++ rabbitNaturalKiller = getDouble("mobs.rabbit.spawn-killer-rabbit-chance", rabbitNaturalKiller); ++ rabbitBreedingTicks = getInt("mobs.rabbit.breeding-delay-ticks", rabbitBreedingTicks); ++ rabbitBypassMobGriefing = getBoolean("mobs.rabbit.bypass-mob-griefing", rabbitBypassMobGriefing); ++ rabbitTakeDamageFromWater = getBoolean("mobs.rabbit.takes-damage-from-water", rabbitTakeDamageFromWater); ++ rabbitAlwaysDropExp = getBoolean("mobs.rabbit.always-drop-exp", rabbitAlwaysDropExp); ++ } ++ ++ public boolean ravagerRidable = false; ++ public boolean ravagerRidableInWater = false; ++ public boolean ravagerControllable = true; ++ public double ravagerMaxHealth = 100.0D; ++ public boolean ravagerBypassMobGriefing = false; ++ public boolean ravagerTakeDamageFromWater = false; ++ public List ravagerGriefableBlocks = new ArrayList<>(); ++ public boolean ravagerAlwaysDropExp = false; ++ private void ravagerSettings() { ++ ravagerRidable = getBoolean("mobs.ravager.ridable", ravagerRidable); ++ ravagerRidableInWater = getBoolean("mobs.ravager.ridable-in-water", ravagerRidableInWater); ++ ravagerControllable = getBoolean("mobs.ravager.controllable", ravagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ravager.attributes.max-health", ravagerMaxHealth); ++ set("mobs.ravager.attributes.max-health", null); ++ set("mobs.ravager.attributes.max_health", oldValue); ++ } ++ ravagerMaxHealth = getDouble("mobs.ravager.attributes.max_health", ravagerMaxHealth); ++ ravagerBypassMobGriefing = getBoolean("mobs.ravager.bypass-mob-griefing", ravagerBypassMobGriefing); ++ ravagerTakeDamageFromWater = getBoolean("mobs.ravager.takes-damage-from-water", ravagerTakeDamageFromWater); ++ getList("mobs.ravager.griefable-blocks", new ArrayList(){{ ++ add("minecraft:oak_leaves"); ++ add("minecraft:spruce_leaves"); ++ add("minecraft:birch_leaves"); ++ add("minecraft:jungle_leaves"); ++ add("minecraft:acacia_leaves"); ++ add("minecraft:dark_oak_leaves"); ++ add("minecraft:beetroots"); ++ add("minecraft:carrots"); ++ add("minecraft:potatoes"); ++ add("minecraft:wheat"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ ravagerGriefableBlocks.add(block); ++ } ++ }); ++ ravagerAlwaysDropExp = getBoolean("mobs.ravager.always-drop-exp", ravagerAlwaysDropExp); ++ } ++ ++ public boolean salmonRidable = false; ++ public boolean salmonControllable = true; ++ public double salmonMaxHealth = 3.0D; ++ public boolean salmonTakeDamageFromWater = false; ++ public boolean salmonAlwaysDropExp = false; ++ private void salmonSettings() { ++ salmonRidable = getBoolean("mobs.salmon.ridable", salmonRidable); ++ salmonControllable = getBoolean("mobs.salmon.controllable", salmonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.salmon.attributes.max-health", salmonMaxHealth); ++ set("mobs.salmon.attributes.max-health", null); ++ set("mobs.salmon.attributes.max_health", oldValue); ++ } ++ salmonMaxHealth = getDouble("mobs.salmon.attributes.max_health", salmonMaxHealth); ++ salmonTakeDamageFromWater = getBoolean("mobs.salmon.takes-damage-from-water", salmonTakeDamageFromWater); ++ salmonAlwaysDropExp = getBoolean("mobs.salmon.always-drop-exp", salmonAlwaysDropExp); ++ } ++ ++ public boolean sheepRidable = false; ++ public boolean sheepRidableInWater = false; ++ public boolean sheepControllable = true; ++ public double sheepMaxHealth = 8.0D; ++ public int sheepBreedingTicks = 6000; ++ public boolean sheepBypassMobGriefing = false; ++ public boolean sheepTakeDamageFromWater = false; ++ public boolean sheepAlwaysDropExp = false; ++ public boolean sheepShearJebRandomColor = false; ++ private void sheepSettings() { ++ sheepRidable = getBoolean("mobs.sheep.ridable", sheepRidable); ++ sheepRidableInWater = getBoolean("mobs.sheep.ridable-in-water", sheepRidableInWater); ++ sheepControllable = getBoolean("mobs.sheep.controllable", sheepControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.sheep.attributes.max-health", sheepMaxHealth); ++ set("mobs.sheep.attributes.max-health", null); ++ set("mobs.sheep.attributes.max_health", oldValue); ++ } ++ sheepMaxHealth = getDouble("mobs.sheep.attributes.max_health", sheepMaxHealth); ++ sheepBreedingTicks = getInt("mobs.sheep.breeding-delay-ticks", sheepBreedingTicks); ++ sheepBypassMobGriefing = getBoolean("mobs.sheep.bypass-mob-griefing", sheepBypassMobGriefing); ++ sheepTakeDamageFromWater = getBoolean("mobs.sheep.takes-damage-from-water", sheepTakeDamageFromWater); ++ sheepAlwaysDropExp = getBoolean("mobs.sheep.always-drop-exp", sheepAlwaysDropExp); ++ sheepShearJebRandomColor = getBoolean("mobs.sheep.jeb-shear-random-color", sheepShearJebRandomColor); ++ } ++ ++ public boolean shulkerRidable = false; ++ public boolean shulkerRidableInWater = false; ++ public boolean shulkerControllable = true; ++ public double shulkerMaxHealth = 30.0D; ++ public boolean shulkerTakeDamageFromWater = false; ++ public float shulkerSpawnFromBulletBaseChance = 1.0F; ++ public boolean shulkerSpawnFromBulletRequireOpenLid = true; ++ public double shulkerSpawnFromBulletNearbyRange = 8.0D; ++ public String shulkerSpawnFromBulletNearbyEquation = "(nearby - 1) / 5.0"; ++ public boolean shulkerSpawnFromBulletRandomColor = false; ++ public boolean shulkerChangeColorWithDye = false; ++ public boolean shulkerAlwaysDropExp = false; ++ private void shulkerSettings() { ++ shulkerRidable = getBoolean("mobs.shulker.ridable", shulkerRidable); ++ shulkerRidableInWater = getBoolean("mobs.shulker.ridable-in-water", shulkerRidableInWater); ++ shulkerControllable = getBoolean("mobs.shulker.controllable", shulkerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.shulker.attributes.max-health", shulkerMaxHealth); ++ set("mobs.shulker.attributes.max-health", null); ++ set("mobs.shulker.attributes.max_health", oldValue); ++ } ++ shulkerMaxHealth = getDouble("mobs.shulker.attributes.max_health", shulkerMaxHealth); ++ shulkerTakeDamageFromWater = getBoolean("mobs.shulker.takes-damage-from-water", shulkerTakeDamageFromWater); ++ shulkerSpawnFromBulletBaseChance = (float) getDouble("mobs.shulker.spawn-from-bullet.base-chance", shulkerSpawnFromBulletBaseChance); ++ shulkerSpawnFromBulletRequireOpenLid = getBoolean("mobs.shulker.spawn-from-bullet.require-open-lid", shulkerSpawnFromBulletRequireOpenLid); ++ shulkerSpawnFromBulletNearbyRange = getDouble("mobs.shulker.spawn-from-bullet.nearby-range", shulkerSpawnFromBulletNearbyRange); ++ shulkerSpawnFromBulletNearbyEquation = getString("mobs.shulker.spawn-from-bullet.nearby-equation", shulkerSpawnFromBulletNearbyEquation); ++ shulkerSpawnFromBulletRandomColor = getBoolean("mobs.shulker.spawn-from-bullet.random-color", shulkerSpawnFromBulletRandomColor); ++ shulkerChangeColorWithDye = getBoolean("mobs.shulker.change-color-with-dye", shulkerChangeColorWithDye); ++ shulkerAlwaysDropExp = getBoolean("mobs.shulker.always-drop-exp", shulkerAlwaysDropExp); ++ } ++ ++ public boolean silverfishRidable = false; ++ public boolean silverfishRidableInWater = false; ++ public boolean silverfishControllable = true; ++ public double silverfishMaxHealth = 8.0D; ++ public boolean silverfishBypassMobGriefing = false; ++ public boolean silverfishTakeDamageFromWater = false; ++ public boolean silverfishAlwaysDropExp = false; ++ private void silverfishSettings() { ++ silverfishRidable = getBoolean("mobs.silverfish.ridable", silverfishRidable); ++ silverfishRidableInWater = getBoolean("mobs.silverfish.ridable-in-water", silverfishRidableInWater); ++ silverfishControllable = getBoolean("mobs.silverfish.controllable", silverfishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.silverfish.attributes.max-health", silverfishMaxHealth); ++ set("mobs.silverfish.attributes.max-health", null); ++ set("mobs.silverfish.attributes.max_health", oldValue); ++ } ++ silverfishMaxHealth = getDouble("mobs.silverfish.attributes.max_health", silverfishMaxHealth); ++ silverfishBypassMobGriefing = getBoolean("mobs.silverfish.bypass-mob-griefing", silverfishBypassMobGriefing); ++ silverfishTakeDamageFromWater = getBoolean("mobs.silverfish.takes-damage-from-water", silverfishTakeDamageFromWater); ++ silverfishAlwaysDropExp = getBoolean("mobs.silverfish.always-drop-exp", silverfishAlwaysDropExp); ++ } ++ ++ public boolean skeletonRidable = false; ++ public boolean skeletonRidableInWater = false; ++ public boolean skeletonControllable = true; ++ public double skeletonMaxHealth = 20.0D; ++ public boolean skeletonTakeDamageFromWater = false; ++ public boolean skeletonAlwaysDropExp = false; ++ public double skeletonHeadVisibilityPercent = 0.5D; ++ public int skeletonFeedWitherRoses = 0; ++ public String skeletonBowAccuracy = "14 - difficulty * 4"; ++ public Map skeletonBowAccuracyMap = new HashMap<>(); ++ private void skeletonSettings() { ++ skeletonRidable = getBoolean("mobs.skeleton.ridable", skeletonRidable); ++ skeletonRidableInWater = getBoolean("mobs.skeleton.ridable-in-water", skeletonRidableInWater); ++ skeletonControllable = getBoolean("mobs.skeleton.controllable", skeletonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.skeleton.attributes.max-health", skeletonMaxHealth); ++ set("mobs.skeleton.attributes.max-health", null); ++ set("mobs.skeleton.attributes.max_health", oldValue); ++ } ++ skeletonMaxHealth = getDouble("mobs.skeleton.attributes.max_health", skeletonMaxHealth); ++ skeletonTakeDamageFromWater = getBoolean("mobs.skeleton.takes-damage-from-water", skeletonTakeDamageFromWater); ++ skeletonAlwaysDropExp = getBoolean("mobs.skeleton.always-drop-exp", skeletonAlwaysDropExp); ++ skeletonHeadVisibilityPercent = getDouble("mobs.skeleton.head-visibility-percent", skeletonHeadVisibilityPercent); ++ skeletonFeedWitherRoses = getInt("mobs.skeleton.feed-wither-roses", skeletonFeedWitherRoses); ++ skeletonBowAccuracy = getString("mobs.skeleton.bow-accuracy", skeletonBowAccuracy); ++ for (int i = 1; i < 4; i++) { ++ float divergence; ++ try { ++ Entity.scriptEngine.eval("difficulty = " + i); ++ divergence = ((Double)Entity.scriptEngine.eval(skeletonBowAccuracy)).floatValue(); ++ } catch (Exception e) { ++ e.printStackTrace(); ++ continue; ++ } ++ skeletonBowAccuracyMap.put(i, divergence); ++ } ++ } ++ ++ public boolean skeletonHorseRidableInWater = true; ++ public boolean skeletonHorseCanSwim = false; ++ public double skeletonHorseMaxHealthMin = 15.0D; ++ public double skeletonHorseMaxHealthMax = 15.0D; ++ public double skeletonHorseJumpStrengthMin = 0.4D; ++ public double skeletonHorseJumpStrengthMax = 1.0D; ++ public double skeletonHorseMovementSpeedMin = 0.2D; ++ public double skeletonHorseMovementSpeedMax = 0.2D; ++ public boolean skeletonHorseTakeDamageFromWater = false; ++ public boolean skeletonHorseAlwaysDropExp = false; ++ private void skeletonHorseSettings() { ++ skeletonHorseRidableInWater = getBoolean("mobs.skeleton_horse.ridable-in-water", skeletonHorseRidableInWater); ++ skeletonHorseCanSwim = getBoolean("mobs.skeleton_horse.can-swim", skeletonHorseCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.skeleton_horse.attributes.max-health", skeletonHorseMaxHealthMin); ++ set("mobs.skeleton_horse.attributes.max-health", null); ++ set("mobs.skeleton_horse.attributes.max_health.min", oldValue); ++ set("mobs.skeleton_horse.attributes.max_health.max", oldValue); ++ } ++ skeletonHorseMaxHealthMin = getDouble("mobs.skeleton_horse.attributes.max_health.min", skeletonHorseMaxHealthMin); ++ skeletonHorseMaxHealthMax = getDouble("mobs.skeleton_horse.attributes.max_health.max", skeletonHorseMaxHealthMax); ++ skeletonHorseJumpStrengthMin = getDouble("mobs.skeleton_horse.attributes.jump_strength.min", skeletonHorseJumpStrengthMin); ++ skeletonHorseJumpStrengthMax = getDouble("mobs.skeleton_horse.attributes.jump_strength.max", skeletonHorseJumpStrengthMax); ++ skeletonHorseMovementSpeedMin = getDouble("mobs.skeleton_horse.attributes.movement_speed.min", skeletonHorseMovementSpeedMin); ++ skeletonHorseMovementSpeedMax = getDouble("mobs.skeleton_horse.attributes.movement_speed.max", skeletonHorseMovementSpeedMax); ++ skeletonHorseTakeDamageFromWater = getBoolean("mobs.skeleton_horse.takes-damage-from-water", skeletonHorseTakeDamageFromWater); ++ skeletonHorseAlwaysDropExp = getBoolean("mobs.skeleton_horse.always-drop-exp", skeletonHorseAlwaysDropExp); ++ } ++ ++ public boolean slimeRidable = false; ++ public boolean slimeRidableInWater = false; ++ public boolean slimeControllable = true; ++ public String slimeMaxHealth = "size * size"; ++ public String slimeAttackDamage = "size"; ++ public Map slimeMaxHealthCache = new HashMap<>(); ++ public Map slimeAttackDamageCache = new HashMap<>(); ++ public boolean slimeTakeDamageFromWater = false; ++ public boolean slimeAlwaysDropExp = false; ++ private void slimeSettings() { ++ slimeRidable = getBoolean("mobs.slime.ridable", slimeRidable); ++ slimeRidableInWater = getBoolean("mobs.slime.ridable-in-water", slimeRidableInWater); ++ slimeControllable = getBoolean("mobs.slime.controllable", slimeControllable); ++ if (PurpurConfig.version < 10) { ++ String oldValue = getString("mobs.slime.attributes.max-health", slimeMaxHealth); ++ set("mobs.slime.attributes.max-health", null); ++ set("mobs.slime.attributes.max_health", oldValue); ++ } ++ slimeMaxHealth = getString("mobs.slime.attributes.max_health", slimeMaxHealth); ++ slimeAttackDamage = getString("mobs.slime.attributes.attack_damage", slimeAttackDamage); ++ slimeMaxHealthCache.clear(); ++ slimeAttackDamageCache.clear(); ++ slimeTakeDamageFromWater = getBoolean("mobs.slime.takes-damage-from-water", slimeTakeDamageFromWater); ++ slimeAlwaysDropExp = getBoolean("mobs.slime.always-drop-exp", slimeAlwaysDropExp); ++ } ++ ++ public boolean snowGolemRidable = false; ++ public boolean snowGolemRidableInWater = false; ++ public boolean snowGolemControllable = true; ++ public boolean snowGolemLeaveTrailWhenRidden = false; ++ public double snowGolemMaxHealth = 4.0D; ++ public boolean snowGolemDropsPumpkin = true; ++ public boolean snowGolemPutPumpkinBack = false; ++ public int snowGolemSnowBallMin = 20; ++ public int snowGolemSnowBallMax = 20; ++ public float snowGolemSnowBallModifier = 10.0F; ++ public double snowGolemAttackDistance = 1.25D; ++ public boolean snowGolemBypassMobGriefing = false; ++ public boolean snowGolemTakeDamageFromWater = true; ++ public boolean snowGolemAlwaysDropExp = false; ++ private void snowGolemSettings() { ++ snowGolemRidable = getBoolean("mobs.snow_golem.ridable", snowGolemRidable); ++ snowGolemRidableInWater = getBoolean("mobs.snow_golem.ridable-in-water", snowGolemRidableInWater); ++ snowGolemControllable = getBoolean("mobs.snow_golem.controllable", snowGolemControllable); ++ snowGolemLeaveTrailWhenRidden = getBoolean("mobs.snow_golem.leave-trail-when-ridden", snowGolemLeaveTrailWhenRidden); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.snow_golem.attributes.max-health", snowGolemMaxHealth); ++ set("mobs.snow_golem.attributes.max-health", null); ++ set("mobs.snow_golem.attributes.max_health", oldValue); ++ } ++ snowGolemMaxHealth = getDouble("mobs.snow_golem.attributes.max_health", snowGolemMaxHealth); ++ snowGolemDropsPumpkin = getBoolean("mobs.snow_golem.drop-pumpkin-when-sheared", snowGolemDropsPumpkin); ++ snowGolemPutPumpkinBack = getBoolean("mobs.snow_golem.pumpkin-can-be-added-back", snowGolemPutPumpkinBack); ++ snowGolemSnowBallMin = getInt("mobs.snow_golem.min-shoot-interval-ticks", snowGolemSnowBallMin); ++ snowGolemSnowBallMax = getInt("mobs.snow_golem.max-shoot-interval-ticks", snowGolemSnowBallMax); ++ snowGolemSnowBallModifier = (float) getDouble("mobs.snow_golem.snow-ball-modifier", snowGolemSnowBallModifier); ++ snowGolemAttackDistance = getDouble("mobs.snow_golem.attack-distance", snowGolemAttackDistance); ++ snowGolemBypassMobGriefing = getBoolean("mobs.snow_golem.bypass-mob-griefing", snowGolemBypassMobGriefing); ++ snowGolemTakeDamageFromWater = getBoolean("mobs.snow_golem.takes-damage-from-water", snowGolemTakeDamageFromWater); ++ snowGolemAlwaysDropExp = getBoolean("mobs.snow_golem.always-drop-exp", snowGolemAlwaysDropExp); ++ } ++ ++ public boolean squidRidable = false; ++ public boolean squidControllable = true; ++ public double squidMaxHealth = 10.0D; ++ public boolean squidImmuneToEAR = true; ++ public double squidOffsetWaterCheck = 0.0D; ++ public boolean squidsCanFly = false; ++ public boolean squidTakeDamageFromWater = false; ++ public boolean squidAlwaysDropExp = false; ++ private void squidSettings() { ++ squidRidable = getBoolean("mobs.squid.ridable", squidRidable); ++ squidControllable = getBoolean("mobs.squid.controllable", squidControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.squid.attributes.max-health", squidMaxHealth); ++ set("mobs.squid.attributes.max-health", null); ++ set("mobs.squid.attributes.max_health", oldValue); ++ } ++ squidMaxHealth = getDouble("mobs.squid.attributes.max_health", squidMaxHealth); ++ squidImmuneToEAR = getBoolean("mobs.squid.immune-to-EAR", squidImmuneToEAR); ++ squidOffsetWaterCheck = getDouble("mobs.squid.water-offset-check", squidOffsetWaterCheck); ++ squidsCanFly = getBoolean("mobs.squid.can-fly", squidsCanFly); ++ squidTakeDamageFromWater = getBoolean("mobs.squid.takes-damage-from-water", squidTakeDamageFromWater); ++ squidAlwaysDropExp = getBoolean("mobs.squid.always-drop-exp", squidAlwaysDropExp); ++ } ++ ++ public boolean spiderRidable = false; ++ public boolean spiderRidableInWater = false; ++ public boolean spiderControllable = true; ++ public double spiderMaxHealth = 16.0D; ++ public boolean spiderTakeDamageFromWater = false; ++ public boolean spiderAlwaysDropExp = false; ++ private void spiderSettings() { ++ spiderRidable = getBoolean("mobs.spider.ridable", spiderRidable); ++ spiderRidableInWater = getBoolean("mobs.spider.ridable-in-water", spiderRidableInWater); ++ spiderControllable = getBoolean("mobs.spider.controllable", spiderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.spider.attributes.max-health", spiderMaxHealth); ++ set("mobs.spider.attributes.max-health", null); ++ set("mobs.spider.attributes.max_health", oldValue); ++ } ++ spiderMaxHealth = getDouble("mobs.spider.attributes.max_health", spiderMaxHealth); ++ spiderTakeDamageFromWater = getBoolean("mobs.spider.takes-damage-from-water", spiderTakeDamageFromWater); ++ spiderAlwaysDropExp = getBoolean("mobs.spider.always-drop-exp", spiderAlwaysDropExp); ++ } ++ ++ public boolean strayRidable = false; ++ public boolean strayRidableInWater = false; ++ public boolean strayControllable = true; ++ public double strayMaxHealth = 20.0D; ++ public boolean strayTakeDamageFromWater = false; ++ public boolean strayAlwaysDropExp = false; ++ private void straySettings() { ++ strayRidable = getBoolean("mobs.stray.ridable", strayRidable); ++ strayRidableInWater = getBoolean("mobs.stray.ridable-in-water", strayRidableInWater); ++ strayControllable = getBoolean("mobs.stray.controllable", strayControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.stray.attributes.max-health", strayMaxHealth); ++ set("mobs.stray.attributes.max-health", null); ++ set("mobs.stray.attributes.max_health", oldValue); ++ } ++ strayMaxHealth = getDouble("mobs.stray.attributes.max_health", strayMaxHealth); ++ strayTakeDamageFromWater = getBoolean("mobs.stray.takes-damage-from-water", strayTakeDamageFromWater); ++ strayAlwaysDropExp = getBoolean("mobs.stray.always-drop-exp", strayAlwaysDropExp); ++ } ++ ++ public boolean striderRidable = false; ++ public boolean striderRidableInWater = false; ++ public boolean striderControllable = true; ++ public double striderMaxHealth = 20.0D; ++ public int striderBreedingTicks = 6000; ++ public boolean striderGiveSaddleBack = false; ++ public boolean striderTakeDamageFromWater = true; ++ public boolean striderAlwaysDropExp = false; ++ private void striderSettings() { ++ striderRidable = getBoolean("mobs.strider.ridable", striderRidable); ++ striderRidableInWater = getBoolean("mobs.strider.ridable-in-water", striderRidableInWater); ++ striderControllable = getBoolean("mobs.strider.controllable", striderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.strider.attributes.max-health", striderMaxHealth); ++ set("mobs.strider.attributes.max-health", null); ++ set("mobs.strider.attributes.max_health", oldValue); ++ } ++ striderMaxHealth = getDouble("mobs.strider.attributes.max_health", striderMaxHealth); ++ striderBreedingTicks = getInt("mobs.strider.breeding-delay-ticks", striderBreedingTicks); ++ striderGiveSaddleBack = getBoolean("mobs.strider.give-saddle-back", striderGiveSaddleBack); ++ striderTakeDamageFromWater = getBoolean("mobs.strider.takes-damage-from-water", striderTakeDamageFromWater); ++ striderAlwaysDropExp = getBoolean("mobs.strider.always-drop-exp", striderAlwaysDropExp); ++ } ++ ++ public boolean tadpoleRidable = false; ++ public boolean tadpoleRidableInWater = false; ++ public boolean tadpoleControllable = true; ++ private void tadpoleSettings() { ++ tadpoleRidable = getBoolean("mobs.tadpole.ridable", tadpoleRidable); ++ tadpoleRidableInWater = getBoolean("mobs.tadpole.ridable-in-water", tadpoleRidableInWater); ++ tadpoleControllable = getBoolean("mobs.tadpole.controllable", tadpoleControllable); ++ } ++ ++ public boolean traderLlamaRidable = false; ++ public boolean traderLlamaRidableInWater = false; ++ public boolean traderLlamaControllable = true; ++ public double traderLlamaMaxHealthMin = 15.0D; ++ public double traderLlamaMaxHealthMax = 30.0D; ++ public double traderLlamaJumpStrengthMin = 0.5D; ++ public double traderLlamaJumpStrengthMax = 0.5D; ++ public double traderLlamaMovementSpeedMin = 0.175D; ++ public double traderLlamaMovementSpeedMax = 0.175D; ++ public int traderLlamaBreedingTicks = 6000; ++ public boolean traderLlamaTakeDamageFromWater = false; ++ public boolean traderLlamaAlwaysDropExp = false; ++ private void traderLlamaSettings() { ++ traderLlamaRidable = getBoolean("mobs.trader_llama.ridable", traderLlamaRidable); ++ traderLlamaRidableInWater = getBoolean("mobs.trader_llama.ridable-in-water", traderLlamaRidableInWater); ++ traderLlamaControllable = getBoolean("mobs.trader_llama.controllable", traderLlamaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.trader_llama.attributes.max-health.min", traderLlamaMaxHealthMin); ++ double oldMax = getDouble("mobs.trader_llama.attributes.max-health.max", traderLlamaMaxHealthMax); ++ set("mobs.trader_llama.attributes.max-health", null); ++ set("mobs.trader_llama.attributes.max_health.min", oldMin); ++ set("mobs.trader_llama.attributes.max_health.max", oldMax); ++ } ++ traderLlamaMaxHealthMin = getDouble("mobs.trader_llama.attributes.max_health.min", traderLlamaMaxHealthMin); ++ traderLlamaMaxHealthMax = getDouble("mobs.trader_llama.attributes.max_health.max", traderLlamaMaxHealthMax); ++ traderLlamaJumpStrengthMin = getDouble("mobs.trader_llama.attributes.jump_strength.min", traderLlamaJumpStrengthMin); ++ traderLlamaJumpStrengthMax = getDouble("mobs.trader_llama.attributes.jump_strength.max", traderLlamaJumpStrengthMax); ++ traderLlamaMovementSpeedMin = getDouble("mobs.trader_llama.attributes.movement_speed.min", traderLlamaMovementSpeedMin); ++ traderLlamaMovementSpeedMax = getDouble("mobs.trader_llama.attributes.movement_speed.max", traderLlamaMovementSpeedMax); ++ traderLlamaBreedingTicks = getInt("mobs.trader_llama.breeding-delay-ticks", traderLlamaBreedingTicks); ++ traderLlamaTakeDamageFromWater = getBoolean("mobs.trader_llama.takes-damage-from-water", traderLlamaTakeDamageFromWater); ++ traderLlamaAlwaysDropExp = getBoolean("mobs.trader_llama.always-drop-exp", traderLlamaAlwaysDropExp); ++ } ++ ++ public boolean tropicalFishRidable = false; ++ public boolean tropicalFishControllable = true; ++ public double tropicalFishMaxHealth = 3.0D; ++ public boolean tropicalFishTakeDamageFromWater = false; ++ public boolean tropicalFishAlwaysDropExp = false; ++ private void tropicalFishSettings() { ++ tropicalFishRidable = getBoolean("mobs.tropical_fish.ridable", tropicalFishRidable); ++ tropicalFishControllable = getBoolean("mobs.tropical_fish.controllable", tropicalFishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.tropical_fish.attributes.max-health", tropicalFishMaxHealth); ++ set("mobs.tropical_fish.attributes.max-health", null); ++ set("mobs.tropical_fish.attributes.max_health", oldValue); ++ } ++ tropicalFishMaxHealth = getDouble("mobs.tropical_fish.attributes.max_health", tropicalFishMaxHealth); ++ tropicalFishTakeDamageFromWater = getBoolean("mobs.tropical_fish.takes-damage-from-water", tropicalFishTakeDamageFromWater); ++ tropicalFishAlwaysDropExp = getBoolean("mobs.tropical_fish.always-drop-exp", tropicalFishAlwaysDropExp); ++ } ++ ++ public boolean turtleRidable = false; ++ public boolean turtleRidableInWater = false; ++ public boolean turtleControllable = true; ++ public double turtleMaxHealth = 30.0D; ++ public int turtleBreedingTicks = 6000; ++ public boolean turtleTakeDamageFromWater = false; ++ public boolean turtleAlwaysDropExp = false; ++ private void turtleSettings() { ++ turtleRidable = getBoolean("mobs.turtle.ridable", turtleRidable); ++ turtleRidableInWater = getBoolean("mobs.turtle.ridable-in-water", turtleRidableInWater); ++ turtleControllable = getBoolean("mobs.turtle.controllable", turtleControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.turtle.attributes.max-health", turtleMaxHealth); ++ set("mobs.turtle.attributes.max-health", null); ++ set("mobs.turtle.attributes.max_health", oldValue); ++ } ++ turtleMaxHealth = getDouble("mobs.turtle.attributes.max_health", turtleMaxHealth); ++ turtleBreedingTicks = getInt("mobs.turtle.breeding-delay-ticks", turtleBreedingTicks); ++ turtleTakeDamageFromWater = getBoolean("mobs.turtle.takes-damage-from-water", turtleTakeDamageFromWater); ++ turtleAlwaysDropExp = getBoolean("mobs.turtle.always-drop-exp", turtleAlwaysDropExp); ++ } ++ ++ public boolean vexRidable = false; ++ public boolean vexRidableInWater = false; ++ public boolean vexControllable = true; ++ public double vexMaxY = 320D; ++ public double vexMaxHealth = 14.0D; ++ public boolean vexTakeDamageFromWater = false; ++ public boolean vexAlwaysDropExp = false; ++ private void vexSettings() { ++ vexRidable = getBoolean("mobs.vex.ridable", vexRidable); ++ vexRidableInWater = getBoolean("mobs.vex.ridable-in-water", vexRidableInWater); ++ vexControllable = getBoolean("mobs.vex.controllable", vexControllable); ++ vexMaxY = getDouble("mobs.vex.ridable-max-y", vexMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.vex.attributes.max-health", vexMaxHealth); ++ set("mobs.vex.attributes.max-health", null); ++ set("mobs.vex.attributes.max_health", oldValue); ++ } ++ vexMaxHealth = getDouble("mobs.vex.attributes.max_health", vexMaxHealth); ++ vexTakeDamageFromWater = getBoolean("mobs.vex.takes-damage-from-water", vexTakeDamageFromWater); ++ vexAlwaysDropExp = getBoolean("mobs.vex.always-drop-exp", vexAlwaysDropExp); ++ } ++ ++ public boolean villagerRidable = false; ++ public boolean villagerRidableInWater = false; ++ public boolean villagerControllable = true; ++ public double villagerMaxHealth = 20.0D; ++ public boolean villagerFollowEmeraldBlock = false; ++ public boolean villagerCanBeLeashed = false; ++ public boolean villagerCanBreed = true; ++ public int villagerBreedingTicks = 6000; ++ public boolean villagerClericsFarmWarts = false; ++ public boolean villagerClericFarmersThrowWarts = true; ++ public boolean villagerBypassMobGriefing = false; ++ public boolean villagerTakeDamageFromWater = false; ++ public boolean villagerAllowTrading = true; ++ public boolean villagerAlwaysDropExp = false; ++ public int villagerMinimumDemand = 0; ++ public boolean villagerLobotomizeEnabled = false; ++ public int villagerLobotomizeCheckInterval = 100; ++ public boolean villagerDisplayTradeItem = true; ++ public int villagerSpawnIronGolemRadius = 0; ++ public int villagerSpawnIronGolemLimit = 0; ++ private void villagerSettings() { ++ villagerRidable = getBoolean("mobs.villager.ridable", villagerRidable); ++ villagerRidableInWater = getBoolean("mobs.villager.ridable-in-water", villagerRidableInWater); ++ villagerControllable = getBoolean("mobs.villager.controllable", villagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.villager.attributes.max-health", villagerMaxHealth); ++ set("mobs.villager.attributes.max-health", null); ++ set("mobs.villager.attributes.max_health", oldValue); ++ } ++ villagerMaxHealth = getDouble("mobs.villager.attributes.max_health", villagerMaxHealth); ++ villagerFollowEmeraldBlock = getBoolean("mobs.villager.follow-emerald-blocks", villagerFollowEmeraldBlock); ++ villagerCanBeLeashed = getBoolean("mobs.villager.can-be-leashed", villagerCanBeLeashed); ++ villagerCanBreed = getBoolean("mobs.villager.can-breed", villagerCanBreed); ++ villagerBreedingTicks = getInt("mobs.villager.breeding-delay-ticks", villagerBreedingTicks); ++ villagerClericsFarmWarts = getBoolean("mobs.villager.clerics-farm-warts", villagerClericsFarmWarts); ++ villagerClericFarmersThrowWarts = getBoolean("mobs.villager.cleric-wart-farmers-throw-warts-at-villagers", villagerClericFarmersThrowWarts); ++ villagerBypassMobGriefing = getBoolean("mobs.villager.bypass-mob-griefing", villagerBypassMobGriefing); ++ villagerTakeDamageFromWater = getBoolean("mobs.villager.takes-damage-from-water", villagerTakeDamageFromWater); ++ villagerAllowTrading = getBoolean("mobs.villager.allow-trading", villagerAllowTrading); ++ villagerAlwaysDropExp = getBoolean("mobs.villager.always-drop-exp", villagerAlwaysDropExp); ++ villagerMinimumDemand = getInt("mobs.villager.minimum-demand", villagerMinimumDemand); ++ if (PurpurConfig.version < 9) { ++ boolean oldValue = getBoolean("mobs.villager.lobotomize-1x1", villagerLobotomizeEnabled); ++ set("mobs.villager.lobotomize.enabled", oldValue); ++ set("mobs.villager.lobotomize-1x1", null); ++ } ++ if (PurpurConfig.version < 27) { ++ int oldValue = getInt("mobs.villager.lobotomize.check-interval", villagerLobotomizeCheckInterval); ++ set("mobs.villager.lobotomize.check-interval", oldValue == 60 ? 100 : oldValue); ++ } ++ villagerLobotomizeEnabled = getBoolean("mobs.villager.lobotomize.enabled", villagerLobotomizeEnabled); ++ villagerLobotomizeCheckInterval = getInt("mobs.villager.lobotomize.check-interval", villagerLobotomizeCheckInterval); ++ villagerDisplayTradeItem = getBoolean("mobs.villager.display-trade-item", villagerDisplayTradeItem); ++ villagerSpawnIronGolemRadius = getInt("mobs.villager.spawn-iron-golem.radius", villagerSpawnIronGolemRadius); ++ villagerSpawnIronGolemLimit = getInt("mobs.villager.spawn-iron-golem.limit", villagerSpawnIronGolemLimit); ++ } ++ ++ public boolean vindicatorRidable = false; ++ public boolean vindicatorRidableInWater = false; ++ public boolean vindicatorControllable = true; ++ public double vindicatorMaxHealth = 24.0D; ++ public double vindicatorJohnnySpawnChance = 0D; ++ public boolean vindicatorTakeDamageFromWater = false; ++ public boolean vindicatorAlwaysDropExp = false; ++ private void vindicatorSettings() { ++ vindicatorRidable = getBoolean("mobs.vindicator.ridable", vindicatorRidable); ++ vindicatorRidableInWater = getBoolean("mobs.vindicator.ridable-in-water", vindicatorRidableInWater); ++ vindicatorControllable = getBoolean("mobs.vindicator.controllable", vindicatorControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.vindicator.attributes.max-health", vindicatorMaxHealth); ++ set("mobs.vindicator.attributes.max-health", null); ++ set("mobs.vindicator.attributes.max_health", oldValue); ++ } ++ vindicatorMaxHealth = getDouble("mobs.vindicator.attributes.max_health", vindicatorMaxHealth); ++ vindicatorJohnnySpawnChance = getDouble("mobs.vindicator.johnny.spawn-chance", vindicatorJohnnySpawnChance); ++ vindicatorTakeDamageFromWater = getBoolean("mobs.vindicator.takes-damage-from-water", vindicatorTakeDamageFromWater); ++ vindicatorAlwaysDropExp = getBoolean("mobs.vindicator.always-drop-exp", vindicatorAlwaysDropExp); ++ } ++ ++ public boolean wanderingTraderRidable = false; ++ public boolean wanderingTraderRidableInWater = false; ++ public boolean wanderingTraderControllable = true; ++ public double wanderingTraderMaxHealth = 20.0D; ++ public boolean wanderingTraderFollowEmeraldBlock = false; ++ public boolean wanderingTraderCanBeLeashed = false; ++ public boolean wanderingTraderTakeDamageFromWater = false; ++ public boolean wanderingTraderAllowTrading = true; ++ public boolean wanderingTraderAlwaysDropExp = false; ++ private void wanderingTraderSettings() { ++ wanderingTraderRidable = getBoolean("mobs.wandering_trader.ridable", wanderingTraderRidable); ++ wanderingTraderRidableInWater = getBoolean("mobs.wandering_trader.ridable-in-water", wanderingTraderRidableInWater); ++ wanderingTraderControllable = getBoolean("mobs.wandering_trader.controllable", wanderingTraderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wandering_trader.attributes.max-health", wanderingTraderMaxHealth); ++ set("mobs.wandering_trader.attributes.max-health", null); ++ set("mobs.wandering_trader.attributes.max_health", oldValue); ++ } ++ wanderingTraderMaxHealth = getDouble("mobs.wandering_trader.attributes.max_health", wanderingTraderMaxHealth); ++ wanderingTraderFollowEmeraldBlock = getBoolean("mobs.wandering_trader.follow-emerald-blocks", wanderingTraderFollowEmeraldBlock); ++ wanderingTraderCanBeLeashed = getBoolean("mobs.wandering_trader.can-be-leashed", wanderingTraderCanBeLeashed); ++ wanderingTraderTakeDamageFromWater = getBoolean("mobs.wandering_trader.takes-damage-from-water", wanderingTraderTakeDamageFromWater); ++ wanderingTraderAllowTrading = getBoolean("mobs.wandering_trader.allow-trading", wanderingTraderAllowTrading); ++ wanderingTraderAlwaysDropExp = getBoolean("mobs.wandering_trader.always-drop-exp", wanderingTraderAlwaysDropExp); ++ } ++ ++ public boolean wardenRidable = false; ++ public boolean wardenRidableInWater = false; ++ public boolean wardenControllable = true; ++ private void wardenSettings() { ++ wardenRidable = getBoolean("mobs.warden.ridable", wardenRidable); ++ wardenRidableInWater = getBoolean("mobs.warden.ridable-in-water", wardenRidableInWater); ++ wardenControllable = getBoolean("mobs.warden.controllable", wardenControllable); ++ } ++ ++ public boolean witchRidable = false; ++ public boolean witchRidableInWater = false; ++ public boolean witchControllable = true; ++ public double witchMaxHealth = 26.0D; ++ public boolean witchTakeDamageFromWater = false; ++ public boolean witchAlwaysDropExp = false; ++ private void witchSettings() { ++ witchRidable = getBoolean("mobs.witch.ridable", witchRidable); ++ witchRidableInWater = getBoolean("mobs.witch.ridable-in-water", witchRidableInWater); ++ witchControllable = getBoolean("mobs.witch.controllable", witchControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.witch.attributes.max-health", witchMaxHealth); ++ set("mobs.witch.attributes.max-health", null); ++ set("mobs.witch.attributes.max_health", oldValue); ++ } ++ witchMaxHealth = getDouble("mobs.witch.attributes.max_health", witchMaxHealth); ++ witchTakeDamageFromWater = getBoolean("mobs.witch.takes-damage-from-water", witchTakeDamageFromWater); ++ witchAlwaysDropExp = getBoolean("mobs.witch.always-drop-exp", witchAlwaysDropExp); ++ } ++ ++ public boolean witherRidable = false; ++ public boolean witherRidableInWater = false; ++ public boolean witherControllable = true; ++ public double witherMaxY = 320D; ++ public double witherMaxHealth = 300.0D; ++ public float witherHealthRegenAmount = 1.0f; ++ public int witherHealthRegenDelay = 20; ++ public boolean witherBypassMobGriefing = false; ++ public boolean witherTakeDamageFromWater = false; ++ public boolean witherCanRideVehicles = false; ++ public float witherExplosionRadius = 1.0F; ++ public boolean witherPlaySpawnSound = true; ++ public boolean witherAlwaysDropExp = false; ++ private void witherSettings() { ++ witherRidable = getBoolean("mobs.wither.ridable", witherRidable); ++ witherRidableInWater = getBoolean("mobs.wither.ridable-in-water", witherRidableInWater); ++ witherControllable = getBoolean("mobs.wither.controllable", witherControllable); ++ witherMaxY = getDouble("mobs.wither.ridable-max-y", witherMaxY); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.wither.max-health", witherMaxHealth); ++ set("mobs.wither.max_health", null); ++ set("mobs.wither.attributes.max-health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wither.attributes.max-health", witherMaxHealth); ++ set("mobs.wither.attributes.max-health", null); ++ set("mobs.wither.attributes.max_health", oldValue); ++ } ++ witherMaxHealth = getDouble("mobs.wither.attributes.max_health", witherMaxHealth); ++ witherHealthRegenAmount = (float) getDouble("mobs.wither.health-regen-amount", witherHealthRegenAmount); ++ witherHealthRegenDelay = getInt("mobs.wither.health-regen-delay", witherHealthRegenDelay); ++ witherBypassMobGriefing = getBoolean("mobs.wither.bypass-mob-griefing", witherBypassMobGriefing); ++ witherTakeDamageFromWater = getBoolean("mobs.wither.takes-damage-from-water", witherTakeDamageFromWater); ++ witherCanRideVehicles = getBoolean("mobs.wither.can-ride-vehicles", witherCanRideVehicles); ++ witherExplosionRadius = (float) getDouble("mobs.wither.explosion-radius", witherExplosionRadius); ++ witherPlaySpawnSound = getBoolean("mobs.wither.play-spawn-sound", witherPlaySpawnSound); ++ witherAlwaysDropExp = getBoolean("mobs.wither.always-drop-exp", witherAlwaysDropExp); ++ } ++ ++ public boolean witherSkeletonRidable = false; ++ public boolean witherSkeletonRidableInWater = false; ++ public boolean witherSkeletonControllable = true; ++ public double witherSkeletonMaxHealth = 20.0D; ++ public boolean witherSkeletonTakeDamageFromWater = false; ++ public boolean witherSkeletonAlwaysDropExp = false; ++ private void witherSkeletonSettings() { ++ witherSkeletonRidable = getBoolean("mobs.wither_skeleton.ridable", witherSkeletonRidable); ++ witherSkeletonRidableInWater = getBoolean("mobs.wither_skeleton.ridable-in-water", witherSkeletonRidableInWater); ++ witherSkeletonControllable = getBoolean("mobs.wither_skeleton.controllable", witherSkeletonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wither_skeleton.attributes.max-health", witherSkeletonMaxHealth); ++ set("mobs.wither_skeleton.attributes.max-health", null); ++ set("mobs.wither_skeleton.attributes.max_health", oldValue); ++ } ++ witherSkeletonMaxHealth = getDouble("mobs.wither_skeleton.attributes.max_health", witherSkeletonMaxHealth); ++ witherSkeletonTakeDamageFromWater = getBoolean("mobs.wither_skeleton.takes-damage-from-water", witherSkeletonTakeDamageFromWater); ++ witherSkeletonAlwaysDropExp = getBoolean("mobs.wither_skeleton.always-drop-exp", witherSkeletonAlwaysDropExp); ++ } ++ ++ public boolean wolfRidable = false; ++ public boolean wolfRidableInWater = false; ++ public boolean wolfControllable = true; ++ public double wolfMaxHealth = 8.0D; ++ public DyeColor wolfDefaultCollarColor = DyeColor.RED; ++ public boolean wolfMilkCuresRabies = true; ++ public double wolfNaturalRabid = 0.0D; ++ public int wolfBreedingTicks = 6000; ++ public boolean wolfTakeDamageFromWater = false; ++ public boolean wolfAlwaysDropExp = false; ++ private void wolfSettings() { ++ wolfRidable = getBoolean("mobs.wolf.ridable", wolfRidable); ++ wolfRidableInWater = getBoolean("mobs.wolf.ridable-in-water", wolfRidableInWater); ++ wolfControllable = getBoolean("mobs.wolf.controllable", wolfControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wolf.attributes.max-health", wolfMaxHealth); ++ set("mobs.wolf.attributes.max-health", null); ++ set("mobs.wolf.attributes.max_health", oldValue); ++ } ++ wolfMaxHealth = getDouble("mobs.wolf.attributes.max_health", wolfMaxHealth); ++ try { ++ wolfDefaultCollarColor = DyeColor.valueOf(getString("mobs.wolf.default-collar-color", wolfDefaultCollarColor.name())); ++ } catch (IllegalArgumentException ignore) { ++ wolfDefaultCollarColor = DyeColor.RED; ++ } ++ wolfMilkCuresRabies = getBoolean("mobs.wolf.milk-cures-rabid-wolves", wolfMilkCuresRabies); ++ wolfNaturalRabid = getDouble("mobs.wolf.spawn-rabid-chance", wolfNaturalRabid); ++ wolfBreedingTicks = getInt("mobs.wolf.breeding-delay-ticks", wolfBreedingTicks); ++ wolfTakeDamageFromWater = getBoolean("mobs.wolf.takes-damage-from-water", wolfTakeDamageFromWater); ++ wolfAlwaysDropExp = getBoolean("mobs.wolf.always-drop-exp", wolfAlwaysDropExp); ++ } ++ ++ public boolean zoglinRidable = false; ++ public boolean zoglinRidableInWater = false; ++ public boolean zoglinControllable = true; ++ public double zoglinMaxHealth = 40.0D; ++ public boolean zoglinTakeDamageFromWater = false; ++ public boolean zoglinAlwaysDropExp = false; ++ private void zoglinSettings() { ++ zoglinRidable = getBoolean("mobs.zoglin.ridable", zoglinRidable); ++ zoglinRidableInWater = getBoolean("mobs.zoglin.ridable-in-water", zoglinRidableInWater); ++ zoglinControllable = getBoolean("mobs.zoglin.controllable", zoglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zoglin.attributes.max-health", zoglinMaxHealth); ++ set("mobs.zoglin.attributes.max-health", null); ++ set("mobs.zoglin.attributes.max_health", oldValue); ++ } ++ zoglinMaxHealth = getDouble("mobs.zoglin.attributes.max_health", zoglinMaxHealth); ++ zoglinTakeDamageFromWater = getBoolean("mobs.zoglin.takes-damage-from-water", zoglinTakeDamageFromWater); ++ zoglinAlwaysDropExp = getBoolean("mobs.zoglin.always-drop-exp", zoglinAlwaysDropExp); ++ } ++ ++ public boolean zombieRidable = false; ++ public boolean zombieRidableInWater = false; ++ public boolean zombieControllable = true; ++ public double zombieMaxHealth = 20.0D; ++ public double zombieSpawnReinforcements = 0.1D; ++ public boolean zombieJockeyOnlyBaby = true; ++ public double zombieJockeyChance = 0.05D; ++ public boolean zombieJockeyTryExistingChickens = true; ++ public boolean zombieAggressiveTowardsVillagerWhenLagging = true; ++ public boolean zombieBypassMobGriefing = false; ++ public boolean zombieTakeDamageFromWater = false; ++ public boolean zombieAlwaysDropExp = false; ++ public double zombieHeadVisibilityPercent = 0.5D; ++ private void zombieSettings() { ++ zombieRidable = getBoolean("mobs.zombie.ridable", zombieRidable); ++ zombieRidableInWater = getBoolean("mobs.zombie.ridable-in-water", zombieRidableInWater); ++ zombieControllable = getBoolean("mobs.zombie.controllable", zombieControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie.attributes.max-health", zombieMaxHealth); ++ set("mobs.zombie.attributes.max-health", null); ++ set("mobs.zombie.attributes.max_health", oldValue); ++ } ++ zombieMaxHealth = getDouble("mobs.zombie.attributes.max_health", zombieMaxHealth); ++ zombieSpawnReinforcements = getDouble("mobs.zombie.attributes.spawn_reinforcements", zombieSpawnReinforcements); ++ zombieJockeyOnlyBaby = getBoolean("mobs.zombie.jockey.only-babies", zombieJockeyOnlyBaby); ++ zombieJockeyChance = getDouble("mobs.zombie.jockey.chance", zombieJockeyChance); ++ zombieJockeyTryExistingChickens = getBoolean("mobs.zombie.jockey.try-existing-chickens", zombieJockeyTryExistingChickens); ++ zombieAggressiveTowardsVillagerWhenLagging = getBoolean("mobs.zombie.aggressive-towards-villager-when-lagging", zombieAggressiveTowardsVillagerWhenLagging); ++ zombieBypassMobGriefing = getBoolean("mobs.zombie.bypass-mob-griefing", zombieBypassMobGriefing); ++ zombieTakeDamageFromWater = getBoolean("mobs.zombie.takes-damage-from-water", zombieTakeDamageFromWater); ++ zombieAlwaysDropExp = getBoolean("mobs.zombie.always-drop-exp", zombieAlwaysDropExp); ++ zombieHeadVisibilityPercent = getDouble("mobs.zombie.head-visibility-percent", zombieHeadVisibilityPercent); ++ } ++ ++ public boolean zombieHorseRidableInWater = false; ++ public boolean zombieHorseCanSwim = false; ++ public double zombieHorseMaxHealthMin = 15.0D; ++ public double zombieHorseMaxHealthMax = 15.0D; ++ public double zombieHorseJumpStrengthMin = 0.4D; ++ public double zombieHorseJumpStrengthMax = 1.0D; ++ public double zombieHorseMovementSpeedMin = 0.2D; ++ public double zombieHorseMovementSpeedMax = 0.2D; ++ public double zombieHorseSpawnChance = 0.0D; ++ public boolean zombieHorseTakeDamageFromWater = false; ++ public boolean zombieHorseAlwaysDropExp = false; ++ private void zombieHorseSettings() { ++ zombieHorseRidableInWater = getBoolean("mobs.zombie_horse.ridable-in-water", zombieHorseRidableInWater); ++ zombieHorseCanSwim = getBoolean("mobs.zombie_horse.can-swim", zombieHorseCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie_horse.attributes.max-health", zombieHorseMaxHealthMin); ++ set("mobs.zombie_horse.attributes.max-health", null); ++ set("mobs.zombie_horse.attributes.max_health.min", oldValue); ++ set("mobs.zombie_horse.attributes.max_health.max", oldValue); ++ } ++ zombieHorseMaxHealthMin = getDouble("mobs.zombie_horse.attributes.max_health.min", zombieHorseMaxHealthMin); ++ zombieHorseMaxHealthMax = getDouble("mobs.zombie_horse.attributes.max_health.max", zombieHorseMaxHealthMax); ++ zombieHorseJumpStrengthMin = getDouble("mobs.zombie_horse.attributes.jump_strength.min", zombieHorseJumpStrengthMin); ++ zombieHorseJumpStrengthMax = getDouble("mobs.zombie_horse.attributes.jump_strength.max", zombieHorseJumpStrengthMax); ++ zombieHorseMovementSpeedMin = getDouble("mobs.zombie_horse.attributes.movement_speed.min", zombieHorseMovementSpeedMin); ++ zombieHorseMovementSpeedMax = getDouble("mobs.zombie_horse.attributes.movement_speed.max", zombieHorseMovementSpeedMax); ++ zombieHorseSpawnChance = getDouble("mobs.zombie_horse.spawn-chance", zombieHorseSpawnChance); ++ zombieHorseTakeDamageFromWater = getBoolean("mobs.zombie_horse.takes-damage-from-water", zombieHorseTakeDamageFromWater); ++ zombieHorseAlwaysDropExp = getBoolean("mobs.zombie_horse.always-drop-exp", zombieHorseAlwaysDropExp); ++ } ++ ++ public boolean zombieVillagerRidable = false; ++ public boolean zombieVillagerRidableInWater = false; ++ public boolean zombieVillagerControllable = true; ++ public double zombieVillagerMaxHealth = 20.0D; ++ public double zombieVillagerSpawnReinforcements = 0.1D; ++ public boolean zombieVillagerJockeyOnlyBaby = true; ++ public double zombieVillagerJockeyChance = 0.05D; ++ public boolean zombieVillagerJockeyTryExistingChickens = true; ++ public boolean zombieVillagerTakeDamageFromWater = false; ++ public int zombieVillagerCuringTimeMin = 3600; ++ public int zombieVillagerCuringTimeMax = 6000; ++ public boolean zombieVillagerCureEnabled = true; ++ public boolean zombieVillagerAlwaysDropExp = false; ++ private void zombieVillagerSettings() { ++ zombieVillagerRidable = getBoolean("mobs.zombie_villager.ridable", zombieVillagerRidable); ++ zombieVillagerRidableInWater = getBoolean("mobs.zombie_villager.ridable-in-water", zombieVillagerRidableInWater); ++ zombieVillagerControllable = getBoolean("mobs.zombie_villager.controllable", zombieVillagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie_villager.attributes.max-health", zombieVillagerMaxHealth); ++ set("mobs.zombie_villager.attributes.max-health", null); ++ set("mobs.zombie_villager.attributes.max_health", oldValue); ++ } ++ zombieVillagerMaxHealth = getDouble("mobs.zombie_villager.attributes.max_health", zombieVillagerMaxHealth); ++ zombieVillagerSpawnReinforcements = getDouble("mobs.zombie_villager.attributes.spawn_reinforcements", zombieVillagerSpawnReinforcements); ++ zombieVillagerJockeyOnlyBaby = getBoolean("mobs.zombie_villager.jockey.only-babies", zombieVillagerJockeyOnlyBaby); ++ zombieVillagerJockeyChance = getDouble("mobs.zombie_villager.jockey.chance", zombieVillagerJockeyChance); ++ zombieVillagerJockeyTryExistingChickens = getBoolean("mobs.zombie_villager.jockey.try-existing-chickens", zombieVillagerJockeyTryExistingChickens); ++ zombieVillagerTakeDamageFromWater = getBoolean("mobs.zombie_villager.takes-damage-from-water", zombieVillagerTakeDamageFromWater); ++ zombieVillagerCuringTimeMin = getInt("mobs.zombie_villager.curing_time.min", zombieVillagerCuringTimeMin); ++ zombieVillagerCuringTimeMax = getInt("mobs.zombie_villager.curing_time.max", zombieVillagerCuringTimeMax); ++ zombieVillagerCureEnabled = getBoolean("mobs.zombie_villager.cure.enabled", zombieVillagerCureEnabled); ++ zombieVillagerAlwaysDropExp = getBoolean("mobs.zombie_villager.always-drop-exp", zombieVillagerAlwaysDropExp); ++ } ++ ++ public boolean zombifiedPiglinRidable = false; ++ public boolean zombifiedPiglinRidableInWater = false; ++ public boolean zombifiedPiglinControllable = true; ++ public double zombifiedPiglinMaxHealth = 20.0D; ++ public double zombifiedPiglinSpawnReinforcements = 0.0D; ++ public boolean zombifiedPiglinJockeyOnlyBaby = true; ++ public double zombifiedPiglinJockeyChance = 0.05D; ++ public boolean zombifiedPiglinJockeyTryExistingChickens = true; ++ public boolean zombifiedPiglinCountAsPlayerKillWhenAngry = true; ++ public boolean zombifiedPiglinTakeDamageFromWater = false; ++ public boolean zombifiedPiglinAlwaysDropExp = false; ++ private void zombifiedPiglinSettings() { ++ zombifiedPiglinRidable = getBoolean("mobs.zombified_piglin.ridable", zombifiedPiglinRidable); ++ zombifiedPiglinRidableInWater = getBoolean("mobs.zombified_piglin.ridable-in-water", zombifiedPiglinRidableInWater); ++ zombifiedPiglinControllable = getBoolean("mobs.zombified_piglin.controllable", zombifiedPiglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombified_piglin.attributes.max-health", zombifiedPiglinMaxHealth); ++ set("mobs.zombified_piglin.attributes.max-health", null); ++ set("mobs.zombified_piglin.attributes.max_health", oldValue); ++ } ++ zombifiedPiglinMaxHealth = getDouble("mobs.zombified_piglin.attributes.max_health", zombifiedPiglinMaxHealth); ++ zombifiedPiglinSpawnReinforcements = getDouble("mobs.zombified_piglin.attributes.spawn_reinforcements", zombifiedPiglinSpawnReinforcements); ++ zombifiedPiglinJockeyOnlyBaby = getBoolean("mobs.zombified_piglin.jockey.only-babies", zombifiedPiglinJockeyOnlyBaby); ++ zombifiedPiglinJockeyChance = getDouble("mobs.zombified_piglin.jockey.chance", zombifiedPiglinJockeyChance); ++ zombifiedPiglinJockeyTryExistingChickens = getBoolean("mobs.zombified_piglin.jockey.try-existing-chickens", zombifiedPiglinJockeyTryExistingChickens); ++ zombifiedPiglinCountAsPlayerKillWhenAngry = getBoolean("mobs.zombified_piglin.count-as-player-kill-when-angry", zombifiedPiglinCountAsPlayerKillWhenAngry); ++ zombifiedPiglinTakeDamageFromWater = getBoolean("mobs.zombified_piglin.takes-damage-from-water", zombifiedPiglinTakeDamageFromWater); ++ zombifiedPiglinAlwaysDropExp = getBoolean("mobs.zombified_piglin.always-drop-exp", zombifiedPiglinAlwaysDropExp); ++ } ++ ++ public float hungerStarvationDamage = 1.0F; ++ private void hungerSettings() { ++ hungerStarvationDamage = (float) getDouble("hunger.starvation-damage", hungerStarvationDamage); ++ } ++ ++ public int conduitDistance = 16; ++ public double conduitDamageDistance = 8; ++ public float conduitDamageAmount = 4; ++ public Block[] conduitBlocks; ++ private void conduitSettings() { ++ conduitDistance = getInt("blocks.conduit.effect-distance", conduitDistance); ++ conduitDamageDistance = getDouble("blocks.conduit.mob-damage.distance", conduitDamageDistance); ++ conduitDamageAmount = (float) getDouble("blocks.conduit.mob-damage.damage-amount", conduitDamageAmount); ++ List conduitBlockList = new ArrayList<>(); ++ getList("blocks.conduit.valid-ring-blocks", new ArrayList(){{ ++ add("minecraft:prismarine"); ++ add("minecraft:prismarine_bricks"); ++ add("minecraft:sea_lantern"); ++ add("minecraft:dark_prismarine"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ conduitBlockList.add(block); ++ } ++ }); ++ conduitBlocks = conduitBlockList.toArray(Block[]::new); ++ } ++ ++ public float cauldronRainChance = 0.05F; ++ public float cauldronPowderSnowChance = 0.1F; ++ public float cauldronDripstoneWaterFillChance = 0.17578125F; ++ public float cauldronDripstoneLavaFillChance = 0.05859375F; ++ private void cauldronSettings() { ++ cauldronRainChance = (float) getDouble("blocks.cauldron.fill-chances.rain", cauldronRainChance); ++ cauldronPowderSnowChance = (float) getDouble("blocks.cauldron.fill-chances.powder-snow", cauldronPowderSnowChance); ++ cauldronDripstoneWaterFillChance = (float) getDouble("blocks.cauldron.fill-chances.dripstone-water", cauldronDripstoneWaterFillChance); ++ cauldronDripstoneLavaFillChance = (float) getDouble("blocks.cauldron.fill-chances.dripstone-lava", cauldronDripstoneLavaFillChance); ++ } ++} ++ +diff --git a/src/main/java/org/purpurmc/purpur/command/CompassCommand.java b/src/main/java/org/purpurmc/purpur/command/CompassCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..34b6b1db6ef85d40cb84a5e19453ef5c5110d539 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/CompassCommand.java +@@ -0,0 +1,27 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.task.CompassTask; ++ ++public class CompassCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("compass") ++ .requires(listener -> listener.hasPermission(2)) ++ .executes(context -> { ++ ServerPlayer player = context.getSource().getPlayerOrException(); ++ CompassTask task = CompassTask.instance(); ++ if (player.compassBar()) { ++ task.removePlayer(player.getBukkitEntity()); ++ player.compassBar(false); ++ } else { ++ task.addPlayer(player.getBukkitEntity()); ++ player.compassBar(true); ++ } ++ return 1; ++ }) ++ ).setPermission("bukkit.command.compass"); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java b/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2189ca24f9fe53ad20ffba73ea73f6a0dc7891b8 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java +@@ -0,0 +1,34 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.network.protocol.game.ClientboundGameEventPacket; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class CreditsCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("credits") ++ .requires((listener) -> listener.hasPermission(2)) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ).setPermission("bukkit.command.credits"); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ ClientboundGameEventPacket packet = new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 1F); ++ player.connection.send(packet); ++ String output = String.format(PurpurConfig.creditsCommandOutput, player.getGameProfile().getName()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/DemoCommand.java b/src/main/java/org/purpurmc/purpur/command/DemoCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..83b3d1fb934d417702fc280e679f88d80f63cff2 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/DemoCommand.java +@@ -0,0 +1,34 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.network.protocol.game.ClientboundGameEventPacket; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class DemoCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("demo") ++ .requires((listener) -> listener.hasPermission(2)) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ).setPermission("bukkit.command.demo"); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ ClientboundGameEventPacket packet = new ClientboundGameEventPacket(ClientboundGameEventPacket.DEMO_EVENT, 0); ++ player.connection.send(packet); ++ String output = String.format(PurpurConfig.demoCommandOutput, player.getGameProfile().getName()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/PingCommand.java b/src/main/java/org/purpurmc/purpur/command/PingCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7c57e812451320da5c97008dd36f74856fec7c8 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/PingCommand.java +@@ -0,0 +1,32 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.craftbukkit.util.CraftChatMessage; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class PingCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("ping") ++ .requires((listener) -> listener.hasPermission(2)) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ).setPermission("bukkit.command.ping"); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ String output = String.format(PurpurConfig.pingCommandOutput, player.getGameProfile().getName(), player.latency); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java b/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2621e54879e9ab0029a875f1d09eee67878b90d5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java +@@ -0,0 +1,66 @@ ++package org.purpurmc.purpur.command; ++ ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.ChatColor; ++import org.bukkit.Location; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++ ++import java.io.File; ++import java.util.Collections; ++import java.util.List; ++import java.util.stream.Collectors; ++import java.util.stream.Stream; ++ ++public class PurpurCommand extends Command { ++ public PurpurCommand(String name) { ++ super(name); ++ this.description = "Purpur related commands"; ++ this.usageMessage = "/purpur [reload | version]"; ++ this.setPermission("bukkit.command.purpur"); ++ } ++ ++ @Override ++ public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { ++ if (args.length == 1) { ++ return Stream.of("reload", "version") ++ .filter(arg -> arg.startsWith(args[0].toLowerCase())) ++ .collect(Collectors.toList()); ++ } ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public boolean execute(CommandSender sender, String commandLabel, String[] args) { ++ if (!testPermission(sender)) return true; ++ ++ if (args.length != 1) { ++ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); ++ return false; ++ } ++ ++ if (args[0].equalsIgnoreCase("reload")) { ++ Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); ++ Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); ++ ++ MinecraftServer console = MinecraftServer.getServer(); ++ PurpurConfig.init((File) console.options.valueOf("purpur-settings")); ++ for (ServerLevel level : console.getAllLevels()) { ++ level.purpurConfig.init(); ++ level.resetBreedingCooldowns(); ++ } ++ console.server.reloadCount++; ++ ++ Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Purpur config reload complete."); ++ } else if (args[0].equalsIgnoreCase("version")) { ++ Command verCmd = org.bukkit.Bukkit.getServer().getCommandMap().getCommand("version"); ++ if (verCmd != null) { ++ return verCmd.execute(sender, commandLabel, new String[0]); ++ } ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java b/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9d3f7b4acab1e4502e6ab5d5b2cc400d948e1cef +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java +@@ -0,0 +1,43 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.RamBarTask; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class RamBarCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("rambar") ++ .requires(listener -> listener.hasPermission(2)) ++ .executes(context -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ).setPermission("bukkit.command.rambar"); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ boolean result = RamBarTask.instance().togglePlayer(player.getBukkitEntity()); ++ player.ramBar(result); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.rambarCommandOutput, ++ Placeholder.component("onoff", Component.translatable(result ? "options.on" : "options.off") ++ .color(result ? NamedTextColor.GREEN : NamedTextColor.RED)), ++ Placeholder.parsed("target", player.getGameProfile().getName())); ++ ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/RamCommand.java b/src/main/java/org/purpurmc/purpur/command/RamCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d16263d4ddf6e28fb99a5cd32a28be262f4c36a7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/RamCommand.java +@@ -0,0 +1,30 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.RamBarTask; ++ ++public class RamCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("ram") ++ .requires(listener -> listener.hasPermission(2)) ++ .executes(context -> { ++ CommandSourceStack sender = context.getSource(); ++ RamBarTask ramBar = RamBarTask.instance(); ++ sender.sendSuccess(PaperAdventure.asVanilla(MiniMessage.miniMessage().deserialize(PurpurConfig.ramCommandOutput, ++ Placeholder.component("allocated", ramBar.format(ramBar.getAllocated())), ++ Placeholder.component("used", ramBar.format(ramBar.getUsed())), ++ Placeholder.component("xmx", ramBar.format(ramBar.getXmx())), ++ Placeholder.component("xms", ramBar.format(ramBar.getXms())), ++ Placeholder.unparsed("percent", ((int) (ramBar.getPercent() * 100)) + "%") ++ )), false); ++ return 1; ++ }) ++ ).setPermission("bukkit.command.ram"); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java b/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7c367d17fa843d4d7562d05780ecffd47400fc13 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java +@@ -0,0 +1,43 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.TPSBarTask; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class TPSBarCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("tpsbar") ++ .requires(listener -> listener.hasPermission(2)) ++ .executes(context -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ).setPermission("bukkit.command.tpsbar"); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ boolean result = TPSBarTask.instance().togglePlayer(player.getBukkitEntity()); ++ player.tpsBar(result); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.tpsbarCommandOutput, ++ Placeholder.component("onoff", Component.translatable(result ? "options.on" : "options.off") ++ .color(result ? NamedTextColor.GREEN : NamedTextColor.RED)), ++ Placeholder.parsed("target", player.getGameProfile().getName())); ++ ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java b/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2a6685b016cca5a8be554b3b8a928ced8d3cebba +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java +@@ -0,0 +1,55 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.server.MinecraftServer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.concurrent.TimeUnit; ++import java.util.function.Function; ++ ++public class UptimeCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("uptime") ++ .requires((listener) -> listener.hasPermission(2)) ++ .executes((context) -> execute(context.getSource())) ++ ).setPermission("bukkit.command.uptime"); ++ } ++ ++ private static int execute(CommandSourceStack sender) { ++ Data data = new Data(); ++ ++ data.format = PurpurConfig.uptimeFormat; ++ data.hide = true; ++ data.millis = System.currentTimeMillis() - MinecraftServer.startTimeMillis; ++ ++ process(data, "", PurpurConfig.uptimeDay, PurpurConfig.uptimeDays, TimeUnit.DAYS, TimeUnit.MILLISECONDS::toDays); ++ process(data, "", PurpurConfig.uptimeHour, PurpurConfig.uptimeHours, TimeUnit.HOURS, TimeUnit.MILLISECONDS::toHours); ++ process(data, "", PurpurConfig.uptimeMinute, PurpurConfig.uptimeMinutes, TimeUnit.MINUTES, TimeUnit.MILLISECONDS::toMinutes); ++ data.hide = false; // never hide seconds ++ process(data, "", PurpurConfig.uptimeSecond, PurpurConfig.uptimeSeconds, TimeUnit.SECONDS, TimeUnit.MILLISECONDS::toSeconds); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.uptimeCommandOutput, Placeholder.unparsed("uptime", data.format)); ++ sender.sendSuccess(output, false); ++ return 1; ++ } ++ ++ private static void process(Data data, String replace, String singular, String plural, TimeUnit unit, Function func) { ++ if (data.format.contains(replace)) { ++ long val = func.apply(data.millis); ++ if (data.hide) data.hide = val == 0; ++ if (!data.hide) data.millis -= unit.toMillis(val); ++ data.format = data.format.replace(replace, data.hide ? "" : String.format(val == 1 ? singular : plural, val)); ++ } ++ } ++ ++ private static class Data { ++ String format; ++ boolean hide; ++ long millis; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed494e0ad278813a0eb261101447b84cca3ad7aa +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java +@@ -0,0 +1,71 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++ ++public class FlyingMoveControllerWASD extends MoveControllerWASD { ++ protected final float groundSpeedModifier; ++ protected final float flyingSpeedModifier; ++ protected int tooHighCooldown = 0; ++ protected boolean setNoGravityFlag; ++ ++ public FlyingMoveControllerWASD(Mob entity) { ++ this(entity, 1.0F); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier) { ++ this(entity, groundSpeedModifier, 1.0F, true); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, float flyingSpeedModifier) { ++ this(entity, groundSpeedModifier, flyingSpeedModifier, true); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, float flyingSpeedModifier, boolean setNoGravityFlag) { ++ super(entity); ++ this.groundSpeedModifier = groundSpeedModifier; ++ this.flyingSpeedModifier = flyingSpeedModifier; ++ this.setNoGravityFlag = setNoGravityFlag; ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = Math.max(0.0F, rider.getForwardMot()); ++ float vertical = forward == 0.0F ? 0.0F : -(rider.xRotO / 45.0F); ++ float strafe = rider.getStrafeMot(); ++ ++ if (rider.jumping && spacebarEvent(entity)) { ++ entity.onSpacebar(); ++ } ++ ++ if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) { ++ if (tooHighCooldown <= 0) { ++ tooHighCooldown = 20; ++ } ++ entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.05D, 0.0D)); ++ vertical = 0.0F; ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float speed = (float) getSpeedModifier(); ++ ++ if (entity.onGround) { ++ speed *= groundSpeedModifier; // TODO = fix this! ++ } else { ++ speed *= flyingSpeedModifier; ++ } ++ ++ if (setNoGravityFlag) { ++ entity.setNoGravity(forward > 0); ++ } ++ ++ entity.setSpeed(speed); ++ entity.setVerticalMot(vertical); ++ entity.setStrafeMot(strafe); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9383c07fa53141127106a1f289366a040960d52e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java +@@ -0,0 +1,63 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.phys.Vec3; ++ ++public class FlyingWithSpacebarMoveControllerWASD extends FlyingMoveControllerWASD { ++ public FlyingWithSpacebarMoveControllerWASD(Mob entity) { ++ super(entity); ++ } ++ ++ public FlyingWithSpacebarMoveControllerWASD(Mob entity, float groundSpeedModifier) { ++ super(entity, groundSpeedModifier); ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot() * 0.5F; ++ float vertical = 0; ++ ++ if (forward < 0.0F) { ++ forward *= 0.5F; ++ strafe *= 0.5F; ++ } ++ ++ float speed = (float) entity.getAttributeValue(Attributes.MOVEMENT_SPEED); ++ ++ if (entity.onGround) { ++ speed *= groundSpeedModifier; ++ } ++ ++ if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar()) { ++ entity.setNoGravity(true); ++ vertical = 1.0F; ++ } else { ++ entity.setNoGravity(false); ++ } ++ ++ if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) { ++ if (tooHighCooldown <= 0) { ++ tooHighCooldown = 20; ++ } ++ entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.2D, 0.0D)); ++ vertical = 0.0F; ++ } ++ ++ setSpeedModifier(speed); ++ entity.setSpeed((float) getSpeedModifier()); ++ entity.setVerticalMot(vertical); ++ entity.setStrafeMot(strafe); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ ++ Vec3 mot = entity.getDeltaMovement(); ++ if (mot.y > 0.2D) { ++ entity.setDeltaMovement(mot.x, 0.2D, mot.z); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b8c25c96e95dd5ec3ad9fa4c41bd6c08e144832d +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java +@@ -0,0 +1,76 @@ ++package org.purpurmc.purpur.controller; ++ ++ ++import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.control.LookControl; ++import net.minecraft.world.entity.player.Player; ++ ++public class LookControllerWASD extends LookControl { ++ protected final Mob entity; ++ private float yOffset = 0; ++ private float xOffset = 0; ++ ++ public LookControllerWASD(Mob entity) { ++ super(entity); ++ this.entity = entity; ++ } ++ ++ // tick ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ ++ protected void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot(), rider.getXRot()); ++ } ++ ++ public void vanillaTick() { ++ super.tick(); ++ } ++ ++ public void setYawPitch(float yRot, float xRot) { ++ entity.setXRot(normalizePitch(xRot + xOffset)); ++ entity.setYRot(normalizeYaw(yRot + yOffset)); ++ entity.setYHeadRot(entity.getYRot()); ++ entity.xRotO = entity.getXRot(); ++ entity.yRotO = entity.getYRot(); ++ ++ entity.tracker.broadcast(new ClientboundMoveEntityPacket ++ .PosRot(entity.getId(), ++ (short) 0, (short) 0, (short) 0, ++ (byte) Mth.floor(entity.getYRot() * 256.0F / 360.0F), ++ (byte) Mth.floor(entity.getXRot() * 256.0F / 360.0F), ++ entity.onGround)); ++ } ++ ++ public void setOffsets(float yaw, float pitch) { ++ yOffset = yaw; ++ xOffset = pitch; ++ } ++ ++ public float normalizeYaw(float yaw) { ++ yaw %= 360.0f; ++ if (yaw >= 180.0f) { ++ yaw -= 360.0f; ++ } else if (yaw < -180.0f) { ++ yaw += 360.0f; ++ } ++ return yaw; ++ } ++ ++ public float normalizePitch(float pitch) { ++ if (pitch > 90.0f) { ++ pitch = 90.0f; ++ } else if (pitch < -90.0f) { ++ pitch = -90.0f; ++ } ++ return pitch; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..21fd6ea2a482758a3016e3bc2cdebe2d89267481 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java +@@ -0,0 +1,89 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.ai.control.MoveControl; ++import net.minecraft.world.entity.player.Player; ++import org.purpurmc.purpur.event.entity.RidableSpacebarEvent; ++ ++public class MoveControllerWASD extends MoveControl { ++ protected final Mob entity; ++ private final double speedModifier; ++ ++ public MoveControllerWASD(Mob entity) { ++ this(entity, 1.0D); ++ } ++ ++ public MoveControllerWASD(Mob entity, double speedModifier) { ++ super(entity); ++ this.entity = entity; ++ this.speedModifier = speedModifier; ++ } ++ ++ @Override ++ public boolean hasWanted() { ++ return entity.getRider() != null ? strafeForwards != 0 || strafeRight != 0 : super.hasWanted(); ++ } ++ ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ ++ public void vanillaTick() { ++ super.tick(); ++ } ++ ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot() * 0.5F; ++ float strafe = rider.getStrafeMot() * 0.25F; ++ ++ if (forward <= 0.0F) { ++ forward *= 0.5F; ++ } ++ ++ float yawOffset = 0; ++ if (strafe != 0) { ++ if (forward == 0) { ++ yawOffset += strafe > 0 ? -90 : 90; ++ forward = Math.abs(strafe * 2); ++ } else { ++ yawOffset += strafe > 0 ? -30 : 30; ++ strafe /= 2; ++ if (forward < 0) { ++ yawOffset += strafe > 0 ? -110 : 110; ++ forward *= -1; ++ } ++ } ++ } else if (forward < 0) { ++ yawOffset -= 180; ++ forward *= -1; ++ } ++ ++ ((LookControllerWASD) entity.getLookControl()).setOffsets(yawOffset, 0); ++ ++ if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar() && entity.onGround) { ++ entity.jumpFromGround(); ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier); ++ ++ entity.setSpeed((float) getSpeedModifier()); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++ ++ public static boolean spacebarEvent(Mob entity) { ++ if (RidableSpacebarEvent.getHandlerList().getRegisteredListeners().length > 0) { ++ return new RidableSpacebarEvent(entity.getBukkitEntity()).callEvent(); ++ } else { ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ba2a37dad43e238e54632975abea8ee6fafaa9e0 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java +@@ -0,0 +1,50 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++ ++public class WaterMoveControllerWASD extends MoveControllerWASD { ++ private final double speedModifier; ++ ++ public WaterMoveControllerWASD(Mob entity) { ++ this(entity, 1.0D); ++ } ++ ++ public WaterMoveControllerWASD(Mob entity, double speedModifier) { ++ super(entity); ++ this.speedModifier = speedModifier; ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot() * 0.5F; // strafe slower by default ++ float vertical = -(rider.xRotO / 90); ++ ++ if (forward == 0.0F) { ++ // strafe slower if not moving forward ++ strafe *= 0.5F; ++ // do not move vertically if not moving forward ++ vertical = 0.0F; ++ } else if (forward < 0.0F) { ++ // water animals can't swim backwards ++ forward = 0.0F; ++ vertical = 0.0F; ++ } ++ ++ if (rider.jumping && spacebarEvent(entity)) { ++ entity.onSpacebar(); ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier); ++ entity.setSpeed((float) getSpeedModifier() * 0.1F); ++ ++ entity.setForwardMot(forward * (float) speedModifier); ++ entity.setStrafeMot(strafe * (float) speedModifier); ++ entity.setVerticalMot(vertical * (float) speedModifier); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5e99789e5156e8ffbf125e77114c547e1f8e7925 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +@@ -0,0 +1,104 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.animal.Dolphin; ++import net.minecraft.world.entity.projectile.LlamaSpit; ++import net.minecraft.world.entity.projectile.ProjectileUtil; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.EntityHitResult; ++import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.Vec3; ++ ++public class DolphinSpit extends LlamaSpit { ++ public LivingEntity dolphin; ++ public int ticksLived; ++ ++ public DolphinSpit(EntityType type, Level world) { ++ super(type, world); ++ } ++ ++ public DolphinSpit(Level world, Dolphin dolphin) { ++ this(EntityType.LLAMA_SPIT, world); ++ setOwner(dolphin.getRider() != null ? dolphin.getRider() : dolphin); ++ this.dolphin = dolphin; ++ this.setPos( ++ dolphin.getX() - (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(dolphin.yBodyRot * 0.017453292F), ++ dolphin.getEyeY() - 0.10000000149011612D, ++ dolphin.getZ() + (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(dolphin.yBodyRot * 0.017453292F)); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResult(this, this::canHitEntity); ++ ++ this.preOnHit(hitResult); ++ ++ double x = this.getX() + mot.x; ++ double y = this.getY() + mot.y; ++ double z = this.getZ() + mot.z; ++ ++ this.updateRotation(); ++ ++ Vec3 motDouble = mot.scale(2.0); ++ for (int i = 0; i < 5; i++) { ++ ((ServerLevel) level).sendParticles(null, ParticleTypes.BUBBLE, ++ getX() + random.nextFloat() / 2 - 0.25F, ++ getY() + random.nextFloat() / 2 - 0.25F, ++ getZ() + random.nextFloat() / 2 - 0.25F, ++ 0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true); ++ } ++ ++ if (++ticksLived > 20) { ++ this.discard(); ++ } else { ++ this.setDeltaMovement(mot.scale(0.99D)); ++ if (!this.isNoGravity()) { ++ this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D)); ++ } ++ ++ this.setPos(x, y, z); ++ } ++ } ++ ++ @Override ++ public void shoot(double x, double y, double z, float speed, float inaccuracy) { ++ setDeltaMovement(new Vec3(x, y, z).normalize().add( ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy) ++ .scale(speed)); ++ } ++ ++ @Override ++ protected void onHitEntity(EntityHitResult entityHitResult) { ++ Entity shooter = this.getOwner(); ++ if (shooter instanceof LivingEntity) { ++ entityHitResult.getEntity().hurt(DamageSource.indirectMobAttack(this, (LivingEntity) shooter).setProjectile(), level.purpurConfig.dolphinSpitDamage); ++ } ++ } ++ ++ @Override ++ protected void onHitBlock(BlockHitResult blockHitResult) { ++ if (this.hitCancelled) { ++ return; ++ } ++ BlockState state = this.level.getBlockState(blockHitResult.getBlockPos()); ++ state.onProjectileHit(this.level, state, blockHitResult, this); ++ this.discard(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/GlowSquidColor.java b/src/main/java/org/purpurmc/purpur/entity/GlowSquidColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c90256f4c16ffdb2d8e767e837ea36ac7a6613be +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/GlowSquidColor.java +@@ -0,0 +1,61 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.util.RandomSource; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++ ++public enum GlowSquidColor { ++ BLUE, RED, GREEN, PINK, YELLOW, ORANGE, INDIGO, PURPLE, WHITE, GRAY, BLACK; ++ ++ @Override ++ public String toString() { ++ return this.name().toLowerCase(Locale.ROOT); ++ } ++ ++ public enum Mode { ++ RAINBOW(RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PURPLE), ++ ALL_COLORS(BLUE, RED, GREEN, PINK, YELLOW, ORANGE, INDIGO, PURPLE, WHITE, GRAY, BLACK), ++ TRANS_PRIDE(BLUE, WHITE, PINK), ++ LESBIAN_PRIDE(RED, ORANGE, WHITE, PINK, PURPLE), ++ BI_PRIDE(BLUE, PINK, PURPLE), ++ GAY_PRIDE(BLUE, GREEN, WHITE), ++ PAN_PRIDE(PINK, YELLOW, BLUE), ++ ACE_PRIDE(BLACK, GRAY, WHITE, PURPLE), ++ ARO_PRIDE(BLACK, GRAY, WHITE, GREEN), ++ ENBY_PRIDE(YELLOW, WHITE, BLACK, PURPLE), ++ GENDERFLUID(PURPLE, WHITE, BLACK, PINK, BLUE), ++ MONOCHROME(BLACK, GRAY, WHITE), ++ VANILLA(BLUE); ++ ++ private static final Map BY_NAME = new HashMap<>(); ++ ++ static { ++ Arrays.stream(values()).forEach(mode -> BY_NAME.put(mode.name(), mode)); ++ } ++ ++ private final List colors = new ArrayList<>(); ++ ++ Mode(GlowSquidColor... colors) { ++ this.colors.addAll(Arrays.stream(colors).toList()); ++ } ++ ++ public static Mode get(String string) { ++ Mode mode = BY_NAME.get(string.toUpperCase(Locale.ROOT)); ++ return mode == null ? RAINBOW : mode; ++ } ++ ++ public GlowSquidColor getRandom(RandomSource random) { ++ return this.colors.get(random.nextInt(this.colors.size())); ++ } ++ ++ @Override ++ public String toString() { ++ return this.name().toLowerCase(Locale.ROOT); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b6a594cd6b08c687cf51c2f5494297ef96ec4b92 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +@@ -0,0 +1,119 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.decoration.ArmorStand; ++import net.minecraft.world.entity.monster.Phantom; ++import net.minecraft.world.entity.projectile.LlamaSpit; ++import net.minecraft.world.entity.projectile.ProjectileUtil; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockBehaviour; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.EntityHitResult; ++import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.Vec3; ++ ++public class PhantomFlames extends LlamaSpit { ++ public Phantom phantom; ++ public int ticksLived; ++ public boolean canGrief = false; ++ ++ public PhantomFlames(EntityType type, Level world) { ++ super(type, world); ++ } ++ ++ public PhantomFlames(Level world, Phantom phantom) { ++ this(EntityType.LLAMA_SPIT, world); ++ setOwner(phantom.getRider() != null ? phantom.getRider() : phantom); ++ this.phantom = phantom; ++ this.setPos( ++ phantom.getX() - (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(phantom.yBodyRot * 0.017453292F), ++ phantom.getEyeY() - 0.10000000149011612D, ++ phantom.getZ() + (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(phantom.yBodyRot * 0.017453292F)); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResult(this, this::canHitEntity); ++ ++ this.preOnHit(hitResult); ++ ++ double x = this.getX() + mot.x; ++ double y = this.getY() + mot.y; ++ double z = this.getZ() + mot.z; ++ ++ this.updateRotation(); ++ ++ Vec3 motDouble = mot.scale(2.0); ++ for (int i = 0; i < 5; i++) { ++ ((ServerLevel) level).sendParticles(null, ParticleTypes.FLAME, ++ getX() + random.nextFloat() / 2 - 0.25F, ++ getY() + random.nextFloat() / 2 - 0.25F, ++ getZ() + random.nextFloat() / 2 - 0.25F, ++ 0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true); ++ } ++ ++ if (++ticksLived > 20) { ++ this.discard(); ++ } else if (this.level.getBlockStates(this.getBoundingBox()).noneMatch(BlockBehaviour.BlockStateBase::isAir)) { ++ this.discard(); ++ } else if (this.isInWaterOrBubble()) { ++ this.discard(); ++ } else { ++ this.setDeltaMovement(mot.scale(0.99D)); ++ if (!this.isNoGravity()) { ++ this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D)); ++ } ++ ++ this.setPos(x, y, z); ++ } ++ } ++ ++ @Override ++ public void shoot(double x, double y, double z, float speed, float inaccuracy) { ++ setDeltaMovement(new Vec3(x, y, z).normalize().add( ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy) ++ .scale(speed)); ++ } ++ ++ @Override ++ protected void onHitEntity(EntityHitResult entityHitResult) { ++ Entity shooter = this.getOwner(); ++ if (shooter instanceof LivingEntity) { ++ Entity target = entityHitResult.getEntity(); ++ if (canGrief || (target instanceof LivingEntity && !(target instanceof ArmorStand))) { ++ boolean hurt = target.hurt(DamageSource.indirectMobAttack(this, (LivingEntity) shooter).setProjectile(), level.purpurConfig.phantomFlameDamage); ++ if (hurt && level.purpurConfig.phantomFlameFireTime > 0) { ++ target.setSecondsOnFire(level.purpurConfig.phantomFlameFireTime); ++ } ++ } ++ } ++ } ++ ++ @Override ++ protected void onHitBlock(BlockHitResult blockHitResult) { ++ if (this.hitCancelled) { ++ return; ++ } ++ if (this.canGrief) { ++ BlockState state = this.level.getBlockState(blockHitResult.getBlockPos()); ++ state.onProjectileHit(this.level, state, blockHitResult, this); ++ } ++ this.discard(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8babdaddd8b33278aea0369dbbeeb445abe45016 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java +@@ -0,0 +1,20 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.goal.Goal; ++ ++import java.util.EnumSet; ++ ++public class HasRider extends Goal { ++ public final Mob entity; ++ ++ public HasRider(Mob entity) { ++ this.entity = entity; ++ setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK, Flag.TARGET, Flag.UNKNOWN_BEHAVIOR)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ return entity.getRider() != null && entity.isControllable(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..432f4f3d82af2f19820890b68d33189a9f2c69f9 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java +@@ -0,0 +1,17 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.animal.horse.AbstractHorse; ++ ++public class HorseHasRider extends HasRider { ++ public final AbstractHorse horse; ++ ++ public HorseHasRider(AbstractHorse entity) { ++ super(entity); ++ this.horse = entity; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && horse.isSaddled(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..18a95e043cbffa65eeaaf65ff7695e5dc939820c +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java +@@ -0,0 +1,17 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.animal.horse.Llama; ++ ++public class LlamaHasRider extends HasRider { ++ public final Llama llama; ++ ++ public LlamaHasRider(Llama entity) { ++ super(entity); ++ this.llama = entity; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && llama.isSaddled() && llama.isControllable(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java b/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java +new file mode 100644 +index 0000000000000000000000000000000000000000..115a3b36cbb7716b28ef940a29ca97ac42a8a521 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java +@@ -0,0 +1,91 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.ai.goal.Goal; ++import net.minecraft.world.entity.animal.IronGolem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.block.Blocks; ++ ++import java.util.EnumSet; ++import java.util.UUID; ++ ++public class ReceiveFlower extends Goal { ++ private final IronGolem irongolem; ++ private ServerPlayer target; ++ private int cooldown; ++ ++ public ReceiveFlower(IronGolem entity) { ++ this.irongolem = entity; ++ setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ if (this.irongolem.getOfferFlowerTick() > 0) { ++ return false; ++ } ++ if (!this.irongolem.isAngry()) { ++ return false; ++ } ++ UUID uuid = this.irongolem.getPersistentAngerTarget(); ++ if (uuid == null) { ++ return false; ++ } ++ Entity target = ((ServerLevel) this.irongolem.level).getEntity(uuid); ++ if (!(target instanceof ServerPlayer player)) { ++ return false; ++ } ++ InteractionHand hand = getPoppyHand(player); ++ if (hand == null) { ++ return false; ++ } ++ removeFlower(player, hand); ++ this.target = player; ++ return true; ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return this.cooldown > 0; ++ } ++ ++ @Override ++ public void start() { ++ this.cooldown = 100; ++ this.irongolem.stopBeingAngry(); ++ this.irongolem.offerFlower(true); ++ } ++ ++ @Override ++ public void stop() { ++ this.irongolem.offerFlower(false); ++ this.target = null; ++ } ++ ++ @Override ++ public void tick() { ++ this.irongolem.getLookControl().setLookAt(this.target, 30.0F, 30.0F); ++ --this.cooldown; ++ } ++ ++ private InteractionHand getPoppyHand(ServerPlayer player) { ++ if (isPoppy(player.getMainHandItem())) { ++ return InteractionHand.MAIN_HAND; ++ } ++ if (isPoppy(player.getOffhandItem())) { ++ return InteractionHand.OFF_HAND; ++ } ++ return null; ++ } ++ ++ private void removeFlower(ServerPlayer player, InteractionHand hand) { ++ player.setItemInHand(hand, ItemStack.EMPTY); ++ } ++ ++ private boolean isPoppy(ItemStack item) { ++ return item.getItem() == Blocks.POPPY.asItem(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java b/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f526883495b3222746de3d0442e9e4fb5107036 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java +@@ -0,0 +1,26 @@ ++package org.purpurmc.purpur.item; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.item.ItemNameBlockItem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import org.bukkit.event.entity.EntityPotionEffectEvent; ++ ++public class GlowBerryItem extends ItemNameBlockItem { ++ public GlowBerryItem(Block block, Properties settings) { ++ super(block, settings); ++ } ++ ++ @Override ++ public ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) { ++ ItemStack result = super.finishUsingItem(stack, world, user); ++ if (world.purpurConfig.glowBerriesEatGlowDuration > 0 && user instanceof ServerPlayer player) { ++ player.addEffect(new MobEffectInstance(MobEffects.GLOWING, world.purpurConfig.glowBerriesEatGlowDuration), EntityPotionEffectEvent.Cause.FOOD); ++ } ++ return result; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java b/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c038fb2bbb0f0e78380bc24bbd6348b869669a90 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java +@@ -0,0 +1,36 @@ ++package org.purpurmc.purpur.item; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.BlockItem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.SpawnerBlockEntity; ++import net.minecraft.world.level.block.state.BlockState; ++ ++public class SpawnerItem extends BlockItem { ++ ++ public SpawnerItem(Block block, Properties settings) { ++ super(block, settings); ++ } ++ ++ @Override ++ protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, Player player, ItemStack stack, BlockState state) { ++ boolean handled = super.updateCustomBlockEntityTag(pos, level, player, stack, state); ++ if (level.purpurConfig.silkTouchEnabled && player.getBukkitEntity().hasPermission("purpur.place.spawners")) { ++ BlockEntity spawner = level.getBlockEntity(pos); ++ if (spawner instanceof SpawnerBlockEntity && stack.hasTag()) { ++ CompoundTag tag = stack.getTag(); ++ if (tag.contains("Purpur.mob_type")) { ++ EntityType.byString(tag.getString("Purpur.mob_type")).ifPresent(type -> ++ ((SpawnerBlockEntity) spawner).getSpawner().setEntityId(type, level, level.random, pos)); ++ } ++ } ++ } ++ return handled; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java b/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..055dd307e9d5ac0d4623c961164c84bab1edd3bd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java +@@ -0,0 +1,81 @@ ++package org.purpurmc.purpur.task; ++ ++import com.google.common.io.ByteArrayDataInput; ++import com.google.common.io.ByteArrayDataOutput; ++import com.google.common.io.ByteStreams; ++import io.netty.buffer.Unpooled; ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.FriendlyByteBuf; ++import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.block.entity.BeehiveBlockEntity; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; ++import org.bukkit.entity.Player; ++import org.bukkit.plugin.PluginBase; ++import org.bukkit.plugin.messaging.PluginMessageListener; ++import org.jetbrains.annotations.NotNull; ++ ++public class BeehiveTask implements PluginMessageListener { ++ public static final ResourceLocation BEEHIVE_C2S = new ResourceLocation("purpur", "beehive_c2s"); ++ public static final ResourceLocation BEEHIVE_S2C = new ResourceLocation("purpur", "beehive_s2c"); ++ ++ private static BeehiveTask instance; ++ ++ public static BeehiveTask instance() { ++ if (instance == null) { ++ instance = new BeehiveTask(); ++ } ++ return instance; ++ } ++ ++ private final PluginBase plugin = new MinecraftInternalPlugin(); ++ ++ private BeehiveTask() { ++ } ++ ++ public void register() { ++ Bukkit.getMessenger().registerOutgoingPluginChannel(this.plugin, BEEHIVE_S2C.toString()); ++ Bukkit.getMessenger().registerIncomingPluginChannel(this.plugin, BEEHIVE_C2S.toString(), this); ++ } ++ ++ public void unregister() { ++ Bukkit.getMessenger().unregisterOutgoingPluginChannel(this.plugin, BEEHIVE_S2C.toString()); ++ Bukkit.getMessenger().unregisterIncomingPluginChannel(this.plugin, BEEHIVE_C2S.toString()); ++ } ++ ++ @Override ++ public void onPluginMessageReceived(@NotNull String channel, Player player, byte[] bytes) { ++ ByteArrayDataInput in = in(bytes); ++ long packedPos = in.readLong(); ++ BlockPos pos = BlockPos.of(packedPos); ++ ++ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); ++ ++ BlockEntity blockEntity = serverPlayer.level.getBlockEntity(pos); ++ if (!(blockEntity instanceof BeehiveBlockEntity beehive)) { ++ return; ++ } ++ ++ ByteArrayDataOutput out = out(); ++ ++ out.writeInt(beehive.getOccupantCount()); ++ out.writeLong(packedPos); ++ ++ FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(out.toByteArray())); ++ serverPlayer.connection.send(new ClientboundCustomPayloadPacket(BEEHIVE_S2C, buf)); ++ } ++ ++ @SuppressWarnings("UnstableApiUsage") ++ private static ByteArrayDataOutput out() { ++ return ByteStreams.newDataOutput(); ++ } ++ ++ @SuppressWarnings("UnstableApiUsage") ++ private static ByteArrayDataInput in(byte[] bytes) { ++ return ByteStreams.newDataInput(bytes); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/BossBarTask.java b/src/main/java/org/purpurmc/purpur/task/BossBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..114f273dd7f8b8a3c02f0651f6944859b33a65d4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/BossBarTask.java +@@ -0,0 +1,121 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; ++import org.bukkit.entity.Player; ++import org.bukkit.scheduler.BukkitRunnable; ++ ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.UUID; ++ ++public abstract class BossBarTask extends BukkitRunnable { ++ private final Map bossbars = new HashMap<>(); ++ private boolean started; ++ ++ abstract BossBar createBossBar(); ++ ++ abstract void updateBossBar(BossBar bossbar, Player player); ++ ++ @Override ++ public void run() { ++ Iterator> iter = bossbars.entrySet().iterator(); ++ while (iter.hasNext()) { ++ Map.Entry entry = iter.next(); ++ Player player = Bukkit.getPlayer(entry.getKey()); ++ if (player == null) { ++ iter.remove(); ++ continue; ++ } ++ updateBossBar(entry.getValue(), player); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ super.cancel(); ++ new HashSet<>(this.bossbars.keySet()).forEach(uuid -> { ++ Player player = Bukkit.getPlayer(uuid); ++ if (player != null) { ++ removePlayer(player); ++ } ++ }); ++ this.bossbars.clear(); ++ } ++ ++ public boolean removePlayer(Player player) { ++ BossBar bossbar = this.bossbars.remove(player.getUniqueId()); ++ if (bossbar != null) { ++ player.hideBossBar(bossbar); ++ return true; ++ } ++ return false; ++ } ++ ++ public void addPlayer(Player player) { ++ removePlayer(player); ++ BossBar bossbar = createBossBar(); ++ this.bossbars.put(player.getUniqueId(), bossbar); ++ this.updateBossBar(bossbar, player); ++ player.showBossBar(bossbar); ++ } ++ ++ public boolean hasPlayer(UUID uuid) { ++ return this.bossbars.containsKey(uuid); ++ } ++ ++ public boolean togglePlayer(Player player) { ++ if (removePlayer(player)) { ++ return false; ++ } ++ addPlayer(player); ++ return true; ++ } ++ ++ public void start() { ++ stop(); ++ this.runTaskTimerAsynchronously(new MinecraftInternalPlugin(), 1, 1); ++ started = true; ++ } ++ ++ public void stop() { ++ if (started) { ++ cancel(); ++ } ++ } ++ ++ public static void startAll() { ++ RamBarTask.instance().start(); ++ TPSBarTask.instance().start(); ++ CompassTask.instance().start(); ++ } ++ ++ public static void stopAll() { ++ RamBarTask.instance().stop(); ++ TPSBarTask.instance().stop(); ++ CompassTask.instance().stop(); ++ } ++ ++ public static void addToAll(ServerPlayer player) { ++ Player bukkit = player.getBukkitEntity(); ++ if (player.ramBar()) { ++ RamBarTask.instance().addPlayer(bukkit); ++ } ++ if (player.tpsBar()) { ++ TPSBarTask.instance().addPlayer(bukkit); ++ } ++ if (player.compassBar()) { ++ CompassTask.instance().addPlayer(bukkit); ++ } ++ } ++ ++ public static void removeFromAll(Player player) { ++ RamBarTask.instance().removePlayer(player); ++ TPSBarTask.instance().removePlayer(player); ++ CompassTask.instance().removePlayer(player); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/CompassTask.java b/src/main/java/org/purpurmc/purpur/task/CompassTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bece7eefc8ba8822b433835526251d2fb916c025 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/CompassTask.java +@@ -0,0 +1,68 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.item.Items; ++import org.bukkit.entity.Player; ++import org.purpurmc.purpur.PurpurConfig; ++ ++public class CompassTask extends BossBarTask { ++ private static CompassTask instance; ++ ++ private int tick = 0; ++ ++ public static CompassTask instance() { ++ if (instance == null) { ++ instance = new CompassTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ public void run() { ++ if (++tick < PurpurConfig.commandCompassBarTickInterval) { ++ return; ++ } ++ tick = 0; ++ ++ MinecraftServer.getServer().getAllLevels().forEach((level) -> { ++ if (level.purpurConfig.compassItemShowsBossBar) { ++ level.players().forEach(player -> { ++ if (!player.compassBar()) { ++ if (player.getMainHandItem().getItem() != Items.COMPASS && player.getOffhandItem().getItem() != Items.COMPASS) { ++ removePlayer(player.getBukkitEntity()); ++ } else if (!hasPlayer(player.getUUID())) { ++ addPlayer(player.getBukkitEntity()); ++ } ++ } ++ }); ++ } ++ }); ++ ++ super.run(); ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), PurpurConfig.commandCompassBarProgressPercent, PurpurConfig.commandCompassBarProgressColor, PurpurConfig.commandCompassBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ float yaw = player.getLocation().getYaw(); ++ int length = PurpurConfig.commandCompassBarTitle.length(); ++ int pos = (int) ((normalize(yaw) * (length / 720F)) + (length / 2F)); ++ bossbar.name(Component.text(PurpurConfig.commandCompassBarTitle.substring(pos - 25, pos + 25))); ++ } ++ ++ private float normalize(float yaw) { ++ while (yaw < -180.0F) { ++ yaw += 360.0F; ++ } ++ while (yaw > 180.0F) { ++ yaw -= 360.0F; ++ } ++ return yaw; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/RamBarTask.java b/src/main/java/org/purpurmc/purpur/task/RamBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e98c0ae73e2c40002a72b5d0d246ffa0c3ab38f +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/RamBarTask.java +@@ -0,0 +1,117 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import org.bukkit.entity.Player; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.lang.management.ManagementFactory; ++import java.lang.management.MemoryUsage; ++ ++public class RamBarTask extends BossBarTask { ++ private static RamBarTask instance; ++ private long allocated = 0L; ++ private long used = 0L; ++ private long xmx = 0L; ++ private long xms = 0L; ++ private float percent = 0F; ++ private int tick = 0; ++ ++ public static RamBarTask instance() { ++ if (instance == null) { ++ instance = new RamBarTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandRamBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ bossbar.progress(getBossBarProgress()); ++ bossbar.color(getBossBarColor()); ++ bossbar.name(MiniMessage.miniMessage().deserialize(PurpurConfig.commandRamBarTitle, ++ Placeholder.component("allocated", format(this.allocated)), ++ Placeholder.component("used", format(this.used)), ++ Placeholder.component("xmx", format(this.xmx)), ++ Placeholder.component("xms", format(this.xms)), ++ Placeholder.unparsed("percent", ((int) (this.percent * 100)) + "%") ++ )); ++ } ++ ++ @Override ++ public void run() { ++ if (++this.tick < PurpurConfig.commandRamBarTickInterval) { ++ return; ++ } ++ this.tick = 0; ++ ++ MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); ++ ++ this.allocated = heap.getCommitted(); ++ this.used = heap.getUsed(); ++ this.xmx = heap.getMax(); ++ this.xms = heap.getInit(); ++ this.percent = Math.max(Math.min((float) this.used / this.xmx, 1.0F), 0.0F); ++ ++ super.run(); ++ } ++ ++ private float getBossBarProgress() { ++ return this.percent; ++ } ++ ++ private BossBar.Color getBossBarColor() { ++ if (this.percent < 0.5F) { ++ return PurpurConfig.commandRamBarProgressColorGood; ++ } else if (this.percent < 0.75F) { ++ return PurpurConfig.commandRamBarProgressColorMedium; ++ } else { ++ return PurpurConfig.commandRamBarProgressColorLow; ++ } ++ } ++ ++ public Component format(long v) { ++ String color; ++ if (this.percent < 0.60F) { ++ color = PurpurConfig.commandRamBarTextColorGood; ++ } else if (this.percent < 0.85F) { ++ color = PurpurConfig.commandRamBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandRamBarTextColorLow; ++ } ++ String value; ++ if (v < 1024) { ++ value = v + "B"; ++ } else { ++ int z = (63 - Long.numberOfLeadingZeros(v)) / 10; ++ value = String.format("%.1f%s", (double) v / (1L << (z * 10)), "BKMGTPE".charAt(z)); ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.unparsed("text", value)); ++ } ++ ++ public long getAllocated() { ++ return this.allocated; ++ } ++ ++ public long getUsed() { ++ return this.used; ++ } ++ ++ public long getXmx() { ++ return this.xmx; ++ } ++ ++ public long getXms() { ++ return this.xms; ++ } ++ ++ public float getPercent() { ++ return this.percent; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java b/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8769993e7ca59da309087051a3cd38fc562c15d1 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java +@@ -0,0 +1,142 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.Bukkit; ++import org.bukkit.entity.Player; ++ ++public class TPSBarTask extends BossBarTask { ++ private static TPSBarTask instance; ++ private double tps = 20.0D; ++ private double mspt = 0.0D; ++ private int tick = 0; ++ ++ public static TPSBarTask instance() { ++ if (instance == null) { ++ instance = new TPSBarTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandTPSBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ bossbar.progress(getBossBarProgress()); ++ bossbar.color(getBossBarColor()); ++ bossbar.name(MiniMessage.miniMessage().deserialize(PurpurConfig.commandTPSBarTitle, ++ Placeholder.component("tps", getTPSColor()), ++ Placeholder.component("mspt", getMSPTColor()), ++ Placeholder.component("ping", getPingColor(player.getPing())) ++ )); ++ } ++ ++ @Override ++ public void run() { ++ if (++tick < PurpurConfig.commandTPSBarTickInterval) { ++ return; ++ } ++ tick = 0; ++ ++ this.tps = Math.max(Math.min(Bukkit.getTPS()[0], 20.0D), 0.0D); ++ this.mspt = Bukkit.getAverageTickTime(); ++ ++ super.run(); ++ } ++ ++ private float getBossBarProgress() { ++ if (PurpurConfig.commandTPSBarProgressFillMode == FillMode.MSPT) { ++ return Math.max(Math.min((float) mspt / 50.0F, 1.0F), 0.0F); ++ } else { ++ return Math.max(Math.min((float) tps / 20.0F, 1.0F), 0.0F); ++ } ++ } ++ ++ private BossBar.Color getBossBarColor() { ++ if (isGood(PurpurConfig.commandTPSBarProgressFillMode)) { ++ return PurpurConfig.commandTPSBarProgressColorGood; ++ } else if (isMedium(PurpurConfig.commandTPSBarProgressFillMode)) { ++ return PurpurConfig.commandTPSBarProgressColorMedium; ++ } else { ++ return PurpurConfig.commandTPSBarProgressColorLow; ++ } ++ } ++ ++ private boolean isGood(FillMode mode) { ++ return isGood(mode, 0); ++ } ++ ++ private boolean isGood(FillMode mode, int ping) { ++ if (mode == FillMode.MSPT) { ++ return mspt < 40; ++ } else if (mode == FillMode.TPS) { ++ return tps >= 19; ++ } else if (mode == FillMode.PING) { ++ return ping < 100; ++ } else { ++ return false; ++ } ++ } ++ ++ private boolean isMedium(FillMode mode) { ++ return isMedium(mode, 0); ++ } ++ ++ private boolean isMedium(FillMode mode, int ping) { ++ if (mode == FillMode.MSPT) { ++ return mspt < 50; ++ } else if (mode == FillMode.TPS) { ++ return tps >= 15; ++ } else if (mode == FillMode.PING) { ++ return ping < 200; ++ } else { ++ return false; ++ } ++ } ++ ++ private Component getTPSColor() { ++ String color; ++ if (isGood(FillMode.TPS)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.TPS)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%.2f", tps))); ++ } ++ ++ private Component getMSPTColor() { ++ String color; ++ if (isGood(FillMode.MSPT)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.MSPT)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%.2f", mspt))); ++ } ++ ++ private Component getPingColor(int ping) { ++ String color; ++ if (isGood(FillMode.PING, ping)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.PING, ping)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%s", ping))); ++ } ++ ++ public enum FillMode { ++ TPS, MSPT, PING ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Actionable.java b/src/main/java/org/purpurmc/purpur/tool/Actionable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e18c37f06730da9d3055d5215e813b1477c1e70e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Actionable.java +@@ -0,0 +1,24 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public abstract class Actionable { ++ private final Block into; ++ private final Map drops; ++ ++ public Actionable(Block into, Map drops) { ++ this.into = into; ++ this.drops = drops; ++ } ++ ++ public Block into() { ++ return into; ++ } ++ ++ public Map drops() { ++ return drops; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Strippable.java b/src/main/java/org/purpurmc/purpur/tool/Strippable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bf5402214f41af9c09bd6c5c4f45d330516d742e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Strippable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Strippable extends Actionable { ++ public Strippable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Tillable.java b/src/main/java/org/purpurmc/purpur/tool/Tillable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..715f6dd44480347eebced43c11bc364e05727498 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Tillable.java +@@ -0,0 +1,50 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.HoeItem; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.context.UseOnContext; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Predicate; ++ ++public class Tillable extends Actionable { ++ private final Condition condition; ++ ++ public Tillable(Condition condition, Block into, Map drops) { ++ super(into, drops); ++ this.condition = condition; ++ } ++ ++ public Condition condition() { ++ return condition; ++ } ++ ++ public enum Condition { ++ AIR_ABOVE(HoeItem::onlyIfAirAbove), ++ ALWAYS((useOnContext) -> true); ++ ++ private final Predicate predicate; ++ ++ Condition(Predicate predicate) { ++ this.predicate = predicate; ++ } ++ ++ public Predicate predicate() { ++ return predicate; ++ } ++ ++ private static final Map BY_NAME = new HashMap<>(); ++ ++ static { ++ for (Condition condition : values()) { ++ BY_NAME.put(condition.name(), condition); ++ } ++ } ++ ++ public static Condition get(String name) { ++ return BY_NAME.get(name.toUpperCase(java.util.Locale.ROOT)); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Waxable.java b/src/main/java/org/purpurmc/purpur/tool/Waxable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64adb13b29b6757dcf227a55588da70ecabe083f +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Waxable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Waxable extends Actionable { ++ public Waxable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Weatherable.java b/src/main/java/org/purpurmc/purpur/tool/Weatherable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7586f494528f30eb0da82420d3bcf5b83a1a902 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Weatherable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Weatherable extends Actionable { ++ public Weatherable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 63d3fcc45be732a4cd2dc8b5347d860fd6577bdd..665625c69b93a2568b1f2218a0db39da435d8c99 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -15,6 +15,7 @@ import net.minecraft.world.entity.ambient.AmbientCreature; + import net.minecraft.world.entity.animal.Animal; + import net.minecraft.world.entity.animal.Bee; + import net.minecraft.world.entity.animal.Sheep; ++import net.minecraft.world.entity.animal.Squid; + import net.minecraft.world.entity.animal.WaterAnimal; + import net.minecraft.world.entity.animal.horse.Llama; + import net.minecraft.world.entity.boss.EnderDragonPart; +@@ -169,7 +170,7 @@ public class ActivationRange + */ + public static void activateEntities(Level world) + { +- MinecraftTimings.entityActivationCheckTimer.startTiming(); ++ //MinecraftTimings.entityActivationCheckTimer.startTiming(); // Purpur + final int miscActivationRange = world.spigotConfig.miscActivationRange; + final int raiderActivationRange = world.spigotConfig.raiderActivationRange; + final int animalActivationRange = world.spigotConfig.animalActivationRange; +@@ -203,6 +204,7 @@ public class ActivationRange + continue; + } + ++ if (!player.level.purpurConfig.idleTimeoutTickNearbyEntities && player.isAfk()) continue; // Purpur + // Paper start + int worldHeight = world.getHeight(); + ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); +@@ -242,7 +244,7 @@ public class ActivationRange + } + // Paper end + } +- MinecraftTimings.entityActivationCheckTimer.stopTiming(); ++ //MinecraftTimings.entityActivationCheckTimer.stopTiming(); // Purpur + } + + /** +@@ -395,6 +397,7 @@ public class ActivationRange + */ + public static boolean checkIfActive(Entity entity) + { ++ if (entity.level.purpurConfig.squidImmuneToEAR && entity instanceof Squid) return true; // Purpur + // Never safe to skip fireworks or entities not yet added to chunk + if ( entity instanceof FireworkRocketEntity ) { + return true; +diff --git a/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +index 9bede6a26c08ede063c7a38f1149c811df14b258..088239d17aa8178cf8af09ec23cfd4deaaf2bbb6 100644 +--- a/src/main/java/org/spigotmc/TicksPerSecondCommand.java ++++ b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +@@ -31,7 +31,7 @@ public class TicksPerSecondCommand extends Command + for ( int i = 0; i < tps.length; i++) { + tpsAvg[i] = TicksPerSecondCommand.format( tps[i] ); + } +- sender.sendMessage(ChatColor.GOLD + "TPS from last 1m, 5m, 15m: " + org.apache.commons.lang.StringUtils.join(tpsAvg, ", ")); ++ sender.sendMessage(ChatColor.GOLD + "TPS from last 5s, 1m, 5m, 15m: " + org.apache.commons.lang.StringUtils.join(tpsAvg, ", ")); // Purpur + if (args.length > 0 && args[0].equals("mem") && sender.hasPermission("bukkit.command.tpsmemory")) { + sender.sendMessage(ChatColor.GOLD + "Current Memory Usage: " + ChatColor.GREEN + ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024)) + "/" + (Runtime.getRuntime().totalMemory() / (1024 * 1024)) + " mb (Max: " + (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " mb)"); + if (!hasShownMemoryWarning) { +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index e9fa7faaa4451e36b3908cbcbbe0baf213abde96..a810bfd3b8d6bd4d8f2ef8797e4281ae4fe8a67f 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -96,7 +96,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + + private WatchdogThread(long timeoutTime, boolean restart) + { +- super( "Paper Watchdog Thread" ); ++ super( "Watchdog Thread" ); // Purpur - use a generic name + this.timeoutTime = timeoutTime; + this.restart = restart; + earlyWarningEvery = Math.min(io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.earlyWarningEvery, timeoutTime); // Paper +@@ -155,14 +155,14 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + if (isLongTimeout) { + // Paper end + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper ++ log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Purpur bug." ); // Paper // Purpur + log.log( Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author" ); + log.log( Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring" ); + log.log( Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once" ); + log.log( Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes" ); +- log.log( Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues" ); ++ log.log( Level.SEVERE, "If you are unsure or still think this is a Purpur bug, please report this to https://github.com/PurpurMC/Purpur/issues" ); // Purpur + log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" ); +- log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() ); ++ log.log( Level.SEVERE, "Purpur version: " + Bukkit.getServer().getVersion() ); // Purpur + // + if ( net.minecraft.world.level.Level.lastPhysicsProblem != null ) + { +@@ -185,12 +185,12 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + // Paper end + } else + { +- log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH - " + Bukkit.getServer().getVersion() + " ---"); ++ log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PURPUR - THIS IS NOT A BUG OR A CRASH - " + Bukkit.getServer().getVersion() + " ---"); // Purpur + log.log(Level.SEVERE, "The server has not responded for " + (currentTime - lastTick) / 1000 + " seconds! Creating thread dump"); + } + // Paper end - Different message for short timeout + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper ++ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Purpur!):" ); // Paper // Purpur + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system + this.dumpTickingInfo(); // Paper - log detailed tick information + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); +@@ -206,7 +206,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + WatchdogThread.dumpThread( thread, log ); + } + } else { +- log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH ---"); ++ log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PURPUR - THIS IS NOT A BUG OR A CRASH ---"); // Purpur + } + + log.log( Level.SEVERE, "------------------------------" ); +diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png +index a7d785f60c884ee4ee487cc364402d66c3dc2ecc..518591dd83289e041a16e2c2e7d7e7640d4b2e1b 100644 +GIT binary patch +literal 9260 +zcmWk!Wmptl7+qlLS~|W3b}1=IK|*5b5@|#_l0AUP +zC6t~Ie$33hbMG7HJ?G9mbDv4n)lnlSVI~2AK;# +z4OMQt9~6LaN1#s_Fh>FQQNXigV2~Fm&;xWcfNA!dm*#+B)G7nz8bF6QFw6s>Er4EOK&%?bF$4;AfiW&% +zoCBC(2Ns!cigf^wAiz2NZqBnLz$Fx@R=#VuNdgNjK)EK+fB@F$fJ{?BsqoI$ED5Mo +z1rqE4Uti#zCSVl@%tLSX$$)HQ;Jq4~ho`CY^a7vP8(5UvMwUftX}AK<&Iwsa{VUUgUh +z2@c>vB_LZ0XlDV+Z-B?IZf;?2Q{C)P0>ZVx9Q3a7hXhz;0*3DaKX~ub_6P=EnF2ok +zcR7!90-gbPNmi@e3BV~F2=oSCMBV|pg>WmCTgLr-01Q8169d>q-)SS&0%(`GBd$p2 +zE<~Lo5bOy|!S8arrBXcyc!~mEJ_FLt@08Ob4a7eO9#jGH#Xy)lfU>@0405w;9)s13%z-g3FI09k4gZC@SEGh21MSdMw@%zE`WMpeH{Z3F$F%s4K+W4gOWCo +zM2#hp$Mq4nbl;~?VJ4luX8W7#1E`a&x!wY&zb88l +zN1p;u{w(({h5e>J1%Y6K8p;U6z`4mh7wrra#_v{h<9beb_Uu_ssLuklU5mIC>bM*t +zcnDp3!57GGcIWN~=GwhPk`|qj+4X29PCWJ%!mm8dv^)i!1KFLpM5tu4Zu)l4x1`+4 +zuool8`RpVod-L>kH&(y$T!#xcuSSC206r^ApC6SB-*ez^5f|E=>CVr`YArBlJ*aH= +zv9&F3YdaKfe*MZ~i5LpP{mi>QT}i><&#tzbf_0y?pPchedeE97X-j8))~zv`+-R^c +zXW+`~MJv5ye2+%Kvb|-$zve#6Fq3j>e(Z$inlS;)z#xljVJ?019I=wkGZKCb>mY`{ +zC8KIq#mXl@3vR}_b~1^fB_&=iJ$$n&3S*05tirE<8!l{ZZCzis{#|hXxvFTM`o}iR +zq}ASGAx!t@bKd6MjeH3iBEIB}%Yan>#e?6>$^PrcHJlA)z3Hwi9`j3t3g5m_#CTR2 +zJWL`g(S4CEDe2|vYOlPr-diIV^oy)GsZk29-%}jHck!wdB5f==uk13XY9^5>Drj(Jal(oLc`8 +z7D=J9cm#rVYK(uzdmUKaOiVbY*(WAOkmLNmxVRVy4%oluYjcE3ejCZtD>w+KsRMI; +z87X<1i$@!2V<|7#NM2W6ZEQ5}eehW7L)rSeirL`4wOgYhpON3G_`a-$K8jR7b1;WL +z!~?6_@enfHT1uN@893H{iZNi1|9834^3*)D`mGffWf%2a)wvAK&u>(jHANwD#zmPX +zb+%6usxsHvd2-@g4UX8e(%gfc=yCIMYT;LE&!w^Hm;hjMmbIls&Ko=!h)JjeBiK7j +zJg8+Wwf=llHxatzP37JYP2MYV!uiXB_G}g(g)9DJKdnb9^WJMK(TfEmW8+Fb{$ocI +zd_IJNIpT-3}l*&5tlx{G{r8LcDpgZGm&_LCN*L6GgC7m* +z9Ckq~9^@3|Q4>gQQNDOZ2HyVd9pW<)EoE)RyU2C+kH**{1?C-Xq|NDRXjCBSZu!dW +zkWoaF%%=W+tXxD?vrJiW1>QdCcU?IwSAAVKXSF08Ri&so^yCRhU8OJJ-7;U}%kOXh +z;uh`2naxh}@bu)!tqCu)Xf)+K3I8Q5)ZcBvHOp43NX8R{ud%yohLd|fFsJPE=-WnM +zy9)c2e5+K_=dT7ym;4dn(y2MkPN*C+c_fH{ZfAWh`@@h_TYs$2*8y!HC~F*!SDduQi((Yakpx9^)x +zJx6lr@C*IFi6C32_c%iv6B`G$;M6Q37L;6Uui3x9r{WSzT7DwjzcfM?D1LbD#A$qd +zu4>LOEHC&2!$+8)#A2;xEg(p|Jdy(=RRcM>kUoYG*Qz;+&oyU511wbY(v-jRqxsns +z%#|?EAim6T>_zCj2<-iPVp$G)<1~m`qSX7m3`(@)$3!@_Il;X3RXlkq+00DbtS7gY +z>#I3K0|TFc5y9cN<3t=oP_kgwm69oEvtnYz&nRnvO7Njr5W~StAj9NIY?n)l*H$Iz +z2YURl{gS{bBrJXar($8yfT4n#w=5%{AEV-@Grkiy0_J +z=#2;?ULHsk`lZB!Pq?X9XYS_xO{hASO}zKeLGkV?N1%z +z>{sH;{En>vxtZU%K&AU5%Gwj}B-+Pkl=AbytOz{;6ed;B5v9!jh)%X6>$P18-6BW0 +z9}ExHska~pvMsOcE?mMPyWj`s*pbbrIhx_ij%6Esl?wROHn#vuvN1k)+M*(8X8`A{ +zS4o(sjn)kE#;i6MtveA?5(&g98!Mcr!J5=}Qa@!L1e14aJEQmcLzXKl5nn2bAJ$tZtP$P8Z1 +zZ>}d+7jWYR^n<^KOqhO^N==PYrD)O9EEFNs=im5ICPNsHVP!Zx`PfqbpT-5=sn650 +zHSeafmr~){Zr!JcC1%$s2t+!}=}Ip+y3BYtETmoWiR}1P><_9ZZ0FT%gEZStrgdbp +zI0aw6jiD;PvU!g!GH_nZQDTX%5h%9pk4-fdm}0u~yzd;=Cni~sWY3r;O}XL;q&8-M +z47pHP%S*mY4oBx}PU^}#j%4iThs7lM7N=#@Fdn{XdiIW0L(=C2~Fu=G5&Vq%Yr +zY&qWfrH35E^L#BnOgMwRc8B)xMX+GlbUbtr`nPrR=jS(Tb=kQ6o7eVqUZd`0fo9D@ +z`vyo59#*A!saE#!$G7Cxfh7n>ZWX!0gkro``0W{b9=XxNGqu#u6^e-7Kk1lUvA@1| +zbiNNGZAv0UHvN5m{KO8QNS~$+u-B>s_5L`L`jBdrke`Xe{0N +zj*umKyN|_A>O3@@8y|M457h5tzNEqIDV$3|c%QOiW7;H(RNLM9W0najA-;HEwdD>u +zkW^fo^J0f?_u(^NWw(nT>JS~~smIbnC8QxFcuQkHNQUJyj;eYMTmvVN^O|RPDvczq +z>kslY@C$0YBmZR=Y@IEsCWJ9}K@HOu`c4n=$kr9!;n+9We=f8unlK5@c*o|I7(=zG +zS}lWvH_fp;i}4qt9>h_?QzN~ap@7nLh*7DJwfvLZAS*_S4BW3*FwwCv)lD1Vg1t7>@!DLp5Su@bc$d;D_VX! +z57eDlJm5IkuTyc>aw+Wt#V5hkX-B+t+|!Fa@@x7=`8`3dZm3$aHF*y2VcD$(D>J3y +z%YwpO#gY%mqyin8q9uH?7OBC}Uv-@)Hl0)Z2+y3x`{XluljPt{<4TU-m$XDvYU9HDGKSg&cT(@MVe23D +z<wCdy-3tTl}j4kzaCmrni9Diizn#a{@0 +zu;<>ZBofe$`#ML)Z*Aj8mbPS3-(VgSdF3LA8LJgBPj{3IaB~uLBuylKO*>ev*i|tuS(Wff3`Ogj^{GsE +zb-&NcY_)d|4#vv+AW}skSWrN{p$KOp?oug)BPoM*O}*h=wrr-PGYCt{qwP-&I!~hn +z&+*{EI5^09slk8i%kFcTjCcO6Oc}M9iiS}cuLLw0fyMPfjZ_M`;HWDHP^ke=f*5^! +z(0!2p!N9nZ8J+AP5sfunmbw64l@2)3@53_`Q@g&4FYKeDLbz2F-Pl@Er#INAtM()n +z<;MFT;osC}L2=Y4P1?cm)V{7nauSgh +zx4)k@#lLT}(uM?{Z&NvzT8pnJk@-MDvv4wOKsY+l9S8OdOw}cex#$t*B6ppAEloQy +z8u`9L`Ky$*$*G}A=)h6BjVn=_YuPie5epXe0vLKZP=Pxps%a&uGrdg4y#R{YobnDn +zWlMl5J;9|5ev`f`BO3Uh(yuK%@i^`+fAimq+_>*)z(;wrFdXn2nVJw1y_>PcV%p-) +zt#ZCU`}~DIE5y1BiI$qSd5XIebq(uRB-FnL#^x=bDHO-ls3({4R}-PgePOPH8ggGN +z^EAdT|N11qATWCZQ}ZY#=hum&;_xeyp*|O{VN?%jM$?&+DK=8LzLP4?U!YQ_JT0`M +z`m!W@TCB5mlr9&jJ{^cX4Ye=uf(7g!BDG1LQfZM#P5u-^$L1K~rOMk7oWI24W>ZJVoZ8!*NX>jC%2pgw@7$v*amGL8JnA`*TjN)y=JPix$ +zHom^xjfr~ZFvhO?xbJHg&jfEZgboIqN@pk*%G-#&cX5DM>_>dMo{P<(#6bOBlgEyG +zzi7pMrVsz<*TT+n%Xo*+rUVN +ztJqe<9EEg`YU)i$PQ+gn%oA5sG>b*6QUjZVf+Ju4QKWr32^B^>#t1pQ*dNT#SxuC8 +zGNJ9mH6R-92O+m#!DECXMrB|3+|cZ$?^qoag;w8-vr)Z5hx@2MVoazOwqdLo2-lu( +ziwF(a3SNH{+U_=d6KFp}-N^eInrfTZ{p%Yc;K{NXO^Sl0qy(%)%sUXL +zA6Ic}&$4}O+ulD-?|#%otX@aO$zD_RW;|WXeL`u5;iz`2Ja937zabkT*wwyB&2HIU +z%>tpNcvDWCsG=IjRr(V|Z7N`9^tphbhiC}fIrVcS>=>)uMStm;g&z-HK{fFz2ud&X +zKo;y87PhB3IZGZ&D%bJ2dZ<-Z7I$fCrPWl6O0ir})>f~+rM8k@!Q(EpWCD9f!k>me +zRhs@Q3_eZ!{s3QHUMaN0uShhn$y!N+)a +z6F#K#O21}#Wz2NGar1LUq3S|3=0~&VTjxVeqcysO1QIGwgglMcjXc3^9R9i556MW! +zA%G1+Y)mPX16RcVB#B?R~Xl +zIeR6G9EBEoU+Sk|=Q=4gQdT~j9ecG~G45m|E+IkhfuBxT`M%^wZkNkXpL0AjS!$%u +zg-={lr6QW@$p<#-f}T{y|0rAEpY~$9`Ffd=BP+`1#;?ge=@mLGkdmI~u?Dai2i+}> +zr-d}7M&xTHsK7@<3$tPd^Yg3fLzxDa!oOJ_N!hGt&$}5!9&TOIuzhIP>)^&CM1CIF +zY>A-293)fzq2lbB(1wKk=>#3=G1eTT&8(iBSJab=V@AGnDSwOhxKg{m8sa~TR||^x +zH?*-f-l|Zq;GkYEt~~O`56i(Z6gi`*^TxMZuc-f=NMlvry~%u2a>kz*@R^tJ@{7%-A@wfa)Z +z0~dL|Z*tTjEX7ED(M%2gZ|Zi_L4TWr%=BR=y4%;?-COo)8t(xaW)#f?Uhc9^d-1?K +z=UDW-cu8J#%~t1$>T7o{K5NXC7Z^ +z;*V>fuSp>3%L~xxUi%mG*w7fJS9FTtSX8!B_=C>_+{-q^3-7Yf_esz?&oN2)zkaPH +z=7a>>$VgQh6LDY?h_Px)L~(27=d%@0UX!M7G~G|aMRJuU!|xkIYM`l6jEi407=MtW +z4YA)qqP8SmCj;2g*%y+BObhJICRimEtC(yhX|B>fl9#O|OsP0h{YfJM(l{DjCSf;_ +zp3jZ#S5YfJ*b;u0&zd}UOl+UMYMCH3kOBnUGziK +zo__#J<(BObj|aew5%LI%9K>!}5UVhW*2b`jvkYQXN*iuj#{{Oatl}s!dyhU&jWEAS +zdKj4YrSTiJ_b6i{@5nk1r@W#B=YNdtri~y>StL>N%B6Gj6O_fwN*T-2>1Q1KuQAXE +zF|^na>CtL$iCMfa_^Bio0%XY3)>F9Uqzf-cky+Lb_H)#4=L+?00yMptx;^o9v}fkd +zWN%~1Sj%)#ggvPi=IpG67#q)qrgFi21qV^a=2vb1s|%}692RR2)zei^W-5JD7Y!4^ +zKfyt1dP(!slOA11Of$_QuYE4W{Uu+DO027{)!7+#Y;Ih{M{`=>H7?QlKD!&7BO7X`Q1_b071zS3lNp +z@&Iv~QHe@3e;q{$b7pE51fo%ee_qC4)CR?kSh?lvkF}D2UuE>>^dhlmktVcN14leO +zB6JLU^U}S?*F=`1q~sTqli@HC^RzgoXWg7-*R1R~2xL-K^`XjV%3c-JMf~;^ILFoK +z%NIwJAU)}8T@nI{XRp;rWaD7cuvC1dj!R2oZ(K-<6)PFEe1C`3{NVSlqPF<~-&+*O +z8>_z7E5JY^sY(1!jgGp<)OE9!*nx9!RX3U}7tvu5m2XXS(V8#x+t;nz?=xkI(DDsz +z;3foX=tMqziX%?kztoj_MYP1$@9}OUi0@Z>26`$9-KBzz0d+H_Z`PU9?gSA=7?H}0 +z{c+|*bet;11)bfWS<)JER^z_5PKJN$@9unBRX+W*oX^32ly +zKo%s>e!Q4Gb1DeiV*R&fzDz|t8*WBSo1ove3somHjybczT^qp^D^Tpx97n^9WuuqI +zTCFUlzMc<9(@Ng!L9ebpnk>0!&~jV#PR~Lz8p-9KECK6 +z;70`vR#D6@#_MoD+$Wl*AAbY|FVo2J6vOlP={WYAqT#$Q!S^VW9|! +zl45qcN$prx36zxggrb_z0Ri=i%$I(SJ6jHx(uR<;b(=zb>GDa-cZTt=EWQ$^+AKKp +z!qtgInkt}{k=PN-erHn(dHX?T{g44@;}a%oYTE7q*K<=mU`H~sCkX#6pyH?M+0W>N +zUr<(Whv)VbXit9iJY4V&;_6xGNVK9d_P*yRzW_7PgbR$4WPdDP8K}{V;@u;E04A#lw1fxY9qPYX%78 +z?2?m&UC)5IREFbfd%r<*8}KZbL-)2J(s!Ni>DER#ah4jLH0*2GPn +zFP?pTI`d2+=5;~B0pYU=P03Hk<$aGh?7&|CDV>N4L&S*{xjJ6{dn!jPj@;!U64ZH$ +zxcv;Jen{NFvhysj-qs<-RN>Q}{r7$Q(|cMV#FVvG1ygSsC^0)`=DwB(4Z7$pIxu3h +zGr}-!!|aUp$DUaVeC9=coIL@I)g|Fm7a7u6JRy|q_3SIEsEkSTn?R2}SIm-}g0D#x +zbFUgC%_08XTvJ@!3#F5}f?b~Rz6CpeASaHdWnNs?a*HD&&M#AO;^>?RDV?gRN(Q&_ +zi^f)H(H|$Fg#yiJBfM*7wTkz_6un^+@bW^qdO0rV +z#80Zot!buo$fS$UtrjI&1`2u(g>;~*Z@I;ZrS@=@)pCyAT-4)ZtWEg^;2QgIBuYCv +zCOT@SpdJ=?l}vNgolDevUl8e7VBri&69qGAFHzdn>vz;Z2PIH&X0IdJxq5&G|lD{8;FN) +zyoId^NoH`da-U0H$$EV_2u9QwI2NQ6xr#xsr4!7>PZTS-+^Ser28(?H>pkyr(t|p)Hl*Rcc?7hPzry4)mHP +zWLX9>Jcr0gs2&(aNg9b2@BIl*r~2X;kn=eI%P577nIX=auf8fXGcC+->CfU?wHoJM +z@{%ht0!KC%-tamvUWKvNx!08PvLF(w-EK-u?Lgd+WfX$LOYI=5t00MKD|Q)#@9mL5 +zWZQ?eQkgCCqwANoAm&K4|fw*k7ipV +zB1v2pR!seE-oV-2@pAb2ne;%k;&0+LB7z16@)VH1MO9)MHU9kSp)dCNVB91j?4mmO +zv3NP2%#IeX=+W7Ni#8IjoTpc(!3T*dt7!EX8VlAzQq6uvFcs%W{$71OuAvW8Wpseg+*PVYLv?WMN=C|H@$;E_S5fVp$L;GyupU7jZ$kfvC58X?uLqEZijH!v +HqBZh=dic2S + +literal 14310 +zcmXY21yoy2uugDycPmm{N^yd_Q`}vP7YOd|PH`>8wLo!q*HRn`6f5rV?*HD)IX5{c +zxpy-=`|Zxo_svGBD$Agwkf4A-AaprdNp;|J21vifLD%hMrT$lZeEex|7Xe0mqFAZArC{!qp)zJmbab@utqA`6 +z61Z~|e!k$IbXNT?PvGuuzT7G514$8e!}lsR>%nURMm+~pde``@(!O=ISt0%B93;Ez +za-qRi4n0Q>zQ2#2^_y08QOl3jT*!Ir5@<8VrFx(6f9sP|H8ttjftN;wrX>jP4BcG1;MfU5x^L`zc09u!bDBt#+ll=7@ +zB;}A$BKgu}V?#qfHvm`~pt%wG2y{MOc%B!8I`p|pc +zO#?sq!Zd&j8UPmvY4RQnfo>!6{a}GFV!}g@qu<3Wu$07X(O`vikNW$~q!ngF23Ls2 +z53p8js<-B_Qd?xX6rtq43Mdz(jOg2QXx#Wng_9^1^^~KqFNq{Kvb@Ap9}bf&xFA-C +z5+#cQ`#v$A=kd0O=agATcleBaxXf_(dnqbQz|cL9R&&Ni1omTs+6~YApmk)MCghxj +z1}mq&IU>1nEiF=q=PI`%jQbyRd=hVI83Sm{E-4uTc#w;NNwEW)C(C`xvWzY_%`_MmO +zD&g-sEaE)}6(&g)y-N&rNy;5@+{M`}!{60Y8wMgF5;HmO#B~hG`W$;7xLG*yF((rq +zxP6I#r#o`B3FppK{v(q1!C+YLFSfySDcHyoW!}EfzuCB1B|C5+oP}dtocnwkcNy1EZ6#5JX4=ePl&cu~0tMnt&79+I4%PaK>VqFx;r!QdNmnxlEqdU-QR%Nmu{aWP +zJxwXvt5fFTCOVgB)Zq +z%H0U=9q7Y0lu&1kc4zYT3*lHA@XJfoK>3WFM&WWf2u6^+wCm8##D$x@Gkw+t^HoO( +z4pxDRqg;$5S=t^k22H5^V3V0Qfy%Ogl8I%LD$52=7)J>Ki9Ej1HyEi_ujELlz8$-+?cdD1Zxi02kW0 +zaY=caFq4~s^R?zxcc3Z0X|az}Aww<{P$>6rk+5Di5J7$kWor0{Q&>+DWSBH^Gf`SP +zT{4}IOFh-hB7xwBdewq%de)q6QvxorV(()2>@j8i!kj)=^hN +zl_N{$9xTHHA;V&Zx#tX&1pOO;v^NiOP#_UK@J;;lp+OOhOOO2mlMdxM;Qv-mWG+^vzox|8t`w| +z=gPlM3)y6G*hfV1WwuMe>bO-vP9g`h5BqgO9x{ROBD;aPl>XDmvt(3PUxt|4RFRpK +z5OEtRz{(Oa_W_!Z4XHf#h;Z-~71XM7wlF*L!-#h_Uy2tGuy-rAZ)4{qE~feNkp}qf +zgvBtLkFPI~I7%C=OHZfPZz$j>L9)rb;l +z@J^dxncy52;wmHg=wC3|Xn6jPYCR7xc}~D0wNjoYxmoRh_zh=6@8coM1UQIa_z*1)cZPw4v40qoZQp-uy#DLv=oP +zX9b3vzFA2r8}|_AO8W1(OMG__0{1AUD&Z%&7-(>s+Z-X6Sv}G5QguIbZ3mYa--?09 +z;wNw?n=yAag4%m#w$$-YZ{(ZJUcwHfzu&!gykNjG)e}!=q8xy2_KS=ULsQwv45NK! +zVqqD8#S{vRjg4(Q6HM_F&tihNIQns<%DVjE$cv33ET>Dvc^#{z&#u&&9RgXO?ZLuebczKv#;! +zCS|2lIa37Bp#3RWj0$V3=I2>o40{(J^LD|EUH?!2;Z&HS*>7*V%{v1)wHaUP85mcX +z%q!K}Ntr*IzJD%++btJ;VQO*OjJL1t{GvR3cy@OC-~pe^bV?N`z0QKCr?Tom)4u%A +z3mi2k&eIgh0^rGI#Di+&3lrsy-r+}zwBkDQtswtPbkj!Y^l`{f!# +zLseC0M;DiifDa!({-G4{W$Wxsgv*(NX%HMyXhArVwY105dUHg?+=@6Sy8n@slS76x +zU7%PI8ToKm#qahfR;7kn#|t@9y(0EkooWBDqA1(mpO)>BBz))giBi8xVHlj#dR9U8 +zRo%`iBdlj8%_tRn^qa%T>{nsLLwTNld&WHLyfbPzv2W62m6q=Nsdxnk +z#{P==5!Lidx3bcr_qlUl%BX!xjywA?jv>FU^mJDa0zQT9Kw8RRHq>7B +zb~DXw0(oqBrOQunsm2ghWV2i1VmN{F?)U;0%*j{FEUxazAJ3)KSWomuhklkDi?5h*MTLDS5ma_Nk1sNZYzZ#$maGRyiXBzjG@(G__fuyBl(^A>s&{jF+J%5| +zv#7nD1XK806#_U_4#N2ANAxznk%;U$Y$z#{K*O07mADqx6LjACqwP<`HFV#C6Q*wx +z8JVP_qGF}V7B?^8)f*2F5AON7v$L~Kr?2}oPai_kG!_6MI(U`LS~+Mo*CSyrw>pPE +zllqxy +z^&rnDn4XA@AUY7~`1lwTCrm8KlVRqX&!kZFH&;i9@=R}UDxNSh*)Iq2U+#9}@ag1t +z%KUOEw0DXT)>hQoLTprY^z=BC=8NAyi3pZWT7A`?;rI<3%65Nqb93%pJ=!+dNtB>W +z7f3O-e-S7ZBgBntcyt~wOG_p$AU2zlGH8=%TEm+z8kLYReEMTkIo#2YiA=iKWrH); +zS%uT3xAyyY=!U)0Evpgx{{38MPR2nN<3913M<0O#YCO=TSt^4IzV3^D%2zC>t_OO} +z_h~AVOk+IIi$Ov;-g93a4j@WaekCC#HFm2_Vu9s)8-GbYtr{LgrxnSIN^PW9)!jYX +z?%-yssA~&R3F)C)wj5i|@!atCx?Qy%P1QEGSZm;iUNai`-F(8a%y+_a>CMzx$XEKx +z>sW|JbN36s+Y{4SZsrspH%UH=+Q6J`c&_-JLGL&5|$XUA1vFOC+rgoc&xT{dFT&pMaEBKwyD;plX0>2nla;jTlQ{!fn2M=Ak*=K*g% +zBm0-$ly1~}CT-5gv){jex9)7&b8u!a+vYHXU>=NF2>g3+_rN{(LUMGwRWKk49sS$v +zazyX8zZ1hwZ|U*5{fK@i@hRl*U%Q2cg+!iIfb)6W%S5F{91qinEZE%~4Gl>rBw9S< +zMP5$exl1jESyt}d~jo?hf`z^32b!}UGtJH+w9(0UrI#~Ei*ii&6z(AVE?(}k_A +zE9Z@mj7HF-ch46I0ipe3gapRj{=zk_J1E^b_JwdrhKi4ytBuwP)m>e$@9v`A{1N{h +zwUN6H=_W+h(a?rGaQ%%LP5C4)XiZ*`1uUwgqWvk`LyDD!Ps#Q5oI($KDJ%8n5kBi- +zghsLx`~mf<>WT)6-cJBbp|htk1NfkZ@e#B4@l?UH7!MDMpO?1NETGk_Eg{z!N3!D< +zWg8gtgS%b(0Bg7dw9u35xq)1vNdnM8iu7Eje*u?#sZ~%^q*HDaZC?5z4ZzhSA%ndS +z4&$M&7(|(9nWY%QShCnuN0 +z`n9&UeypypUgx;R+x;XM#8uDM{p`9~j<49)^dotHJVO*A@HL&g7F={FP#trj@{dzm +zeQUiqRWJ&pkKkA1O-|vOf8O1UQ$$0lIExffio|}F@ROV#MXcPH$ +z?$$kxAF@B#KT}u;R@SVyIO>1sw1!i?C(_013w9@?8$bKaLQi34zC$g*^}F&(%NEO6 +zQzD-^6}HQMnGJ{h$J*)HjSxjblWegsW&rLC8Ov_r_20jLjUS$Ptnm|p9fK%r0j+4; +z57^mjL&lISh8>DC;eB$B69$h4XxE3qU4T&zUpDeV@4g>or%D-x@qhie>6mqD959ck74(h?S0BA0}YQ18d?hr6}%}y{%ZNJ^-(?=Op~; +z#2-UNh)jH9>RXmvPJ(Y!8(uhyW|sFpyvv)AaNeljHj^Fx+RC +z!`@c->W1C^FUKHmG2w_atkdsMnzY+l!CV8havQ8-Gu)<8t{#V*2Pwp4h?ayXsi5Z> +zo!guta>TA~iv#iJpQkN>#)QF%As@2WgU&V_Y^qm#E*O}M_ijJfFWq}ts)-l4>D)kCqJJ@MG2$69ph0jzwI8ry1u8D@CyinC$oT?7S*Z}Eg +zYs}PWLqr4u@)w}#!{cMx;KxO6W2H6~3k$laJjAt+C{0mmCRnfs=OJYbh}HMh&e`#> +zj;jrpjqKCh41OK{FOS`@_sPP$iCm46G^EMNk8(l-1f>!gEV+4vMVRZ#8infUenP+k +zL^tBOHF^=)k&U-Tw{gfijqQ&^ +z-RHHII5yp}2|o8pTsf6x7$teW9Em!~iy2DN?D@|U)g%I6VG%JBO$|~;c~1Q^3|x`1 +z6HRbq1#~Ke)wWpALcc&@P;m+*sGavR0{aOx3=IwUE3YPWAwV45pzD$~02inxi7(6X +z$zk683M=_r#M*+6fQ)&FK0y|lm7JLwS)K=t&ZJk!U_-y%_o@fhr{s37MUEQOF*M)3 +zB$;4>Zx;Xk*(hwFjb>1iJ1f*D#nyWL{=>{2|9*^vCNN!%bF8Oe<`xz#s;jFz?;I}4M3lL;!fy_;J-E96Of+;sG%K=fZdR)99pJ}fM( +zq%(s8UrsEL{NrdF`!#RY+VjFyPpE_vtqPMM!MQ+QnE)+_g9Z^{4^;k&Sa^=w*yuxB_*Z!U%!3{_9Qr)Jfz4IeS#io4oj_Kqhq`HCUub|Ke!v$1-$v=kc+O#rlCej?%dhY +zxxKUTsFPG1nfoFp3%7@gh9S?vM0N27#*fpJyaX;Vy{!pt*}!9_mX9uC#J5RyjknW2Dm3dCvZYU +zSW?0kvI9!o2un}*%`AYhr^CQT1aZF=-Nt^atn@Kt%b2!hT(pK!|MclbBv3-<+6{>_ +z8toMfWc9rpOk(8|KW>Z-k>Fr(xc_+q9ocf`8!_n}XYUrW?Ax|*_|=5m*4F0V+46wJ +z1IGS^Z5t=0Zj86J2MfJc +zUq#WKCfhoB<;P2&&`*_G4^_0uqDR20m!>T8ay_rxSzA&9_v5##g6tzXTkx+KRfz32 +z9vvpp?+YxHTxDthCBu7)&Q052y4s9*$M4_2w-OdPyK?F-EBoUuSsIk@@(!gA*A_!0 +z2eu1y;-Q$Ut(M>8FCOtw?vZR-%*ly^x)<95vK@P0tJoZws@+M*NGhg_NU`!}DZnWBHQz%*@6))$BWN;EM0xAF+B4Mph#S??J?K+&viwPmes*n^HGDL9iBf +zCk|mDu46wwughN!isu&G((DO>Ws`(VLY?^#w=RONxUgFGby--Y=5NJ|(>qXOS`;lZhmXyMEyBdVM@jJh71E-})~`?t4w8^Kwy) +z<+KACjs!F^TS-;FT24_iWF+=l(nR}j7U#;Vd +z)IT3=b&}A}1PUKFa6DKfgHkJci!~7u?a%k9h7Rri^{y`|;;xNDoQbV}+oJ=LdApL}|77o@C= +z;~aed)XpbrMtt1x3gHPWxbliQH4nKBCew{9 +z*-_PTyn~`1VrwKcc4ZrhI^!MsZ{D0O0%O2!SHHi^Dfyr9*x*DGFKwc()b;q6nM*M7 +zvA$x_?$BMJJHN5HIn9Ps{_7-sn79~BZegaa5V;s(BA<5BnU?^AeJHXtd)cIj_UCjA +zW|N@MjV~vrJz{sE0Dzv}tXxUDQAXm)1(kX7C_ZVFX%!TlZ850i(P1A0BxaJu)#LcH +zoxMFRzxoxw$bM=B6gpuMD#vcsa^00?%=D+T9-dQqV*=zD|)W!3BLun2&^n)~$ +z2_^{i9~sGXOAsF_S=k&4mWJ@`mD+G%MiPTlhuomboeFNwHb(< +zVpVR!mwf;JmpO3JL|B%L-!;@7TG}+`HZA;-{VIlQGY|T=f|!9!S=!c?sq5|KeEQ*~ +zm!1xeZcJPbSsfjU9e>K|=Ni<+YgrIG!|5@|Z>4bjx+`1j^O-{QK8XARf +zUG$nLRiTEtt;)9F30rvw>nj)@vCF{$d7>o2n>}~Y2^^C79l@s`uXRZOcuy>^%2@t- +zRGv={pKlDXFUgvG_^DWGR==il1rIzn{$p4r(FVOQxZi!_*Ksfl2hR{Aj>01RbFAM= +zpr0wzMwlOwlkt4|JLK)$>VL+{4nv>^`yMa)T;(9f*B(9;{T+)_=M4dN>M&&hS-#(G +z)-sW(WxVkHR)`x#g)25Lu7qnN;~Q-bvKDZ=;^fyLy@okDpvt&ZU{!U)WVtmnp +zAN-CzM{jPFWep9NAKDDq@=kynkGi_GQ@Z2y_Wn)xc_q3-&+9`qdGy_{PF-2c^$)%x +zd0sonEJhtG*2|P*Q-f_3`Akk96HzBz2 +z!5tnJaCcA2hGQrSw*{F)epvfYX?7toP=O0dN +zizY2w`>O@4Vqff!dBhQ^><#TjMP}loM9ProiD-Og@$V=*zQ|Avg0D!+96lr^u(1fl +z3J52PHoJYDdvdiIW?q?JIC*r?88VruLx#bp0lys39v$(c6uC*j}2IFFh +zViOX|K+DH18cd9%Rgjs$*sXuoW<>p^Fv-7CV|zpgTUnj812pyyX-nhA4TZ^UyYY9; +z?}BOarTT1q;0xSTjV_DPWE11?Y2+wSA*ybzebDoy8JwhznKa6SvYxE$WswX7Z6pG$ +zsA2GgHFFL3^zA@XTYK{a+6$Q8di%@1-|q9U15y+~R-L7Kwx8*xr(FP{g*JDPa`e((jSl#~?Rx=3ne(nLfeP9k0grubJK +zU4euzZqt~$Cl%k^{-!e6YQZi|D3#+MUS}VsYZ)0S>y@)kyqRI?A_esvAu-{`1Uq@! +zC+b`wnMK&<_mitl+k@e*$*{&S>vayX*>D>Q5sw2FZ?l(8ff%(8lo<^mBMrwQXOXe+ +z*7sZdWzBTIwZO$y^F)qZL1XbOMY<@M_a56y{({Vg@YN<_y}toq41V%~w=+4ZQvg)X +zVw~l$z-sId^nKU%dlk7W(mG}eS&KV2BdYqNJnX-p=YrG&&`_m0fzA_|iKD${5?oL* +zdS$heR@%Q+(3!!T&k;tIN|v2j=UI))rgkvyC7MTTrKP3g>Fma@_R0`GE5(tL%sS$7 +zG41ag%(Y(xZ5cjlk=R~(3XC+$25r*Fo=G5OhGgR}i!nDoG?^sult?Eo*x$x6CH-3L@LtZ0dfq!Bbbw-S}RwlN%lpH8c=4l2qH +z1wRszHSPh~=esnWvXD8B{D4<}?}6cA+@Ob1760Is6`g!zl@WL(L&={LA}SxAt0>Tw +z%b7i^&yNKM;(vGcNwuxAK{g|S3Y1&pH_6U1G +z3M4zx5FU=O;=l_?VzQ-~bx~xN1axPgYI0am3d25BjYmfSTX7Q}==Vcryl6@Se0(Jv +zxKW_o%H`jdnC7QXlkFbCsACHN1Dx=0gf<~@PW-&<=`1Hd)@#ypH7%OpalDj-P=ts+3^~yWs~TV}BD20HjkW6zc1L +z0#HzMkn3JV%7N-18_@tgE82*YnmEzxirriDSx#_|<|q1vL{k}7>^mRzO(ueTSN2~H +zG}kxp)Qn!&)><3|e>62+GXSpQKcemfqU!&BHZ5Ca;DT<63bBM&uV1BDS?MM$M;x8w>gShAPMxJM^BbMZn}Unm{OC9^4x3%% +zlmX8!km-u$N4fQXQ>jRe`7)3+RFGjhz +z18zf(Fo2<>YV^7LJO^UTZ2Ivd#mpN}o?7pBV&q=f%ID>haV7M8R3jsF*@a%iwIy>| +zsZ!-y{!%&j7`B?W8TcF4NH-RHH1xZ{;7BsA<#APu!;cND)te)FhoXz$BIU}2&^7WP +zT}TX>ZO58$VNPuh6JV7~s(W$vAj`^%AtUamex3YdVl3~4+pqk?G)qUibNMrj0*M25 +zY>5Ac|Dnv6xBQmV#$3JA?&HTN(lYl~J}@$l{*TY^kORrCB)3dDO}^^v!dcLf^CHty +zanjllIQeSLmpuG+h&ae`r*v!C*0A&W^a&q>93?BAXzG7n +z2*3TGPIcN`-_hY9&oaiv#fiv~>}7`T`4=pInEqWX*3e8+yPm^9h-tr&ts55$l+388 +zW)~F}2JH!}VLbQ>?6~H@&k`MnSsTeVj0TRVP4jGbP*!!CwM6`Z11c)yI2w$+R0zxo +zT|obYS1&&`{>>Z9(jnVU&=yI*%PGe*f78ie*_9oap?sd7fx7{r^WT>=XHF +zl`f{=UJEn2?tRw`Fem?eRE6#*nOes(ebRcmaK3~a3{a3EyE1zXSF0p7I_iDJ&%;3V +zU;AS}e?*mH#Yh2P9E3QBigIqu2iXf=@t)2+I~f*_E^JtEP1@IR{CBfTj%T}E3e#n% +zUa{@vU?D$l4DEANwkkK@ruP4ta)E*e^KLGg%$PizyPmHvKNMWtuJQ6sPXY=(1m#>W +z7V?9E!Vj}>a|KfQx5ESpH+q6$@gAp-P#~lbz`aj1_?xinN>3o8b2-Z3w>UZ3QZ}W0 +zWg-!>p>AADDcU^4;0*L4UFgB0QLlXd^y1E&4>txV!T|!`RwjZGl`;-4ZgFf>luHIy +zZ8d8Rh{I3r!g-ht6mAZxMB6VxRqnA0UY`h|mJZy2 +z17BazT$jMKFL3J6Ue_HL1^)4s%$Jj~Qx~1HG#tS@kwL(KP_ZI3dWz0SH(sqj#-*TNGsIWqPj>cj?!GyWvfdEiNOu4$>MIqL=F&Cc0{g*~L5 +zA1wt)=_zMFUkCT5$l!G{1-Y9QtGQ#qm5E(3fYPms_EP*sSVI)bfXN|uNO`BqVuCvd +zv)z8IGRgtM1<_trndVhQ^xA)wn~*W~#d*X@E=W)jcQWI8+?kdzHe;DZ`%+JE%gE}m +z6H=FO8rJxM{N90S=Gi!Mel)TyanxPa;E}C?hJl@e9UWad->;S|v;axgFjrY$z3(rV{MiJ}3M)t;Q?P5wZy0e3G{dcDO7n}3slDXLMrB$;#*W@Qv)D$=?Xs$F(8eTcyGIQ~IWgD%Gn&E>F9y#o>cR-7spE;Rur<_E~Pu)e0I +z#&y1|@8D~8c55<|KMf;&x;hg!A%VOZ38_+uk`jH4#=b9M&xcpxV-7cMN{jXVRnKSe +zlKJJ%=VBV{$DNeI1QkiA;DfdVT?$;O#22z6v6bTK9)fjrfIh!Hq__l~KzuNqT{&kA +zKs@YV6^1ZLGjTgR%(=NHS-DvWnnP)NM#qbHINqmQdCE5??co$3nuikqgm=s7*#Kd*+j_weKrZjMeLeHEoiJm>zuDRU` +zh~ggr^knneWU!Nn}AQt=0Id6Hk; +z4bJqse|V$H`stT?NS0yreYvaZ9YF!fw+N}{3#yXRU!C7?exl35BDC%+!jDMGT^DN# +zN9FGd#5t#;$h}5UgQ?q-Gr15>C6=nLUszle9<+_!!oi_m@_L^-R>_Qty7_g|C%m|5 +z-7^5X5V_ARi?h9_LW%2vByD3X_IvUktqBv{%SYXO1&;e&O#Ll_cfC`Wv1u+l_#RI< +zQ5Kly0;P`%TXaQN(heOg~>V&L{d+ZDA%eq-UKo#1)$rkjSm=nzAE2r +z5--RyKhxfXoGVU3^ab{5XGlyL1+26foG)4HZvN +zG@&I3h0fnK5lIjcrg*XxPy1(gK3_TN`&VYnxP;C|j$~0rT$0f|*#=OzM^NbE-1T5D +z%Csnt)n!sx3N#b(8G&+G3W~Q_B#StA6jZZ=p#wuu`DrAMXm{T@#S;ku4Dme@{Njmk +zCtrh3z6O>o)~o{&Htx+6kn*)$NNBH-biu^aYtWUq +z(G>4rCEKr#tO>!x8A@%W@6g)Xs%2Hq!y#Mbb@9R2@GDWi&!{jhZvzQ1D9nMuPoOS+ +z+cj{9nx5X{jJOIavbFf)Kz5Jnbe5Bu#(XE-z$j&iaP%c9W59OoT0~|N#D*(N2kz={ +zs(|)nH!_+_g1)#ZH2xk>ZTG#6WN#qa3BxZM{NWxq`*#$H255k6Ky?hw*hSA6`c_fl +zT@Ua%E5Ez3;~`kQFmrC#$Nlvc_Uy3#yzhd-6UYuuIwgIBZZC-`dwOBJbfurL(FfhH +z{YkjE+9OrOveY`{t{sGw&51YO1@{iO4)Ki=!Z5#q=m_Hi)_j0`>?;t2j);vv%BUif +z;wpTZdLQLsGvZ()DCdxYudn^Pt;BZ}Rin$4F8h{R`HxT2z`uc&aMXIQOvwgA5%{&) +zFW52MiN!$!EXgx}Px~e1!EMp;#&kY65oDho95j~!qD%YJr`+aK4jCJ4UJ^;q>w@Lf +zvDfg|M`S^@DGxu+7aR3Cx#;%?advj&1~L-m +zJqCP9&TW3migV*`Z$#)Qa>3>Jf)g9D6Ki28P@iX(uso)hic8Dp1F< +zeF;(n8Po8A*~^T{De(J)Z2nqLl@Vv3yoSlGwq0aeOg4ymI(KIkTeur-=J-yp9z?qe)it6gq-wl@I +z0D-_I{|T<5kwD9uH3yf1GWXp5*8eOgJf*q0IRoK|+r{}Fug&0WpNDKMTC@(Xc)9K8 +zy`lByMn!1fnY)1KYP(0Je1)c~WilUuh<&Q8^OE?L9Q^xK*Y@M$`6D6TDCZ^@l8{|} +zxmmNw)mng$hYBii+&ZqedxWT0dnV#LG4zC%+kzcK+-??vEHT>Q-T8zu|s_1IbA#OV)^+1pg1OmmZn` + +diff --git a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java +index b2d510459bcf90a3611f3d91dae4ccc3d29b4079..7a052f6deaa30f8a177a2aaf172f9da6c308a22b 100644 +--- a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java ++++ b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java +@@ -37,7 +37,7 @@ public class VanillaMobGoalTest { + } + + List> classes; +- try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft").scan()) { ++ try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft", "org.purpurmc.purpur.entity.ai").scan()) { // Purpur + classes = scanResult.getSubclasses(net.minecraft.world.entity.ai.goal.Goal.class.getName()).loadClasses(); + } + +diff --git a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +index 8665e2740aedcc2895b0e2c44ebaba53d2a40568..918b5a8f40e489e4d9d6406161878d6277c9ebc9 100644 +--- a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java ++++ b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +@@ -45,6 +45,7 @@ public class MinecraftCommandPermissionsTest extends AbstractTestingBase { + Set foundPerms = new HashSet<>(); + for (CommandNode child : root.getChildren()) { + final String vanillaPerm = VanillaCommandWrapper.getPermission(child); ++ if (TO_SKIP.contains(vanillaPerm)) continue; // Purpur + if (!perms.contains(vanillaPerm)) { + missing.add("Missing permission for " + child.getName() + " (" + vanillaPerm + ") command"); + } else { +@@ -57,6 +58,25 @@ public class MinecraftCommandPermissionsTest extends AbstractTestingBase { + } + + private static final List TO_SKIP = List.of( ++ // Purpur start ++ "bukkit.command.compass", ++ "bukkit.command.credits", ++ "bukkit.command.demo", ++ "bukkit.command.ping", ++ "bukkit.command.ram", ++ "bukkit.command.rambar", ++ "bukkit.command.tpsbar", ++ "bukkit.command.uptime", ++ "minecraft.command.debug", ++ "minecraft.command.gamemode.adventure", ++ "minecraft.command.gamemode.adventure.other", ++ "minecraft.command.gamemode.creative", ++ "minecraft.command.gamemode.creative.other", ++ "minecraft.command.gamemode.spectator", ++ "minecraft.command.gamemode.spectator.other", ++ "minecraft.command.gamemode.survival", ++ "minecraft.command.gamemode.survival.other", ++ // Purpur end + "minecraft.command.selector" + ); + +diff --git a/src/test/java/org/bukkit/potion/PotionTest.java b/src/test/java/org/bukkit/potion/PotionTest.java +index 83226ec2fa977819e12a499eb3765232543c17b3..a742774dabaee0629f4e6adabee5f3ec4b3be41c 100644 +--- a/src/test/java/org/bukkit/potion/PotionTest.java ++++ b/src/test/java/org/bukkit/potion/PotionTest.java +@@ -9,6 +9,7 @@ import net.minecraft.resources.ResourceLocation; + import net.minecraft.world.effect.MobEffect; + import net.minecraft.world.effect.MobEffectInstance; + import net.minecraft.world.item.alchemy.Potion; ++import org.bukkit.NamespacedKey; + import org.bukkit.support.AbstractTestingBase; + import org.junit.Test; + +@@ -47,4 +48,27 @@ public class PotionTest extends AbstractTestingBase { + assertEquals("Same type not returned by name " + key, bukkit, byName); + } + } ++ ++ // Purpur start ++ @Test ++ public void testNamespacedKey() { ++ NamespacedKey key = new NamespacedKey("testnamespace", "testkey"); ++ PotionEffect namedSpacedEffect = new PotionEffect(PotionEffectType.DOLPHINS_GRACE, 20, 0, true, true, true, key); ++ assertNotNull(namedSpacedEffect.getKey()); ++ assertTrue(namedSpacedEffect.hasKey()); ++ assertFalse(namedSpacedEffect.withKey(null).hasKey()); ++ ++ PotionEffect effect = new PotionEffect(PotionEffectType.DOLPHINS_GRACE, 20, 0, true, true, true); ++ assertNull(effect.getKey()); ++ assertFalse(effect.hasKey()); ++ assertTrue(namedSpacedEffect.withKey(key).hasKey()); ++ ++ Map s1 = namedSpacedEffect.serialize(); ++ Map s2 = effect.serialize(); ++ assertTrue(s1.containsKey("namespacedKey")); ++ assertFalse(s2.containsKey("namespacedKey")); ++ assertNotNull(new PotionEffect(s1).getKey()); ++ assertNull(new PotionEffect(s2).getKey()); ++ } ++ // Purpur end + } diff --git a/patches/server/0003-Rebrand.patch b/patches/server/0003-Rebrand.patch new file mode 100644 index 0000000..25e9714 --- /dev/null +++ b/patches/server/0003-Rebrand.patch @@ -0,0 +1,376 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Wed, 21 Dec 2022 19:31:24 +0900 +Subject: [PATCH] Rebrand + + +diff --git a/build.gradle.kts b/build.gradle.kts +index b6fde7181a58037a2e2a6fd23daac9766127fc0e..4a75fb8eb264cc34bb9fadfaa1b0cd62eef9d247 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -7,7 +7,7 @@ plugins { + } + + dependencies { +- implementation(project(":purpur-api")) // Purpur ++ implementation(project(":plazma-api")) // Pufferfish // Purpur // Plazma + // Pufferfish start + implementation("io.papermc.paper:paper-mojangapi:1.19.3-R0.1-SNAPSHOT") { + exclude("io.papermc.paper", "paper-api") +@@ -84,7 +84,7 @@ tasks.jar { + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", + "Implementation-Title" to "CraftBukkit", +- "Implementation-Version" to "git-Purpur-$implementationVersion", // Purpur ++ "Implementation-Version" to "git-Plazma-$implementationVersion", // Pufferfish // Purpur // Plazma + "Implementation-Vendor" to date, // Paper + "Specification-Title" to "Bukkit", + "Specification-Version" to project.version, +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 9713263c3bd34ab8a3bfc0a8797ba0b1b88ed733..9d9c9bcbd9bd86915e084f2438eae2a732eadd33 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Purpur", serverUUID, logFailedRequests, Bukkit.getLogger()); // Purpur ++ Metrics metrics = new Metrics("Plazma", serverUUID, logFailedRequests, Bukkit.getLogger()); // Purpur // Plazma + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -603,7 +603,7 @@ public class Metrics { + + metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); + metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() ? "bungee" : "offline"))); // Purpur +- metrics.addCustomChart(new Metrics.SimplePie("purpur_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur ++ metrics.addCustomChart(new Metrics.SimplePie("plazma_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur // Plazma + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +index fba5dbdb7bcbb55400ef18342c9b54612972a718..abd9b2bb9b2079b1eef20e3b1ab1c5c24cc91e65 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -20,7 +20,7 @@ import java.util.stream.StreamSupport; + public class PaperVersionFetcher implements VersionFetcher { + private static final java.util.regex.Pattern VER_PATTERN = java.util.regex.Pattern.compile("^([0-9\\.]*)\\-.*R"); // R is an anchor, will always give '-R' at end + // Purpur start +- private static final String DOWNLOAD_PAGE = "https://purpurmc.org/downloads"; ++ private static final String DOWNLOAD_PAGE = "https://github.com/PlazmaMC/Plazma/releases"; // Plazma + private static int distance = -2; public int distance() { return distance; } + // Purpur end + private static @Nullable String mcVer; +@@ -33,8 +33,8 @@ public class PaperVersionFetcher implements VersionFetcher { + @Nonnull + @Override + public Component getVersionMessage(@Nonnull String serverVersion) { +- String[] parts = serverVersion.substring("git-Purpur-".length()).split("[-\\s]"); // Purpur +- final Component updateMessage = getUpdateStatusMessage("PurpurMC/Purpur", "ver/" + getMinecraftVersion(), parts[0]); // Purpur ++ String[] parts = serverVersion.substring("git-Plazma-".length()).split("[-\\s]"); // Purpur // Plazma ++ final Component updateMessage = getUpdateStatusMessage("PlazmaMC/Plazma", "ver/" + getMinecraftVersion(), parts[0]); // Purpur // Plazma + final Component history = getHistory(); + + return history != null ? Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()), history, updateMessage) : updateMessage; // Purpur +@@ -47,7 +47,7 @@ public class PaperVersionFetcher implements VersionFetcher { + String result = matcher.group(); + mcVer = result.substring(0, result.length() - 2); // strip 'R' anchor and trailing '-' + } else { +- org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Purpur!"); // Purpur ++ org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Plazma!"); // Purpur // Plazma + org.bukkit.Bukkit.getLogger().warning("Pattern: " + VER_PATTERN.toString()); + org.bukkit.Bukkit.getLogger().warning("Version: " + org.bukkit.Bukkit.getBukkitVersion()); + } +@@ -57,6 +57,7 @@ public class PaperVersionFetcher implements VersionFetcher { + } + + private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) { ++ /* // Plazma - Disalbe CI Checking + //int distance; // Purpur - use field + try { + int jenkinsBuild = Integer.parseInt(versionInfo); +@@ -65,6 +66,9 @@ public class PaperVersionFetcher implements VersionFetcher { + versionInfo = versionInfo.replace("\"", ""); + distance = fetchDistanceFromGitHub(repo, branch, versionInfo); + } ++ */ // Plazma - Disable CI Checking ++ versionInfo = versionInfo.replace("\"", ""); // Plazma ++ distance = fetchDistanceFromGitHub(repo, branch, versionInfo); // Plazma + + switch (distance) { + case -1: +@@ -83,6 +87,7 @@ public class PaperVersionFetcher implements VersionFetcher { + } + } + ++ /* // Plazma - Disalbe CI Checking + private static int fetchDistanceFromSiteApi(int jenkinsBuild, @Nullable String siteApiVersion) { + if (siteApiVersion == null) { return -1; } + try { +@@ -102,6 +107,7 @@ public class PaperVersionFetcher implements VersionFetcher { + return -1; + } + } ++ */ // Plazma - Disalbe CI Checking + + // Contributed by Techcable in GH-65 + private static int fetchDistanceFromGitHub(@Nonnull String repo, @Nonnull String branch, @Nonnull String hash) { +diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +index 3cb56595822799926a8141e60a42f5d1edfc6de5..76eb52435d80a71e8247ecdb8301a64d7078464e 100644 +--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java ++++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +@@ -17,7 +17,7 @@ public final class PaperConsole extends SimpleTerminalConsole { + @Override + protected LineReader buildReader(LineReaderBuilder builder) { + builder +- .appName("Purpur") // Purpur ++ .appName("Plazma") // Purpur // Plazma + .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) + .completer(new ConsoleCommandCompleter(this.server)) + .option(LineReader.Option.COMPLETE_IN_WORD, true); +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index b5b6657e52e4f7a630229bd3ba433438af293e22..703b2cb51940ff8625d022e884258ff46a628efa 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -35,7 +35,7 @@ public class CrashReport { + io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(cause); // Paper + this.title = message; + this.exception = cause; +- this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit ++ this.systemReport.setDetail("Plazma Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit // Plazma + } + + public String getTitle() { +@@ -125,7 +125,7 @@ public class CrashReport { + stringbuilder.append("---- Minecraft Crash Report ----\n"); + // Purpur start + stringbuilder.append("// "); +- stringbuilder.append("// DO NOT REPORT THIS TO PAPER! REPORT TO PURPUR INSTEAD!"); ++ stringbuilder.append("// DO NOT REPORT THIS TO PAPER! REPORT TO PLAZMA INSTEAD!"); // Plazma + // Purpur end + stringbuilder.append("// "); + stringbuilder.append(CrashReport.getErrorComment()); +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index 781b72fd88149642c9fceaecfbfe7546273fb749..a1f2a4f2f239e4addd0cda1c247c63f67bd9f0f5 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -76,6 +76,20 @@ public class Main { + + @DontObfuscate + public static void main(final OptionSet optionset) { // CraftBukkit - replaces main(String[] astring) ++ // Plazma start - Branding ++ LOGGER.warn("PLAZMA IS VERY UNSTABLE, SO DO NOT USE IT IN PRODUCTION SERVER!"); ++ System.out.println(""" ++ ++ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$ /$$$$$$\s ++ /$$__ $$| $$$ | $$| $$__ $$| $$__ $$ /$$__ $$| $$$ /$$$| $$_____/| $$__ $$ /$$__ $$ ++ | $$ \\ $$| $$$$| $$| $$ \\ $$| $$ \\ $$| $$ \\ $$| $$$$ /$$$$| $$ | $$ \\ $$| $$ \\ $$ ++ | $$$$$$$$| $$ $$ $$| $$ | $$| $$$$$$$/| $$ | $$| $$ $$/$$ $$| $$$$$ | $$ | $$| $$$$$$$$ ++ | $$__ $$| $$ $$$$| $$ | $$| $$__ $$| $$ | $$| $$ $$$| $$| $$__/ | $$ | $$| $$__ $$ ++ | $$ | $$| $$\\ $$$| $$ | $$| $$ \\ $$| $$ | $$| $$\\ $ | $$| $$ | $$ | $$| $$ | $$ ++ | $$ | $$| $$ \\ $$| $$$$$$$/| $$ | $$| $$$$$$/| $$ \\/ | $$| $$$$$$$$| $$$$$$$/| $$ | $$ ++ |__/ |__/|__/ \\__/|_______/ |__/ |__/ \\______/ |__/ |__/|________/|_______/ |__/ |__/ ++ """); ++ // Plazma end + SharedConstants.tryDetectVersion(); + /* CraftBukkit start - Replace everything + OptionParser optionparser = new OptionParser(); +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 1772800c123353207e3563a7e2c2b70431aec097..85b19d19e5614c2e21802452e7e5aa35493dcffb 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -927,7 +927,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Wed, 11 Jan 2023 02:24:51 +0900 +Subject: [PATCH] Plazma Configurations + + +diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java +index 7a4a7a654fe2516ed894a68f2657344df9d70f4c..ae51ab3c895b1b98d768e52b7c446bd6a3e89b9f 100644 +--- a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java ++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java +@@ -1,6 +1,6 @@ + package io.papermc.paper.configuration; + +-abstract class ConfigurationPart { ++public abstract class ConfigurationPart { // Plazma - package -> public + + public static abstract class Post extends ConfigurationPart { + +diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java +index c2dca89291361d60cbf160cab77749cb0130035a..02262c6b4a49951f4c9fc33c6bd78b9714b78a24 100644 +--- a/src/main/java/io/papermc/paper/configuration/Configurations.java ++++ b/src/main/java/io/papermc/paper/configuration/Configurations.java +@@ -88,7 +88,7 @@ public abstract class Configurations { + }; + } + +- static CheckedFunction reloader(Class type, T instance) { ++ public static CheckedFunction reloader(Class type, T instance) { // Plazma - package -> public + return node -> { + ObjectMapper.Factory factory = (ObjectMapper.Factory) Objects.requireNonNull(node.options().serializers().get(type)); + ObjectMapper.Mutable mutable = (ObjectMapper.Mutable) factory.get(type); +@@ -148,7 +148,7 @@ public abstract class Configurations { + final YamlConfigurationLoader loader = result.loader(); + final ConfigurationNode node = loader.load(); + if (result.isNewFile()) { // add version to new files +- node.node(Configuration.VERSION_FIELD).raw(WorldConfiguration.CURRENT_VERSION); ++ node.node(Configuration.VERSION_FIELD).raw(getWorldConfigurationCurrentVersion()); // Plazma + } + this.applyWorldConfigTransformations(contextMap, node); + final W instance = node.require(this.worldConfigClass); +@@ -207,7 +207,7 @@ public abstract class Configurations { + .build(); + final ConfigurationNode worldNode = worldLoader.load(); + if (newFile) { // set the version field if new file +- worldNode.node(Configuration.VERSION_FIELD).set(WorldConfiguration.CURRENT_VERSION); ++ worldNode.node(Configuration.VERSION_FIELD).set(getWorldConfigurationCurrentVersion()); // Plazma + } + this.applyWorldConfigTransformations(contextMap, worldNode); + this.applyDefaultsAwareWorldConfigTransformations(contextMap, worldNode, defaultsNode); +@@ -308,4 +308,19 @@ public abstract class Configurations { + return "ContextKey{" + this.name + "}"; + } + } ++ ++ // Plazma start ++ @Deprecated ++ public org.bukkit.configuration.file.YamlConfiguration createLegacyObject(final net.minecraft.server.MinecraftServer server) { ++ org.bukkit.configuration.file.YamlConfiguration global = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); ++ org.bukkit.configuration.ConfigurationSection worlds = global.createSection("__________WORLDS__________"); ++ worlds.set("__defaults__", org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); ++ for (ServerLevel level : server.getAllLevels()) { ++ worlds.set(level.getWorld().getName(), org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); ++ } ++ return global; ++ } ++ ++ protected abstract int getWorldConfigurationCurrentVersion(); ++ // Plazma end + } +diff --git a/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java +index a0aa1f1a7adf986d500a2135aa42e138aa3c4f08..28a1d21900dbff4b9d1887b9aa4e68f4703b778f 100644 +--- a/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java ++++ b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java +@@ -17,7 +17,7 @@ import java.util.Map; + + import static io.leangen.geantyref.GenericTypeReflector.erase; + +-final class InnerClassFieldDiscoverer implements FieldDiscoverer> { ++public final class InnerClassFieldDiscoverer implements FieldDiscoverer> { // Plazma - package -> public + + private final Map, Object> instanceMap = new HashMap<>(); + private final Map, Object> overrides; +@@ -136,7 +136,19 @@ final class InnerClassFieldDiscoverer implements FieldDiscoverer globalConfig() { ++ public static FieldDiscoverer globalConfig() { // Plazma - package -> public + return new InnerClassFieldDiscoverer(Collections.emptyMap()); + } ++ ++ // Plazma start ++ public static FieldDiscoverer plazmaLevelConfiguration(Configurations.ContextMap contextMap) { ++ final Map, Object> overrides = Map.of( ++ org.plazmamc.plazma.configurations.LevelConfigurations.class, ++ new org.plazmamc.plazma.configurations.LevelConfigurations( ++ contextMap.require(Configurations.WORLD_KEY) ++ ) ++ ); ++ return new InnerClassFieldDiscoverer(overrides); ++ } ++ // Plazma end + } +diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +index 9fde9ccb5d069ddce8dd837ef1bc68b93ce66434..9068ff25a77a2a2a4f9ba5eb097ae3b1180a38a0 100644 +--- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java ++++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +@@ -127,13 +127,13 @@ public class PaperConfigurations extends Configurations SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255)) { ++ public static final Supplier SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255)) { // Plazma - private -> public + @Override // override to ensure "verbose" is false + public void init() { + SpigotConfig.readConfig(SpigotWorldConfig.class, this); + } + }); +- static final ContextKey> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken>() {}, "spigot world config"); ++ public static final ContextKey> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken>() {}, "spigot world config"); // Plazma - package -> public + + + public PaperConfigurations(final Path globalFolder) { +@@ -297,7 +297,7 @@ public class PaperConfigurations extends Configurations public + return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig); + } + +@@ -398,17 +398,6 @@ public class PaperConfigurations extends Configurations public + if (!Files.isDirectory(path)) { + Files.createDirectories(path); + } + } ++ ++ // Plazma start ++ @Override ++ protected int getWorldConfigurationCurrentVersion() { ++ return WorldConfiguration.CURRENT_VERSION; ++ } ++ // Plazma end + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index ba6d5d63d6de5db581c8bc94155d8a3acf8f67c3..454910a4d258b55442e7e7d5d013405b9f03dda2 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -305,6 +305,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public + + public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory, File userCacheFile, joptsimple.OptionSet optionSet) throws Exception { // Paper +@@ -30,7 +38,12 @@ public record Services(MinecraftSessionService sessionService, SignatureValidato + final java.nio.file.Path legacyConfigPath = ((File) optionSet.valueOf("paper-settings")).toPath(); + final java.nio.file.Path configDirPath = ((File) optionSet.valueOf("paper-settings-directory")).toPath(); + io.papermc.paper.configuration.PaperConfigurations paperConfigurations = io.papermc.paper.configuration.PaperConfigurations.setup(legacyConfigPath, configDirPath, rootDirectory.toPath(), (File) optionSet.valueOf("spigot-settings")); +- return new Services(minecraftSessionService, signatureValidator, gameProfileRepository, gameProfileCache, paperConfigurations); ++ // Plazma start ++ final java.nio.file.Path legacyPlazmaConfigurationPath = ((File) optionSet.valueOf("plazma-configurations")).toPath(); ++ final java.nio.file.Path plazmaConfigurationDirPath = ((File) optionSet.valueOf("plazma-configurations-directory")).toPath(); ++ org.plazmamc.plazma.configurations.PlazmaConfigurations plazmaConfigurations = org.plazmamc.plazma.configurations.PlazmaConfigurations.setup(legacyPlazmaConfigurationPath, plazmaConfigurationDirPath); ++ return new Services(minecraftSessionService, signatureValidator, gameProfileRepository, gameProfileCache, paperConfigurations, plazmaConfigurations); ++ // Plazma end + // Paper end + } + } +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index fd1b0564d2d2b45128e6f2556fb93ee56bd683b5..667dbe26a96984f370e121f961abefd4f377c3d8 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -208,6 +208,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // Paper - load mappings for stacktrace deobf and etc. + paperConfigurations.initializeGlobalConfiguration(); + paperConfigurations.initializeWorldDefaultsConfiguration(); ++ // Plazma start ++ plazmaConfigurations.initializeGlobalConfiguration(); ++ plazmaConfigurations.initializeWorldDefaultsConfiguration(); ++ // Plazma end + // Paper start - moved up to right after PlayerList creation but before file load/save + if (this.convertOldUsers()) { + this.getProfileCache().save(false); // Paper +@@ -217,6 +221,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + org.spigotmc.WatchdogThread.doStart(org.spigotmc.SpigotConfig.timeoutTime, org.spigotmc.SpigotConfig.restartOnCrash); + thread.start(); // Paper - start console thread after MinecraftServer.console & PaperConfig are initialized + io.papermc.paper.command.PaperCommands.registerCommands(this); ++ org.plazmamc.plazma.commands.PlazmaCommands.registerCommands(this); + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); + // Purpur start + try { +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index a5655ebb233f1e1e1dd7f79fdd948020478928fc..57fc73fecb1e3118a8b4ab4d4f304dc7b8ff61d2 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -530,7 +530,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { + // Holder holder = worlddimension.type(); // CraftBukkit - decompile error + // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error +- super(iworlddataserver, resourcekey, worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig)), executor); // Paper - Async-Anti-Xray - Pass executor ++ super(iworlddataserver, resourcekey, worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig)), spigotConfig -> minecraftserver.plazmaConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig)), executor); // Paper - Async-Anti-Xray - Pass executor // Plazma + this.pvpMode = minecraftserver.isPvpAllowed(); + this.convertable = convertable_conversionsession; + this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 8b37fa386a43fac8144a2094681ed49a38546efb..4ab4ec1e325059e570c73216b5aaa267bdfa4f19 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -171,7 +171,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return this.paperConfig; + } + // Paper end +- ++ // Plazma start ++ private final org.plazmamc.plazma.configurations.LevelConfigurations plazmaLevelConfiguration; ++ public org.plazmamc.plazma.configurations.LevelConfigurations plazmaLevelConfiguration() { ++ return this.plazmaLevelConfiguration; ++ } ++ // Plazma end + public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray + public final org.purpurmc.purpur.PurpurWorldConfig purpurConfig; // Purpur + +@@ -326,9 +331,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + @Override public final int getHeight() { return this.height; } + // Pufferfish end + +- protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor ++ protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.function.Function plazmaLevelConfigurationCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper ++ this.plazmaLevelConfiguration = plazmaLevelConfigurationCreator.apply(this.spigotConfig); // Plazma + this.purpurConfig = new org.purpurmc.purpur.PurpurWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), env); // Purpur + this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur + this.generator = gen; +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 416c43e9a18cac4be52553020ba0af7a9a653d3b..7ac4cc8d5a33aee63e17ecd403f8021a9ba153ca 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -975,6 +975,7 @@ public final class CraftServer implements Server { + + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot + this.console.paperConfigurations.reloadConfigs(this.console); ++ this.console.plazmaConfigurations.reloadConfigurations(this.console); // Plazma + org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur + for (ServerLevel world : this.console.getAllLevels()) { + // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty +@@ -2818,6 +2819,13 @@ public final class CraftServer implements Server { + return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console); + } + ++ // Plazma start ++ @Override ++ public YamlConfiguration getPlazmaConfiguration() { ++ return CraftServer.this.console.plazmaConfigurations.createLegacyObject(CraftServer.this.console); ++ } ++ // Plazma end ++ + // Purpur start + @Override + public YamlConfiguration getPurpurConfig() { +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 576cd8e20982bb20d10213b6c7a229428eec1c2f..d92ec08aaf240759fb8d7f3febb9c25799646d60 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -166,6 +166,19 @@ public class Main { + .describedAs("Jar file"); + // Paper end + ++ // Plazma start ++ acceptsAll(asList("plazma-dir", "plazma-configurations-directory"), "Directory for Plazma configurations") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File(io.papermc.paper.configuration.PaperConfigurations.CONFIG_DIR)) ++ .describedAs("Config directory"); ++ acceptsAll(asList("plazma", "plazma-configurations"), "File for Plazma configurations") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("plazma.yml")) ++ .describedAs("Yml file"); ++ // Plazma end ++ + // Purpur Start + acceptsAll(asList("purpur", "purpur-settings"), "File for purpur settings") + .withRequiredArg() +diff --git a/src/main/java/org/plazmamc/plazma/commands/PlazmaCommand.java b/src/main/java/org/plazmamc/plazma/commands/PlazmaCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9fb819b3745ec98d81a53650d9095ded4e9aa914 +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/commands/PlazmaCommand.java +@@ -0,0 +1,127 @@ ++package org.plazmamc.plazma.commands; ++ ++import io.papermc.paper.command.CommandUtil; ++import it.unimi.dsi.fastutil.Pair; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.minecraft.Util; ++import org.bukkit.Bukkit; ++import org.bukkit.Location; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.bukkit.plugin.PluginManager; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.plazmamc.plazma.commands.subcommands.PlazmaSubCommand; ++import org.plazmamc.plazma.commands.subcommands.ReloadCommand; ++import org.plazmamc.plazma.commands.subcommands.VersionCommand; ++ ++import java.util.*; ++import java.util.stream.Collectors; ++ ++import static net.kyori.adventure.text.Component.text; ++ ++@DefaultQualifier(NonNull.class) ++public class PlazmaCommand extends Command { ++ ++ private static final Map SUB_COMMANDS = Util.make(() -> { ++ final Map, PlazmaSubCommand> commands = new HashMap<>(); ++ ++ commands.put(Set.of("reload"), new ReloadCommand()); ++ commands.put(Set.of("version"), new VersionCommand()); ++ ++ return commands.entrySet().stream() ++ .flatMap(entry -> entry.getKey().stream().map(key -> Map.entry(key, entry.getValue()))) ++ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); ++ }); ++ ++ private static final Map ALIASES = Util.make(() -> { ++ final Map> aliases = new HashMap<>(); ++ ++ aliases.put("reload", Set.of("rl")); ++ aliases.put("version", Set.of("ver")); ++ ++ return aliases.entrySet().stream() ++ .flatMap(entry -> entry.getValue().stream().map(s -> Map.entry(s, entry.getKey()))) ++ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); ++ }); ++ ++ public PlazmaCommand(final String name) { ++ super(name); ++ ++ final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); ++ ++ final List permissions = new ArrayList<>(); ++ permissions.add("bukkit.command.plazma"); ++ permissions.addAll(SUB_COMMANDS.keySet().stream().map(s -> "bukkit.command.plazma." + s).toList()); ++ ++ this.description = "Plazma related commands"; ++ this.usageMessage = String.format("/plazma [%s]", String.join("|", SUB_COMMANDS.keySet())); ++ this.setPermission(String.join(";", permissions)); ++ ++ for (final String perm : permissions) ++ pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String aliases, final String[] args) throws IllegalArgumentException { ++ if (args.length <= 1) ++ return CommandUtil.getListMatchingLast(sender, args, SUB_COMMANDS.keySet()); ++ ++ final @Nullable Pair subCommand = resolveSubCommand(args[0]); ++ if (subCommand != null) ++ return subCommand.second().tabComplete(sender, subCommand.first(), Arrays.copyOfRange(args, 1, args.length)); ++ ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ if (!testPermission(sender)) return true; ++ ++ if (args.length == 0) { ++ sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED)); ++ return false; ++ } ++ ++ final @Nullable Pair subCommand = resolveSubCommand(args[0]); ++ ++ if (subCommand == null) { ++ sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED)); ++ return false; ++ } ++ ++ if (!testPermission(sender, subCommand.first())) return true; ++ ++ final String[] choppedArgs = Arrays.copyOfRange(args, 1, args.length); ++ return subCommand.second().execute(sender, subCommand.first(), choppedArgs); ++ } ++ ++ private static @Nullable Pair resolveSubCommand(String label) { ++ label = label.toLowerCase(Locale.ENGLISH); ++ @Nullable PlazmaSubCommand subCommand = SUB_COMMANDS.get(label); ++ ++ if (subCommand == null) { ++ final @Nullable String command = ALIASES.get(label); ++ if (command != null) { ++ label = command; ++ subCommand = SUB_COMMANDS.get(command); ++ } ++ } ++ ++ if (subCommand != null) ++ return Pair.of(label, subCommand); ++ ++ return null; ++ } ++ ++ private static boolean testPermission(final CommandSender sender, final String permission) { ++ if (sender.hasPermission("bukkit.command.plazma." + permission) || sender.hasPermission("bukkit.command.plazma")) ++ return true; ++ ++ sender.sendMessage(Bukkit.permissionMessage()); ++ return false; ++ } ++} +diff --git a/src/main/java/org/plazmamc/plazma/commands/PlazmaCommands.java b/src/main/java/org/plazmamc/plazma/commands/PlazmaCommands.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4223faf9cdeb76cbafbb9b5b402ca3293c2afc1d +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/commands/PlazmaCommands.java +@@ -0,0 +1,25 @@ ++package org.plazmamc.plazma.commands; ++ ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.Command; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import java.util.HashMap; ++import java.util.Map; ++ ++@DefaultQualifier(NonNull.class) ++public final class PlazmaCommands { ++ ++ private PlazmaCommands() {} ++ ++ private static final Map COMMANDS = new HashMap<>(); ++ static { ++ COMMANDS.put("plazma", new PlazmaCommand("plazma")); ++ } ++ ++ public static void registerCommands(final MinecraftServer server) { ++ COMMANDS.forEach((s, command) -> server.server.getCommandMap().register(s, "Plazma", command)); ++ } ++ ++} +diff --git a/src/main/java/org/plazmamc/plazma/commands/subcommands/PlazmaSubCommand.java b/src/main/java/org/plazmamc/plazma/commands/subcommands/PlazmaSubCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..32f2f9c933069c32c321f71ec2c57e2261f7ad1e +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/commands/subcommands/PlazmaSubCommand.java +@@ -0,0 +1,17 @@ ++package org.plazmamc.plazma.commands.subcommands; ++ ++import org.bukkit.command.CommandSender; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import java.util.Collections; ++import java.util.List; ++ ++@DefaultQualifier(NonNull.class) ++public interface PlazmaSubCommand { ++ boolean execute(CommandSender sender, String subCommand, String[] args); ++ ++ default List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { ++ return Collections.emptyList(); ++ } ++} +diff --git a/src/main/java/org/plazmamc/plazma/commands/subcommands/ReloadCommand.java b/src/main/java/org/plazmamc/plazma/commands/subcommands/ReloadCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0b7ca5562c78758c30780f919e9c54b33ba0d305 +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/commands/subcommands/ReloadCommand.java +@@ -0,0 +1,28 @@ ++package org.plazmamc.plazma.commands.subcommands; ++ ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftServer; ++ ++import static net.kyori.adventure.text.Component.text; ++ ++public class ReloadCommand implements PlazmaSubCommand { ++ ++ @Override ++ public boolean execute(CommandSender sender, String subCommand, String[] args) { ++ return true; ++ } ++ ++ private void doReload(final CommandSender sender) { ++ Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", NamedTextColor.RED)); ++ Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", NamedTextColor.RED)); ++ ++ MinecraftServer server = ((CraftServer) sender.getServer()).getServer(); ++ server.plazmaConfigurations.reloadConfigurations(server); ++ server.server.reloadCount++; ++ ++ Command.broadcastCommandMessage(sender, text("Successfully reloaded Plazma configuration files.", NamedTextColor.GREEN)); ++ } ++} +diff --git a/src/main/java/org/plazmamc/plazma/commands/subcommands/VersionCommand.java b/src/main/java/org/plazmamc/plazma/commands/subcommands/VersionCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..85002144e0b350c4ae044e1a4a4c1734cc27c059 +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/commands/subcommands/VersionCommand.java +@@ -0,0 +1,15 @@ ++package org.plazmamc.plazma.commands.subcommands; ++ ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.checkerframework.checker.nullness.qual.Nullable; ++ ++public class VersionCommand implements PlazmaSubCommand { ++ @Override ++ public boolean execute(CommandSender sender, String subCommand, String[] args) { ++ final @Nullable Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); ++ if (ver != null) ver.execute(sender, "plazma", new String[0]); ++ return true; ++ } ++} +diff --git a/src/main/java/org/plazmamc/plazma/configurations/GlobalConfiguration.java b/src/main/java/org/plazmamc/plazma/configurations/GlobalConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f1d4e242cbf3e7eb2fc7a3dd40ba7865e26b12b1 +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/configurations/GlobalConfiguration.java +@@ -0,0 +1,22 @@ ++package org.plazmamc.plazma.configurations; ++ ++import io.papermc.paper.configuration.Configuration; ++import io.papermc.paper.configuration.ConfigurationPart; ++import org.spongepowered.configurate.objectmapping.meta.Setting; ++ ++@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) ++public class GlobalConfiguration extends ConfigurationPart { ++ static final int CURRENT_VERSION = 1; ++ private static GlobalConfiguration instance; ++ ++ public static GlobalConfiguration get() { ++ return instance; ++ } ++ ++ static void set(GlobalConfiguration instance) { ++ GlobalConfiguration.instance = instance; ++ } ++ ++ @Setting(Configuration.VERSION_FIELD) ++ public int version = CURRENT_VERSION; ++} +diff --git a/src/main/java/org/plazmamc/plazma/configurations/LevelConfigurations.java b/src/main/java/org/plazmamc/plazma/configurations/LevelConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d1b5b269173c43452960bbe38d5d940f2089c0fa +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/configurations/LevelConfigurations.java +@@ -0,0 +1,24 @@ ++package org.plazmamc.plazma.configurations; ++ ++import io.papermc.paper.configuration.Configuration; ++import io.papermc.paper.configuration.ConfigurationPart; ++import io.papermc.paper.configuration.PaperConfigurations; ++import net.minecraft.resources.ResourceLocation; ++import org.spongepowered.configurate.objectmapping.meta.Setting; ++ ++@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) ++public class LevelConfigurations extends ConfigurationPart { ++ public static final int CURRENT_VERSION = 1; ++ ++ private transient final ResourceLocation worldKey; ++ public LevelConfigurations(ResourceLocation worldKey) { ++ this.worldKey = worldKey; ++ } ++ ++ public boolean isDefault() { ++ return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY); ++ } ++ ++ @Setting(Configuration.VERSION_FIELD) ++ public int version = CURRENT_VERSION; ++} +diff --git a/src/main/java/org/plazmamc/plazma/configurations/PlazmaConfigurations.java b/src/main/java/org/plazmamc/plazma/configurations/PlazmaConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..97f81de9bcaa2c61db95834327ee4d6f7b13b7f2 +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/configurations/PlazmaConfigurations.java +@@ -0,0 +1,332 @@ ++package org.plazmamc.plazma.configurations; ++ ++import com.google.common.collect.Table; ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.*; ++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; ++import io.papermc.paper.configuration.serializer.ComponentSerializer; ++import io.papermc.paper.configuration.serializer.EnumValueSerializer; ++import io.papermc.paper.configuration.serializer.PacketClassSerializer; ++import io.papermc.paper.configuration.serializer.StringRepresentableSerializer; ++import io.papermc.paper.configuration.serializer.collections.FastutilMapSerializer; ++import io.papermc.paper.configuration.serializer.collections.MapSerializer; ++import io.papermc.paper.configuration.serializer.collections.TableSerializer; ++import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer; ++import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer; ++import io.papermc.paper.configuration.transformation.Transformations; ++import io.papermc.paper.configuration.type.*; ++import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer; ++import it.unimi.dsi.fastutil.objects.Reference2IntMap; ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; ++import org.jetbrains.annotations.VisibleForTesting; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.*; ++import org.spongepowered.configurate.objectmapping.ObjectMapper; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++import java.io.File; ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.Collections; ++import java.util.List; ++import java.util.function.Function; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++@SuppressWarnings("Convert2Diamond") ++public class PlazmaConfigurations extends Configurations { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ static final String GLOBAL_CONFIGURATION_FILE_NAME= "plazma-global.yml"; ++ static final String LEVEL_DEFAULT_CONFIGURATION_FILE_NAME = "plazma-level-dafaults.yml"; ++ static final String LEVEL_CONFIGURATION_FILE_NAME = "plazma-configuration.yml"; ++ ++ private static final String HEADER_START = """ ++ # English ++ This is the %s configuration file for Plazma. ++ As you can see, there's a lot to configure. Some options may impact gameplay, ++ so use with caution, and make sure you know what each option does before configuring. ++ ++ If you need help with the configuration or have any questions related to Plazma, ++ join us in our Discord for Plazma, or check the GitHub Wiki pages. ++ ++ %s ++ ++ # ํ•œ๊ตญ์–ด ++ ๋ณธ ํŒŒ์ผ์€ Plazma์˜ %s ๊ตฌ์„ฑ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. ++ ๋ณด์‹œ๋‹ค์‹œํ”ผ, ๊ต‰์žฅํžˆ ๋งŽ์€ ์„ค์ •์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ช‡๋ช‡ ์„ค์ •์€ ๊ฒŒ์ž„ํ”Œ๋ ˆ์ด์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ++ ์ฃผ์˜ํ•ด์„œ ์‚ฌ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ผ๋ฉฐ, ๊ฐ ์„ค์ •์ด ์–ด๋– ํ•œ ์—ญํ• ์„ ํ•˜๋Š”์ง€ ์•Œ๊ณ  ์‚ฌ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ++ ++ ๋งŒ์•ฝ ๊ตฌ์„ฑ์— ๊ด€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ Plazma์— ๊ด€๋ จํ•œ ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋‹ค๋ฉด, ++ Discord ๋˜๋Š” ๋„ค์ด๋ฒ„ ์นดํŽ˜์— ๊ฐ€์ž…ํ•˜๊ฑฐ๋‚˜, GitHub ์œ„ํ‚ค ํŽ˜์ด์ง€๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ++ ++ %s ++ ++ Wiki: https://github.com/PlazmaMC/Plazma/wiki ++ Discord: *COMMING SOON* ++ Twitter: *COMMING SOON* ++ Naver Cafe: *COMMING SOON* ++ """; ++ ++ private static final Function LEVEL_SPECIFIC_HEADER = map -> String.format(""" ++ # English ++ This is a level specific configuration file for Plazma. ++ This file may start empty, but can be filled with settings to override ones in the %s/%s ++ ++ If you need help with the configuration or have any questions related to Plazma, ++ join us in our Discord for Plazma, or check the GitHub Wiki pages. ++ ++ ++ # ํ•œ๊ตญ์–ด ++ ๋ณธ ํŒŒ์ผ์€ Plazma์˜ ๋ ˆ๋ฒจ๋ณ„ ๊ตฌ์„ฑ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. ++ ์ด ํŒŒ์ผ์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์ง€๋งŒ, %s/%s ํŒŒ์ผ์˜ ์„ค์ •์„ ๋ฎ์–ด์“ฐ๊ธฐ ์œ„ํ•ด ์„ค์ •์„ ์ฑ„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ++ ++ ๋งŒ์•ฝ ๊ตฌ์„ฑ์— ๊ด€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ Plazma์— ๊ด€๋ จํ•œ ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋‹ค๋ฉด, ++ Discord ๋˜๋Š” ๋„ค์ด๋ฒ„ ์นดํŽ˜์— ๊ฐ€์ž…ํ•˜๊ฑฐ๋‚˜, GitHub ์œ„ํ‚ค ํŽ˜์ด์ง€๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ++ ++ ++ Level: %s (%s) ++ ++ Wiki: https://github.com/PlazmaMC/Plazma/wiki ++ Discord: *COMMING SOON* ++ Twitter: *COMMING SOON* ++ Naver Cafe: *COMMING SOON* ++ """, PaperConfigurations.CONFIG_DIR, LEVEL_DEFAULT_CONFIGURATION_FILE_NAME, PaperConfigurations.CONFIG_DIR, LEVEL_DEFAULT_CONFIGURATION_FILE_NAME, ++ map.require(WORLD_NAME), map.require(WORLD_KEY)); ++ ++ private static final String GLOBAL_HEADER = String.format(HEADER_START, ++ "global", String.format(""" ++ The level configuration options are inside their respective level folder. ++ The files are named %s ++ """, LEVEL_CONFIGURATION_FILE_NAME), ++ "์ „์—ญ", String.format(""" ++ ๋ ˆ๋ฒจ ๊ตฌ์„ฑ ์˜ต์…˜์€ ๊ฐ๊ฐ์˜ ๋ ˆ๋ฒจ ํด๋” ์•ˆ์— ์žˆ์œผ๋ฉฐ, ํŒŒ์ผ ์ด๋ฆ„์€ %s ์ž…๋‹ˆ๋‹ค. ++ """, LEVEL_CONFIGURATION_FILE_NAME) ++ ); ++ ++ private static final String LEVEL_DEFAULTS_HEADER = String.format(HEADER_START, ++ "level defaults", """ ++ Configuration options here apply to all levels, unless you specify overrides inside ++ the level-specific config file inside each level folder. ++ """, ++ "๊ธฐ๋ณธ ๋ ˆ๋ฒจ", """ ++ ์ด ๊ตฌ์„ฑ ํŒŒ์ผ์˜ ์„ค์ •์€ ๊ฐ ๋ ˆ๋ฒจ ํด๋” ์•ˆ ๊ตฌ์„ฑ ํŒŒ์ผ์—์„œ ๋ฎ์–ด์“ฐ๊ธฐ ๋˜์ง€ ์•Š๋Š” ํ•œ ๋ชจ๋“  ๋ ˆ๋ฒจ์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ++ """ ++ ); ++ ++ private static final String MOVED_NOTICE = """ ++ # English ++ The global and level default configuration have moved to %s ++ and the level-specific configuration has been moved inside ++ the respective level folder. ++ ++ See GitHub Wiki for more information. ++ ++ ++ # ํ•œ๊ตญ์–ด ++ ์ „์—ญ ๋ฐ ๋ ˆ๋ฒจ ์ „์—ญ ๊ตฌ์„ฑ ํŒŒ์ผ์€ %s ๋กœ ์ด๋™๋˜์—ˆ์œผ๋ฉฐ, ๋ ˆ๋ฒจ๋ณ„ ๊ตฌ์„ฑ ํŒŒ์ผ์€ ++ ๊ฐ ๋ ˆ๋ฒจ ํด๋” ์•ˆ์œผ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ++ ++ ์ž์„ธํ•œ ๋‚ด์šฉ์€ GitHub ์œ„ํ‚ค๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ++ ++ ++ Wiki: https://github.com/TeamEarendel/Plazma/wiki ++ """; ++ ++ private static final List DEFAULTS_AWARE_TRANSFORMATIONS = Collections.emptyList(); ++ ++ public PlazmaConfigurations(final Path globalFolder) { ++ super(globalFolder, GlobalConfiguration.class, LevelConfigurations.class, GLOBAL_CONFIGURATION_FILE_NAME, LEVEL_DEFAULT_CONFIGURATION_FILE_NAME, LEVEL_CONFIGURATION_FILE_NAME); ++ } ++ ++ // Create Loader Builder ++ private static ConfigurationOptions defaultOptions(ConfigurationOptions options) { ++ return options.serializers(builder -> builder ++ .register(MapSerializer.TYPE, new MapSerializer(false)) ++ .register(new EnumValueSerializer()) ++ .register(new ComponentSerializer()) ++ ); ++ } ++ ++ @Override ++ protected YamlConfigurationLoader.Builder createLoaderBuilder() { ++ return super.createLoaderBuilder().defaultOptions(PlazmaConfigurations::defaultOptions); ++ } ++ ++ // Create Global Object Mapper Factory Builder ++ private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMapper.Factory.Builder builder) { ++ return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig()); ++ } ++ ++ @Override ++ protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() { ++ return defaultGlobalFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder()); ++ } ++ ++ // Create Global Loader Builder ++ private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) { ++ return options.header(GLOBAL_HEADER).serializers(builder -> builder.register(new PacketClassSerializer())); ++ } ++ ++ @Override ++ protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() { ++ return super.createGlobalLoaderBuilder().defaultOptions(PlazmaConfigurations::defaultGlobalOptions); ++ } ++ ++ // Initialize ++ @Override ++ public GlobalConfiguration initializeGlobalConfiguration() throws ConfigurateException { ++ GlobalConfiguration configuration = super.initializeGlobalConfiguration(); ++ GlobalConfiguration.set(configuration); ++ return configuration; ++ } ++ ++ @Override ++ protected ContextMap.Builder createDefaultContextMap() { ++ return super.createDefaultContextMap().put(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY, PaperConfigurations.SPIGOT_WORLD_DEFAULTS); ++ } ++ ++ @Override ++ protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) { ++ return super.createWorldObjectMapperFactoryBuilder(contextMap) ++ .addNodeResolver(new RequiresSpigotInitialization.Factory(contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get())) ++ .addNodeResolver(new NestedSetting.Factory()) ++ .addDiscoverer(InnerClassFieldDiscoverer.plazmaLevelConfiguration(contextMap)); ++ } ++ ++ @Override ++ protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) { ++ return super.createWorldConfigLoaderBuilder(contextMap).defaultOptions(options -> options ++ .header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? LEVEL_DEFAULTS_HEADER : LEVEL_SPECIFIC_HEADER.apply(contextMap)) ++ .serializers(serializers -> serializers ++ .register(new TypeToken>() {}, new FastutilMapSerializer.SomethingToPrimitive>(Reference2IntOpenHashMap::new, Integer.TYPE)) ++ .register(new TypeToken>() {}, new FastutilMapSerializer.SomethingToPrimitive>(Reference2LongOpenHashMap::new, Long.TYPE)) ++ .register(new TypeToken>() {}, new TableSerializer()) ++ .register(new StringRepresentableSerializer()) ++ .register(IntOr.Default.SERIALIZER) ++ .register(DoubleOrDefault.SERIALIZER) ++ .register(BooleanOrDefault.SERIALIZER) ++ .register(Duration.SERIALIZER) ++ .register(EngineMode.SERIALIZER) ++ .register(FallbackValueSerializer.create(contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), MinecraftServer::getServer)) ++ .register(new RegistryValueSerializer<>(new TypeToken>() {}, Registries.ENTITY_TYPE, true)) ++ .register(new RegistryValueSerializer<>(Item.class, Registries.ITEM, true)) ++ .register(new RegistryHolderSerializer<>(new TypeToken>() {}, Registries.CONFIGURED_FEATURE, false)) ++ .register(new RegistryHolderSerializer<>(Item.class, Registries.ITEM, true)) ++ ) ++ ); ++ } ++ ++ private void applyTransformations(final NodePath[] paths, final ConfigurationNode node) throws ConfigurateException { ++ if (paths.length > 0) { ++ ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder(); ++ ++ for (NodePath path : paths) ++ builder.addAction(path, TransformAction.remove()); ++ ++ builder.build().apply(node); ++ } ++ } ++ ++ @Override ++ protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException { ++ applyTransformations(RemovedConfigurations.REMOVED_GLOBAL_PATHS, node); ++ } ++ ++ @Override ++ protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException { ++ final ConfigurationNode version = node.node(Configuration.VERSION_FIELD); ++ final String world = contextMap.require(WORLD_NAME); ++ ++ if (version.virtual()) { ++ LOGGER.warn("The Plazma level configuration file for {} didn't have a version field, assuming latest", world); ++ version.raw(LevelConfigurations.CURRENT_VERSION); ++ } ++ ++ applyTransformations(RemovedConfigurations.REMOVED_LEVEL_PATHS, node); ++ } ++ ++ @Override ++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode levelNode, final ConfigurationNode defaultsNode) throws ConfigurateException { ++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder(); ++ DEFAULTS_AWARE_TRANSFORMATIONS.forEach(transform -> transform.apply(builder, contextMap, defaultsNode)); ++ ++ ConfigurationTransformation transformation; ++ try { ++ transformation = builder.build(); ++ } catch (IllegalArgumentException ignored) { ++ return; ++ } ++ transformation.apply(levelNode); ++ } ++ ++ @Override ++ public LevelConfigurations createWorldConfig(final ContextMap contextMap) { ++ final String levelName = contextMap.require(WORLD_NAME); ++ try { ++ return super.createWorldConfig(contextMap); ++ } catch (IOException exception) { ++ throw new RuntimeException(String.format("Could not create Plazma level configuration for %s", levelName), exception); ++ } ++ } ++ ++ @Override ++ protected boolean isConfigType(Type type) { ++ return ConfigurationPart.class.isAssignableFrom(erase(type)); ++ } ++ ++ @Override ++ protected int getWorldConfigurationCurrentVersion() { ++ return LevelConfigurations.CURRENT_VERSION; ++ } ++ ++ @VisibleForTesting ++ static ConfigurationNode createForTesting() { ++ ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build(); ++ ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults())) ++ .serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer())); ++ return BasicConfigurationNode.root(options); ++ } ++ ++ public static PlazmaConfigurations setup(final Path legacyConfigurations, final Path configurationDir) throws Exception { ++ final Path legacy = Files.isSymbolicLink(legacyConfigurations) ? Files.readSymbolicLink(legacyConfigurations) : legacyConfigurations; ++ final Path replacementFile = legacy.resolveSibling(legacyConfigurations.getFileName() + "-README.txt"); ++ ++ if (Files.exists(legacyConfigurations) && Files.notExists(replacementFile)) { ++ Path path = configurationDir.toAbsolutePath(); ++ Files.createFile(replacementFile); ++ Files.writeString(replacementFile, String.format(MOVED_NOTICE, path, path)); ++ } ++ ++ try { ++ PaperConfigurations.createDirectoriesSymlinkAware(configurationDir); ++ return new PlazmaConfigurations(configurationDir); ++ } catch (final IOException e) { ++ throw new RuntimeException("Could not setup Plazma configuration files", e); ++ } ++ } ++ ++ public void reloadConfigurations(MinecraftServer server) { ++ try { ++ this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get())); ++ this.initializeWorldDefaultsConfiguration(); ++ for (ServerLevel level : server.getAllLevels()) ++ this.createWorldConfig(PaperConfigurations.createWorldContextMap(level), reloader(this.worldConfigClass, level.plazmaLevelConfiguration())); ++ } catch (Exception e) { ++ throw new RuntimeException("Could not reload Plazma configuration files", e); ++ } ++ } ++} +diff --git a/src/main/java/org/plazmamc/plazma/configurations/RemovedConfigurations.java b/src/main/java/org/plazmamc/plazma/configurations/RemovedConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..469100cd86e6742eeebad22923097782dc86bf37 +--- /dev/null ++++ b/src/main/java/org/plazmamc/plazma/configurations/RemovedConfigurations.java +@@ -0,0 +1,11 @@ ++package org.plazmamc.plazma.configurations; ++ ++import org.spongepowered.configurate.NodePath; ++ ++interface RemovedConfigurations { ++ ++ NodePath[] REMOVED_GLOBAL_PATHS = {}; ++ ++ NodePath[] REMOVED_LEVEL_PATHS = {}; ++ ++} +diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java +index 1fa801c93597f6939b88442ad72812cc5080c37e..b3a436f861b1678053991a6276261032eeb19f21 100644 +--- a/src/test/java/org/bukkit/support/AbstractTestingBase.java ++++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java +@@ -63,6 +63,7 @@ public abstract class AbstractTestingBase { + + DummyEnchantments.setup(); + io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper ++ org.plazmamc.plazma.configurations.GlobalConfigurationTestingBase.setupGlobalConfigurationForTest(); // Plazma + + ImmutableList.Builder builder = ImmutableList.builder(); + for (Material m : Material.values()) { +diff --git a/src/test/java/org/plazmamc/plazma/configurations/GlobalConfigurationTestingBase.java b/src/test/java/org/plazmamc/plazma/configurations/GlobalConfigurationTestingBase.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2b9a17a2e04f7fb6b801920ed04133db9478f984 +--- /dev/null ++++ b/src/test/java/org/plazmamc/plazma/configurations/GlobalConfigurationTestingBase.java +@@ -0,0 +1,18 @@ ++package org.plazmamc.plazma.configurations; ++ ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public class GlobalConfigurationTestingBase { ++ public static void setupGlobalConfigurationForTest() { ++ if (GlobalConfiguration.get() == null) { ++ ConfigurationNode node = PlazmaConfigurations.createForTesting(); ++ try { ++ GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class); ++ GlobalConfiguration.set(globalConfiguration); ++ } catch (SerializationException e) { ++ throw new RuntimeException(e); ++ } ++ } ++ } ++} diff --git a/scripts/fixupAPI.cmd b/scripts/fixupAPI.cmd new file mode 100644 index 0000000..41ad628 --- /dev/null +++ b/scripts/fixupAPI.cmd @@ -0,0 +1,8 @@ +@echo off +cd Andromeda-API >> NUL +git add . >> NUL +git commit -m "fixup" >> NUL +git format-patch -1 >> NUL +0001-fixup.patch >> NUL +cd .. >> NUL +echo Complete. diff --git a/scripts/fixupServer.cmd b/scripts/fixupServer.cmd new file mode 100644 index 0000000..4e574e8 --- /dev/null +++ b/scripts/fixupServer.cmd @@ -0,0 +1,8 @@ +@echo off +cd Andromeda-Server >> NUL +git add . >> NUL +git commit -m "fixup" >> NUL +git format-patch -1 >> NUL +0001-fixup.patch >> NUL +cd .. >> NUL +echo Complete. diff --git a/scripts/pull.cmd b/scripts/pull.cmd new file mode 100644 index 0000000..6828405 --- /dev/null +++ b/scripts/pull.cmd @@ -0,0 +1,8 @@ +@echo off +git pull +taskkill /f /t /im java.exe >> NUL +taskkill /f /t /im git.exe >> NUL +rd /s /q Andromeda-API >> NUL +rd /s /q Andromeda-Server >> NUL +rd /s /q .gradle >> NUL +call gradlew.bat applyPatches diff --git a/scripts/reapply.cmd b/scripts/reapply.cmd new file mode 100644 index 0000000..38ede24 --- /dev/null +++ b/scripts/reapply.cmd @@ -0,0 +1,7 @@ +@echo off +taskkill /f /t /im java.exe >> NUL +taskkill /f /t /im git.exe >> NUL +rd /s /q Andromeda-API >> NUL +rd /s /q Andromeda-Server >> NUL +rd /s /q .gradle >> NUL +call gradlew.bat applyPatches diff --git a/scripts/reapplyAPI.cmd b/scripts/reapplyAPI.cmd new file mode 100644 index 0000000..dcb4da2 --- /dev/null +++ b/scripts/reapplyAPI.cmd @@ -0,0 +1,5 @@ +@echo off +taskkill /f /t /im java.exe >> NUL +taskkill /f /t /im git.exe >> NUL +rd /s /q Andromeda-API >> NUL +call gradlew.bat applyAPIPatches diff --git a/scripts/reapplyServer.cmd b/scripts/reapplyServer.cmd new file mode 100644 index 0000000..7f43fdd --- /dev/null +++ b/scripts/reapplyServer.cmd @@ -0,0 +1,5 @@ +@echo off +taskkill /f /t /im java.exe >> NUL +taskkill /f /t /im git.exe >> NUL +rd /s /q Andromeda-Server >> NUL +call gradlew.bat applyServerPatches diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..79f8870 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,15 @@ +import java.util.Locale + +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://papermc.io/repo/repository/maven-public/") + } +} + +rootProject.name = "plazma" +for (name in listOf("Plazma-API", "Plazma-Server")) { + val projName = name.toLowerCase(Locale.ENGLISH) + include(projName) + findProject(":$projName")!!.projectDir = file(name) +}