diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fbce1e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI +on: + - push + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '18' + cache: 'gradle' + - name: Patch and build + run: | + git config --global user.email "no-reply@github.com" + git config --global user.name "Github Actions" + ./gradlew applyPatches --stacktrace + ./gradlew build --stacktrace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11c8052 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +.idea/ + +petal-api/ +petal-server/ + +build-data/ +build/ +run/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7e54ada --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,71 @@ +import io.papermc.paperweight.util.constants.PAPERCLIP_CONFIG +import java.nio.charset.StandardCharsets + +plugins { + java + id("com.github.johnrengelman.shadow") version "7.1.2" apply false + id("io.papermc.paperweight.patcher") version "1.3.8" +} + +repositories { + mavenCentral() + maven("https://papermc.io/repo/repository/maven-public/") { + content { onlyForConfigurations(PAPERCLIP_CONFIG) } + } +} + +dependencies { + remapper("net.fabricmc:tiny-remapper:0.8.2:fat") + decompiler("net.minecraftforge:forgeflower:1.5.605.7") + paperclip("io.papermc:paperclip:3.0.2") +} + +subprojects { + apply(plugin = "java") + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + + tasks.withType().configureEach { + options.encoding = StandardCharsets.UTF_8.name() + options.release.set(17) + } + + repositories { + mavenLocal() + 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") + } +} + +paperweight { + serverProject.set(project(":petal-server")) + + remapRepo.set("https://maven.fabricmc.net/") + decompileRepo.set("https://files.minecraftforge.net/maven/") + + useStandardUpstream("purpur") { + url.set(github("PurpurMC", "Purpur")) + ref.set(providers.gradleProperty("purpurRef")) + + withStandardPatcher { + apiSourceDirPath.set("Purpur-API") + serverSourceDirPath.set("Purpur-Server") + + apiPatchDir.set(layout.projectDirectory.dir("patches/api")) + serverPatchDir.set(layout.projectDirectory.dir("patches/server")) + + apiOutputDir.set(layout.projectDirectory.dir("petal-api")) + serverOutputDir.set(layout.projectDirectory.dir("petal-server")) + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ce9a2ce --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.vfs.watch=false + +group=host.bloom.petal +version=1.19-R0.1-SNAPSHOT +mcVersion=1.19 +packageVersion=1_19_R1 +purpurRef=8aa5c259c5cb5ebdc0b8e081834aef5aeee0dac6 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 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..8049c68 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/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/master/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# 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*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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..53a6b23 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@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=. +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/server/0001-conf-brand-server-to-petal.patch b/patches/server/0001-conf-brand-server-to-petal.patch new file mode 100644 index 0000000..f34e77e --- /dev/null +++ b/patches/server/0001-conf-brand-server-to-petal.patch @@ -0,0 +1,186 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: peaches94 +Date: Tue, 14 Jun 2022 21:20:31 -0500 +Subject: [PATCH] conf: brand server to petal + + +diff --git a/build.gradle.kts b/build.gradle.kts +index fd3805da1f276c76a2e814f755f6755245ec2ff4..92842974ebdaeb4f44fd3966088920d4539edd35 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -9,7 +9,7 @@ plugins { + } + + dependencies { +- implementation(project(":purpur-api")) // Purpur ++ implementation(project(":petal-api")) // Purpur // petal + // Pufferfish start + implementation("io.papermc.paper:paper-mojangapi:1.19-R0.1-SNAPSHOT") { + exclude("io.papermc.paper", "paper-api") +@@ -87,7 +87,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-petal-$implementationVersion",// Purpur // petal + "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 acd95cf1dc7f009b63e44e4404e1736283fd458e..f10c8cde1bf11db618fcd128aa60c3b524746360 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("petal", serverUUID, logFailedRequests, Bukkit.getLogger()); // Purpur // petal + + 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", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Purpur ++ metrics.addCustomChart(new Metrics.SimplePie("petal_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Purpur // petal + + 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..3f9aa4292ff45c6b6af0ddfeecb645f813ec5eca 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -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-petal-".length()).split("[-\\s]"); // Purpur // petal ++ final Component updateMessage = getUpdateStatusMessage("Bloom-host/petal", "ver/" + getMinecraftVersion(), parts[0]); // Purpur // petal + final Component history = getHistory(); + + return history != null ? Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()), history, updateMessage) : updateMessage; // Purpur +@@ -58,6 +58,8 @@ public class PaperVersionFetcher implements VersionFetcher { + + private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) { + //int distance; // Purpur - use field ++ // petal start ++ /* + try { + int jenkinsBuild = Integer.parseInt(versionInfo); + distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion()); +@@ -65,6 +67,10 @@ public class PaperVersionFetcher implements VersionFetcher { + versionInfo = versionInfo.replace("\"", ""); + distance = fetchDistanceFromGitHub(repo, branch, versionInfo); + } ++ */ ++ versionInfo = versionInfo.replace("\"", ""); ++ distance = fetchDistanceFromGitHub(repo, branch, versionInfo); ++ // petal end + + switch (distance) { + case -1: +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 2ece154b5e7daaa3e0b128145fc5e1d452125f72..36854f0c3be5b17b771d73c3863e45adf3267933 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -913,7 +913,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ // petal start ++ final String purpurModName = org.purpurmc.purpur.PurpurConfig.serverModName; // Purpur - Purpur > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ ++ return switch (purpurModName) { ++ case "Purpur" -> "petal"; ++ default -> purpurModName; ++ }; ++ // petal end + } + + public SystemReport fillSystemReport(SystemReport details) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 7208236ee3c604620fd0d0afe49215320e45e24b..62f3ee7b1b12239319f00882b854fcbe3a8d5ed8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -246,7 +246,7 @@ import javax.annotation.Nullable; // Paper + import javax.annotation.Nonnull; // Paper + + public final class CraftServer implements Server { +- private final String serverName = "Purpur"; // Paper // Purpur ++ private final String serverName = "petal"; // Paper // Purpur // petal + private final String serverVersion; + private final String bukkitVersion = Versioning.getBukkitVersion(); + private final Logger logger = Logger.getLogger("Minecraft"); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index fb87620c742ff7912f5e8ccd2a7930dd605576d9..8c0215954360adf6c51bd71994273e8b6b8e07a3 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/org.purpurmc.purpur/purpur-api/pom.properties"); // Purpur ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/host.bloom.petal/petal-api/pom.properties"); // Purpur // petal + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 4a88fbee3566ba54b615745a2e4801691f494557..d89aecd421b83d539d29fd5e87c1bf9a992ab2dc 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -155,14 +155,14 @@ public class WatchdogThread extends Thread + if (isLongTimeout) { + // Paper end + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Purpur bug." ); // Paper // Purpur ++ log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a petal bug." ); // Paper // Purpur // petal + 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 Purpur bug, please report this to https://github.com/PurpurMC/Purpur/issues" ); // Purpur ++ log.log( Level.SEVERE, "If you are unsure or still think this is a petal bug, please report this to https://github.com/Bloom-host/petal/issues" ); // Purpur // petal + log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" ); +- log.log( Level.SEVERE, "Purpur version: " + Bukkit.getServer().getVersion() ); // Purpur ++ log.log( Level.SEVERE, "petal version: " + Bukkit.getServer().getVersion() ); // Purpur // petal + // + if ( net.minecraft.world.level.Level.lastPhysicsProblem != null ) + { +@@ -185,12 +185,12 @@ public class WatchdogThread extends Thread + // Paper end + } else + { +- 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, "--- DO NOT REPORT THIS TO PETAL - THIS IS NOT A BUG OR A CRASH - " + Bukkit.getServer().getVersion() + " ---"); // Purpur // petal + 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 Purpur!):" ); // Paper // Purpur ++ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to petal!):" ); // Paper // Purpur // petal + com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper + this.dumpTickingInfo(); // Paper - log detailed tick information + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log ); +@@ -206,7 +206,7 @@ public class WatchdogThread extends Thread + WatchdogThread.dumpThread( thread, log ); + } + } else { +- log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PURPUR - THIS IS NOT A BUG OR A CRASH ---"); // Purpur ++ log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PETAL - THIS IS NOT A BUG OR A CRASH ---"); // Purpur // petal + } + + log.log( Level.SEVERE, "------------------------------" ); diff --git a/patches/server/0002-feat-async-path-processing.patch b/patches/server/0002-feat-async-path-processing.patch new file mode 100644 index 0000000..2e611ab --- /dev/null +++ b/patches/server/0002-feat-async-path-processing.patch @@ -0,0 +1,975 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: peaches94 +Date: Sun, 26 Jun 2022 16:51:37 -0500 +Subject: [PATCH] feat: async path processing + + +diff --git a/src/main/java/host/bloom/pathfinding/AsyncPath.java b/src/main/java/host/bloom/pathfinding/AsyncPath.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c6d9d79707d7953f30e7373ca6e3eeced76de761 +--- /dev/null ++++ b/src/main/java/host/bloom/pathfinding/AsyncPath.java +@@ -0,0 +1,280 @@ ++package host.bloom.pathfinding; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.pathfinder.Node; ++import net.minecraft.world.level.pathfinder.Path; ++import net.minecraft.world.phys.Vec3; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.CompletableFuture; ++import java.util.function.Supplier; ++ ++/** ++ * i'll be using this to represent a path that not be processed yet! ++ */ ++public class AsyncPath extends Path { ++ ++ /** ++ * marks whether this async path has been processed ++ */ ++ private volatile boolean processed = false; ++ ++ /** ++ * a future representing the processing state of this path ++ */ ++ private final @NotNull CompletableFuture processingFuture; ++ ++ /** ++ * a list of positions that this path could path towards ++ */ ++ private final Set positions; ++ ++ /** ++ * the supplier of the real processed path ++ */ ++ private final Supplier pathSupplier; ++ ++ /* ++ * Processed values ++ */ ++ ++ /** ++ * this is a reference to the nodes list in the parent `Path` object ++ */ ++ private final List nodes; ++ /** ++ * the block we're trying to path to ++ * ++ * while processing, we have no idea where this is so consumers of `Path` should check that the path is processed before checking the target block ++ */ ++ private @Nullable BlockPos target; ++ /** ++ * how far we are to the target ++ * ++ * while processing, the target could be anywhere but theoretically we're always "close" to a theoretical target so default is 0 ++ */ ++ private float distToTarget = 0; ++ /** ++ * whether we can reach the target ++ * ++ * while processing we can always theoretically reach the target so default is true ++ */ ++ private boolean canReach = true; ++ ++ public AsyncPath(@NotNull List emptyNodeList, @NotNull Set positions, @NotNull Supplier pathSupplier) { ++ //noinspection ConstantConditions ++ super(emptyNodeList, null, false); ++ ++ this.nodes = emptyNodeList; ++ this.positions = positions; ++ this.pathSupplier = pathSupplier; ++ ++ this.processingFuture = AsyncPathProcessor.queue(this).thenApply((unused) -> this); ++ } ++ ++ @Override ++ public boolean isProcessed() { ++ return this.processed; ++ } ++ ++ /** ++ * returns the future representing the processing state of this path ++ * @return a future ++ */ ++ public @NotNull CompletableFuture getProcessingFuture() { ++ return this.processingFuture; ++ } ++ ++ /** ++ * an easy way to check if this processing path is the same as an attempted new path ++ * ++ * @param positions - the positions to compare against ++ * @return true if we are processing the same positions ++ */ ++ public boolean hasSameProcessingPositions(final Set positions) { ++ if (this.positions.size() != positions.size()) { ++ return false; ++ } ++ ++ return this.positions.containsAll(positions); ++ } ++ ++ /** ++ * starts processing this path ++ */ ++ public synchronized void process() { ++ if (this.processed) { ++ return; ++ } ++ ++ final Path bestPath = this.pathSupplier.get(); ++ ++ this.nodes.addAll(bestPath.nodes); // we mutate this list to reuse the logic in Path ++ this.target = bestPath.getTarget(); ++ this.distToTarget = bestPath.getDistToTarget(); ++ this.canReach = bestPath.canReach(); ++ ++ this.processed = true; ++ } ++ ++ /** ++ * if this path is accessed while it hasn't processed, just process it in-place ++ */ ++ private void checkProcessed() { ++ if (!this.processed) { ++ this.process(); ++ } ++ } ++ ++ /* ++ * overrides we need for final fields that we cannot modify after processing ++ */ ++ ++ @Override ++ public @NotNull BlockPos getTarget() { ++ this.checkProcessed(); ++ ++ return this.target; ++ } ++ ++ @Override ++ public float getDistToTarget() { ++ this.checkProcessed(); ++ ++ return this.distToTarget; ++ } ++ ++ @Override ++ public boolean canReach() { ++ this.checkProcessed(); ++ ++ return this.canReach; ++ } ++ ++ /* ++ * overrides to ensure we're processed first ++ */ ++ ++ @Override ++ public boolean isDone() { ++ return this.isProcessed() && super.isDone(); ++ } ++ ++ @Override ++ public void advance() { ++ this.checkProcessed(); ++ ++ super.advance(); ++ } ++ ++ @Override ++ public boolean notStarted() { ++ this.checkProcessed(); ++ ++ return super.notStarted(); ++ } ++ ++ @Nullable ++ @Override ++ public Node getEndNode() { ++ this.checkProcessed(); ++ ++ return super.getEndNode(); ++ } ++ ++ @Override ++ public Node getNode(int index) { ++ this.checkProcessed(); ++ ++ return super.getNode(index); ++ } ++ ++ @Override ++ public void truncateNodes(int length) { ++ this.checkProcessed(); ++ ++ super.truncateNodes(length); ++ } ++ ++ @Override ++ public void replaceNode(int index, Node node) { ++ this.checkProcessed(); ++ ++ super.replaceNode(index, node); ++ } ++ ++ @Override ++ public int getNodeCount() { ++ this.checkProcessed(); ++ ++ return super.getNodeCount(); ++ } ++ ++ @Override ++ public int getNextNodeIndex() { ++ this.checkProcessed(); ++ ++ return super.getNextNodeIndex(); ++ } ++ ++ @Override ++ public void setNextNodeIndex(int nodeIndex) { ++ this.checkProcessed(); ++ ++ super.setNextNodeIndex(nodeIndex); ++ } ++ ++ @Override ++ public Vec3 getEntityPosAtNode(Entity entity, int index) { ++ this.checkProcessed(); ++ ++ return super.getEntityPosAtNode(entity, index); ++ } ++ ++ @Override ++ public BlockPos getNodePos(int index) { ++ this.checkProcessed(); ++ ++ return super.getNodePos(index); ++ } ++ ++ @Override ++ public Vec3 getNextEntityPos(Entity entity) { ++ this.checkProcessed(); ++ ++ return super.getNextEntityPos(entity); ++ } ++ ++ @Override ++ public BlockPos getNextNodePos() { ++ this.checkProcessed(); ++ ++ return super.getNextNodePos(); ++ } ++ ++ @Override ++ public Node getNextNode() { ++ this.checkProcessed(); ++ ++ return super.getNextNode(); ++ } ++ ++ @Nullable ++ @Override ++ public Node getPreviousNode() { ++ this.checkProcessed(); ++ ++ return super.getPreviousNode(); ++ } ++ ++ @Override ++ public boolean hasNext() { ++ this.checkProcessed(); ++ ++ return super.hasNext(); ++ } ++} +diff --git a/src/main/java/host/bloom/pathfinding/AsyncPathProcessor.java b/src/main/java/host/bloom/pathfinding/AsyncPathProcessor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e900f8fb637c0d69f94c5a24fb17f794513a482b +--- /dev/null ++++ b/src/main/java/host/bloom/pathfinding/AsyncPathProcessor.java +@@ -0,0 +1,44 @@ ++package host.bloom.pathfinding; ++ ++import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.level.pathfinder.Path; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.Executor; ++import java.util.concurrent.Executors; ++import java.util.function.Consumer; ++ ++/** ++ * used to handle the scheduling of async path processing ++ */ ++public class AsyncPathProcessor { ++ ++ private static final Executor mainThreadExecutor = MinecraftServer.getServer(); ++ private static final Executor pathProcessingExecutor = Executors.newCachedThreadPool(new ThreadFactoryBuilder() ++ .setNameFormat("petal-path-processor-%d") ++ .setPriority(Thread.NORM_PRIORITY - 2) ++ .build()); ++ ++ protected static CompletableFuture queue(@NotNull AsyncPath path) { ++ return CompletableFuture.runAsync(path::process, pathProcessingExecutor); ++ } ++ ++ /** ++ * takes a possibly unprocessed path, and waits until it is completed ++ * the consumer will be immediately invoked if the path is already processed ++ * the consumer will always be called on the main thread ++ * ++ * @param path a path to wait on ++ * @param afterProcessing a consumer to be called ++ */ ++ public static void awaitProcessing(@Nullable Path path, Consumer<@Nullable Path> afterProcessing) { ++ if (path != null && !path.isProcessed() && path instanceof AsyncPath asyncPath) { ++ asyncPath.getProcessingFuture().thenAcceptAsync(afterProcessing, mainThreadExecutor); ++ } else { ++ afterProcessing.accept(path); ++ } ++ } ++} +diff --git a/src/main/java/host/bloom/pathfinding/NodeEvaluatorCache.java b/src/main/java/host/bloom/pathfinding/NodeEvaluatorCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d70c82e4b117ee3bf9df6ba322e4ae9fc99d1124 +--- /dev/null ++++ b/src/main/java/host/bloom/pathfinding/NodeEvaluatorCache.java +@@ -0,0 +1,39 @@ ++package host.bloom.pathfinding; ++ ++import net.minecraft.world.level.pathfinder.NodeEvaluator; ++import org.apache.commons.lang.Validate; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.Map; ++import java.util.Queue; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.ConcurrentLinkedQueue; ++ ++public class NodeEvaluatorCache { ++ private static final Map> threadLocalNodeEvaluators = new ConcurrentHashMap<>(); ++ private static final Map nodeEvaluatorToGenerator = new ConcurrentHashMap<>(); ++ ++ private static @NotNull Queue getDequeForGenerator(@NotNull NodeEvaluatorGenerator generator) { ++ return threadLocalNodeEvaluators.computeIfAbsent(generator, (key) -> new ConcurrentLinkedQueue<>()); ++ } ++ ++ public static @NotNull NodeEvaluator takeNodeEvaluator(@NotNull NodeEvaluatorGenerator generator) { ++ var nodeEvaluator = getDequeForGenerator(generator).poll(); ++ ++ if (nodeEvaluator == null) { ++ nodeEvaluator = generator.generate(); ++ } ++ ++ nodeEvaluatorToGenerator.put(nodeEvaluator, generator); ++ ++ return nodeEvaluator; ++ } ++ ++ public static void returnNodeEvaluator(@NotNull NodeEvaluator nodeEvaluator) { ++ final var generator = nodeEvaluatorToGenerator.remove(nodeEvaluator); ++ Validate.notNull(generator, "NodeEvaluator already returned"); ++ ++ getDequeForGenerator(generator).offer(nodeEvaluator); ++ } ++ ++} +diff --git a/src/main/java/host/bloom/pathfinding/NodeEvaluatorGenerator.java b/src/main/java/host/bloom/pathfinding/NodeEvaluatorGenerator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d5327cb257d63291adc8b5c60cffb4e47e1e5b0e +--- /dev/null ++++ b/src/main/java/host/bloom/pathfinding/NodeEvaluatorGenerator.java +@@ -0,0 +1,10 @@ ++package host.bloom.pathfinding; ++ ++import net.minecraft.world.level.pathfinder.NodeEvaluator; ++import org.jetbrains.annotations.NotNull; ++ ++public interface NodeEvaluatorGenerator { ++ ++ @NotNull NodeEvaluator generate(); ++ ++} +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 bf3b8ccb3e031e0ad24cd51e28ea8cbd4f8a8030..e0453df8c0fcdb40ef0ed5ae8865d45df3325e46 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 +@@ -93,8 +93,21 @@ public class AcquirePoi extends Behavior { + io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, this.poiType, predicate, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); + Set, BlockPos>> set = new java.util.HashSet<>(poiposes); + // Paper end - optimise POI access +- Path path = findPathToPois(entity, set); +- if (path != null && path.canReach()) { ++ // petal start - await on path async ++ Path possiblePath = findPathToPois(entity, set); ++ ++ // petal - wait on the path to be processed ++ host.bloom.pathfinding.AsyncPathProcessor.awaitProcessing(possiblePath, path -> { ++ // petal - readd canReach check ++ if (path == null || !path.canReach()) { ++ for(Pair, BlockPos> pair : set) { ++ this.batchCache.computeIfAbsent(pair.getSecond().asLong(), (m) -> { ++ return new AcquirePoi.JitteredLinearRetry(entity.level.random, time); ++ }); ++ } ++ return; ++ } ++ + BlockPos blockPos = path.getTarget(); + poiManager.getType(blockPos).ifPresent((holder) -> { + poiManager.take(this.poiType, (holderx, blockPos2) -> { +@@ -107,13 +120,8 @@ public class AcquirePoi extends Behavior { + this.batchCache.clear(); + DebugPackets.sendPoiTicketCountPacket(world, blockPos); + }); +- } else { +- for(Pair, BlockPos> pair : set) { +- this.batchCache.computeIfAbsent(pair.getSecond().asLong(), (m) -> { +- return new AcquirePoi.JitteredLinearRetry(entity.level.random, time); +- }); +- } +- } ++ }); ++ // petal end + + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java b/src/main/java/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java +index 18364ce4c60172529b10bc9e3a813dcedc4b766f..e6cd04bc4e19a54b1bd621ce1c880e932207bce3 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java +@@ -58,6 +58,7 @@ public class MoveToTargetSink extends Behavior { + + @Override + protected boolean canStillUse(ServerLevel serverLevel, Mob mob, long l) { ++ if (this.path != null && !this.path.isProcessed()) return true; // petal - wait for path to process + if (this.path != null && this.lastTargetPos != null) { + Optional optional = mob.getBrain().getMemory(MemoryModuleType.WALK_TARGET); + PathNavigation pathNavigation = mob.getNavigation(); +@@ -87,6 +88,8 @@ public class MoveToTargetSink extends Behavior { + + @Override + protected void tick(ServerLevel world, Mob entity, long time) { ++ if (this.path != null && !this.path.isProcessed()) return; // petal - wait for processing ++ + Path path = entity.getNavigation().getPath(); + Brain brain = entity.getBrain(); + if (this.path != path) { +@@ -94,6 +97,12 @@ public class MoveToTargetSink extends Behavior { + brain.setMemory(MemoryModuleType.PATH, path); + } + ++ // petal start - periodically recall moveTo to ensure we're moving towards the correct path ++ if (time % 8 == 0) { ++ entity.getNavigation().moveTo(this.path, (double)this.speedModifier); ++ } ++ // petal end ++ + if (path != null && this.lastTargetPos != null) { + WalkTarget walkTarget = brain.getMemory(MemoryModuleType.WALK_TARGET).get(); + if (walkTarget.getTarget().currentBlockPosition().distSqr(this.lastTargetPos) > 4.0D && this.tryComputePath(entity, walkTarget, world.getGameTime())) { +@@ -112,6 +121,20 @@ public class MoveToTargetSink extends Behavior { + if (this.reachedTarget(entity, walkTarget)) { + brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); + } else { ++ // petal start - move this out to postProcessPath ++ host.bloom.pathfinding.AsyncPathProcessor.awaitProcessing(this.path, (unusedPath) -> { ++ this.postProcessPath(entity, walkTarget, time); ++ }); ++ return true; ++ } ++ ++ return false; ++ } ++ ++ private boolean postProcessPath(Mob entity, WalkTarget walkTarget, long time) { ++ Brain brain = entity.getBrain(); ++ BlockPos blockPos = walkTarget.getTarget().currentBlockPosition(); ++ + boolean bl = this.path != null && this.path.canReach(); + if (bl) { + brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); +@@ -128,10 +151,17 @@ public class MoveToTargetSink extends Behavior { + this.path = entity.getNavigation().createPath(vec3.x, vec3.y, vec3.z, 0); + return this.path != null; + } ++ ++ // We failed, so erase and move on ++ brain.eraseMemory(MemoryModuleType.WALK_TARGET); ++ if (bl) { ++ brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); + } ++ this.path = null; + + return false; + } ++ // petal end + + private boolean reachedTarget(Mob entity, WalkTarget walkTarget) { + return walkTarget.getTarget().currentBlockPosition().distManhattan(entity.blockPosition()) <= walkTarget.getCloseEnoughDist(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java b/src/main/java/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java +index 9bd6d4f7b86daaaa9cfbad454dde06b797e3f667..dc9dca72a22df9acadb8cdae8383522c996cbe10 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java +@@ -71,19 +71,25 @@ public class SetClosestHomeAsWalkTarget extends Behavior { + Set, BlockPos>> set = poiManager.findAllWithType((poiType) -> { + return poiType.is(PoiTypes.HOME); + }, predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY).collect(Collectors.toSet()); +- Path path = AcquirePoi.findPathToPois(pathfinderMob, set); +- if (path != null && path.canReach()) { ++ // petal start - await on path async ++ Path possiblePath = AcquirePoi.findPathToPois(pathfinderMob, set); ++ ++ // petal - wait on the path to be processed ++ host.bloom.pathfinding.AsyncPathProcessor.awaitProcessing(possiblePath, path -> { ++ if (path == null || !path.canReach() || this.triedCount < 5) { // petal - readd canReach check ++ this.batchCache.long2LongEntrySet().removeIf((entry) -> { ++ return entry.getLongValue() < this.lastUpdate; ++ }); ++ return; ++ } ++ + BlockPos blockPos = path.getTarget(); + Optional> optional = poiManager.getType(blockPos); + if (optional.isPresent()) { + entity.getBrain().setMemory(MemoryModuleType.WALK_TARGET, new WalkTarget(blockPos, this.speedModifier, 1)); + DebugPackets.sendPoiTicketCountPacket(world, blockPos); + } +- } else if (this.triedCount < 5) { +- this.batchCache.long2LongEntrySet().removeIf((entry) -> { +- return entry.getLongValue() < this.lastUpdate; +- }); +- } +- ++ }); ++ // petal end + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/AmphibiousPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/AmphibiousPathNavigation.java +index 29a872393f2f995b13b4ed26b42c6464ab27ca73..664d94c39948888bbd452ffd4b68c3790cfb0291 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/AmphibiousPathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/AmphibiousPathNavigation.java +@@ -8,6 +8,14 @@ import net.minecraft.world.level.pathfinder.PathFinder; + import net.minecraft.world.phys.Vec3; + + public class AmphibiousPathNavigation extends PathNavigation { ++ // petal start ++ private static final host.bloom.pathfinding.NodeEvaluatorGenerator nodeEvaluatorGenerator = () -> { ++ var nodeEvaluator = new AmphibiousNodeEvaluator(false); ++ nodeEvaluator.setCanPassDoors(true); ++ return nodeEvaluator; ++ }; ++ // petal end ++ + public AmphibiousPathNavigation(Mob mob, Level world) { + super(mob, world); + } +@@ -16,7 +24,7 @@ public class AmphibiousPathNavigation extends PathNavigation { + protected PathFinder createPathFinder(int range) { + this.nodeEvaluator = new AmphibiousNodeEvaluator(false); + this.nodeEvaluator.setCanPassDoors(true); +- return new PathFinder(this.nodeEvaluator, range); ++ return new PathFinder(this.nodeEvaluator, range, nodeEvaluatorGenerator); // petal + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java +index 27cd393e81f6ef9b5690c051624d8d2af50acd34..da870c729a8a4673d734e8704355ad1c92855f3c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java +@@ -12,6 +12,15 @@ import net.minecraft.world.level.pathfinder.PathFinder; + import net.minecraft.world.phys.Vec3; + + public class FlyingPathNavigation extends PathNavigation { ++ ++ // petal start ++ private static final host.bloom.pathfinding.NodeEvaluatorGenerator nodeEvaluatorGenerator = () -> { ++ var nodeEvaluator = new FlyNodeEvaluator(); ++ nodeEvaluator.setCanPassDoors(true); ++ return nodeEvaluator; ++ }; ++ // petal end ++ + public FlyingPathNavigation(Mob entity, Level world) { + super(entity, world); + } +@@ -20,7 +29,7 @@ public class FlyingPathNavigation extends PathNavigation { + protected PathFinder createPathFinder(int range) { + this.nodeEvaluator = new FlyNodeEvaluator(); + this.nodeEvaluator.setCanPassDoors(true); +- return new PathFinder(this.nodeEvaluator, range); ++ return new PathFinder(this.nodeEvaluator, range, nodeEvaluatorGenerator); // petal + } + + @Override +@@ -45,6 +54,8 @@ public class FlyingPathNavigation extends PathNavigation { + this.recomputePath(); + } + ++ if (this.path != null && !this.path.isProcessed()) return; // petal ++ + if (!this.isDone()) { + if (this.canUpdatePath()) { + this.followThePath(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +index f610c06d7bb51ec2c63863dd46711712986a106a..ff5f0788e1df9ea7a96a8fea475cc010d12e9772 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +@@ -15,6 +15,15 @@ import net.minecraft.world.level.pathfinder.WalkNodeEvaluator; + import net.minecraft.world.phys.Vec3; + + public class GroundPathNavigation extends PathNavigation { ++ ++ // petal start ++ private static final host.bloom.pathfinding.NodeEvaluatorGenerator nodeEvaluatorGenerator = () -> { ++ var nodeEvaluator = new WalkNodeEvaluator(); ++ nodeEvaluator.setCanPassDoors(true); ++ return nodeEvaluator; ++ }; ++ // petal end ++ + private boolean avoidSun; + + public GroundPathNavigation(Mob entity, Level world) { +@@ -25,7 +34,7 @@ public class GroundPathNavigation extends PathNavigation { + protected PathFinder createPathFinder(int range) { + this.nodeEvaluator = new WalkNodeEvaluator(); + this.nodeEvaluator.setCanPassDoors(true); +- return new PathFinder(this.nodeEvaluator, range); ++ return new PathFinder(this.nodeEvaluator, range, nodeEvaluatorGenerator); // petal + } + + @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 3f672d7c2377fca16a6d8d31cf7aaae4f009fdce..bcc368d3d3adf73ff9ff2395d2f2b321c6134efd 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 +@@ -151,6 +151,9 @@ public abstract class PathNavigation { + return null; + } else if (!this.canUpdatePath()) { + return null; ++ } else if (this.path instanceof host.bloom.pathfinding.AsyncPath asyncPath && !asyncPath.isProcessed() && asyncPath.hasSameProcessingPositions(positions)) { // petal start - catch early if it's still processing these positions let it keep processing ++ return this.path; ++ // petal end + } else if (this.path != null && !this.path.isDone() && positions.contains(this.targetPos)) { + return this.path; + } else { +@@ -177,11 +180,20 @@ public abstract class PathNavigation { + 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(); +- if (path != null && path.getTarget() != null) { +- this.targetPos = path.getTarget(); +- this.reachRange = distance; +- this.resetStuckTimeout(); +- } ++ ++ if (!positions.isEmpty()) this.targetPos = positions.iterator().next(); // petal - assign early a target position. most calls will only have 1 position ++ ++ // petal start - async ++ host.bloom.pathfinding.AsyncPathProcessor.awaitProcessing(path, processedPath -> { ++ if (processedPath != this.path) return; // petal - check that processing didn't take so long that we calculated a new path ++ ++ if (processedPath != null && processedPath.getTarget() != null) { ++ this.targetPos = processedPath.getTarget(); ++ this.reachRange = distance; ++ this.resetStuckTimeout(); ++ } ++ }); ++ // petal end + + return path; + } +@@ -228,8 +240,8 @@ public abstract class PathNavigation { + if (this.isDone()) { + return false; + } else { +- this.trimPath(); +- if (this.path.getNodeCount() <= 0) { ++ if (path.isProcessed()) this.trimPath(); // petal - only trim if processed ++ if (path.isProcessed() && this.path.getNodeCount() <= 0) { // petal - only check node count if processed + return false; + } else { + this.speedModifier = speed; +@@ -253,6 +265,8 @@ public abstract class PathNavigation { + this.recomputePath(); + } + ++ if (this.path != null && !this.path.isProcessed()) return; // petal - skip pathfinding if we're still processing ++ + if (!this.isDone()) { + if (this.canUpdatePath()) { + this.followThePath(); +@@ -278,6 +292,7 @@ public abstract class PathNavigation { + } + + protected void followThePath() { ++ if (!this.path.isProcessed()) return; // petal + Vec3 vec3 = this.getTempMobPos(); + this.maxDistanceToWaypoint = this.mob.getBbWidth() > 0.75F ? this.mob.getBbWidth() / 2.0F : 0.75F - this.mob.getBbWidth() / 2.0F; + Vec3i vec3i = this.path.getNextNodePos(); +@@ -440,7 +455,7 @@ public abstract class PathNavigation { + // Paper start + public boolean isViableForPathRecalculationChecking() { + return !this.needsPathRecalculation() && +- (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0); ++ (this.path != null && this.path.isProcessed() && !this.path.isDone() && this.path.getNodeCount() != 0); + } + // Paper end + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +index 8db20db72cd51046213625fac46c35854c59ec5d..11b386697279333ffd5f3abc9e1dbc9c19711764 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +@@ -57,20 +57,28 @@ public class NearestBedSensor extends Sensor { + java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); + // don't ask me why it's unbounded. ask mojang. + io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); +- Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); ++ ++ // petal start - await on path async ++ Path possiblePath = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); + // Paper end - optimise POI access +- if (path != null && path.canReach()) { ++ // petal - wait on the path to be processed ++ host.bloom.pathfinding.AsyncPathProcessor.awaitProcessing(possiblePath, path -> { ++ // petal - readd canReach check ++ if (path == null || !path.canReach()) { ++ this.batchCache.long2LongEntrySet().removeIf((entry) -> { ++ return entry.getLongValue() < this.lastUpdate; ++ }); ++ return; ++ } ++ + BlockPos blockPos = path.getTarget(); + Optional> optional = poiManager.getType(blockPos); + if (optional.isPresent()) { + entity.getBrain().setMemory(MemoryModuleType.NEAREST_BED, blockPos); + } +- } else if (this.triedCount < 5) { +- this.batchCache.long2LongEntrySet().removeIf((entry) -> { +- return entry.getLongValue() < this.lastUpdate; +- }); +- } + ++ }); ++ // petal end + } + } + } +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 a7c0c2a8b9b5c1336ce33418e24e0bfd77cec5b0..227a8d1381277ad2b8d545e9e2b951ff5f7a2e36 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java +@@ -1143,7 +1143,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } else { + Bee.this.pathfindRandomlyTowards(Bee.this.hivePos); + } +- } else { ++ } else if (navigation.getPath() != null && navigation.getPath().isProcessed()) { // petal - check processing + boolean flag = this.pathfindDirectlyTowards(Bee.this.hivePos); + + if (!flag) { +@@ -1205,7 +1205,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } else { + Path pathentity = Bee.this.navigation.getPath(); + +- return pathentity != null && pathentity.getTarget().equals(pos) && pathentity.canReach() && pathentity.isDone(); ++ return pathentity != null && pathentity.isProcessed() && pathentity.getTarget().equals(pos) && pathentity.canReach() && pathentity.isDone(); // petal - ensure path is processed + } + } + } +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 8210aa958b8bc7d36f2959d261a750c444017fec..1ddb90fbd3ace1345f77b37fc517f924819f6d7a 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 +@@ -464,6 +464,14 @@ public class Frog extends Animal { + } + + static class FrogPathNavigation extends AmphibiousPathNavigation { ++ // petal start ++ private static final host.bloom.pathfinding.NodeEvaluatorGenerator nodeEvaluatorGenerator = () -> { ++ var nodeEvaluator = new Frog.FrogNodeEvaluator(true); ++ nodeEvaluator.setCanPassDoors(true); ++ return nodeEvaluator; ++ }; ++ // petal end ++ + FrogPathNavigation(Frog frog, Level world) { + super(frog, world); + } +@@ -472,7 +480,7 @@ public class Frog extends Animal { + protected PathFinder createPathFinder(int range) { + this.nodeEvaluator = new Frog.FrogNodeEvaluator(true); + this.nodeEvaluator.setCanPassDoors(true); +- return new PathFinder(this.nodeEvaluator, range); ++ return new PathFinder(this.nodeEvaluator, range, nodeEvaluatorGenerator); // petal + } + } + } +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 68e31cf561f3d76bce6fa4324a75594c776f8964..6ce824a1f74163f60fc3b222f3aaf59cbbe5c857 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Drowned.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +@@ -288,7 +288,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + protected boolean closeToNextPos() { + Path pathentity = this.getNavigation().getPath(); + +- if (pathentity != null) { ++ if (pathentity != null && pathentity.isProcessed()) { // petal - ensure path is processed + BlockPos blockposition = pathentity.getTarget(); + + if (blockposition != null) { +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/Path.java b/src/main/java/net/minecraft/world/level/pathfinder/Path.java +index 2a335f277bd0e4b8ad0f60d8226eb8aaa80a871f..527f5fb55b596b44c7418a6f70e7243432c160dd 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/Path.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/Path.java +@@ -30,6 +30,17 @@ public class Path { + this.reached = reachesTarget; + } + ++ // petal start ++ /** ++ * checks if the path is completely processed in the case of it being computed async ++ * ++ * @return true if the path is processed ++ */ ++ public boolean isProcessed() { ++ return true; ++ } ++ // petal end ++ + public void advance() { + ++this.nextNodeIndex; + } +@@ -104,6 +115,8 @@ public class Path { + } + + public boolean sameAs(@Nullable Path o) { ++ if (o == this) return true; // petal - short circuit ++ + if (o == null) { + return false; + } else if (o.nodes.size() != this.nodes.size()) { +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..57bac2a4a8b2e4249daf3905dfa035592b3ef189 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +@@ -25,36 +25,73 @@ public class PathFinder { + private static final boolean DEBUG = false; + private final BinaryHeap openSet = new BinaryHeap(); + +- public PathFinder(NodeEvaluator pathNodeMaker, int range) { ++ private final @Nullable host.bloom.pathfinding.NodeEvaluatorGenerator nodeEvaluatorGenerator; // petal - we use this later to generate an evaluator ++ ++ // petal start - add nodeEvaluatorGenerator as optional param ++ public PathFinder(NodeEvaluator pathNodeMaker, int range, @Nullable host.bloom.pathfinding.NodeEvaluatorGenerator nodeEvaluatorGenerator) { + this.nodeEvaluator = pathNodeMaker; + this.maxVisitedNodes = range; ++ this.nodeEvaluatorGenerator = nodeEvaluatorGenerator; ++ } ++ ++ public PathFinder(NodeEvaluator pathNodeMaker, int range) { ++ this(pathNodeMaker, range, null); + } ++ // petal end + + @Nullable + public Path findPath(PathNavigationRegion world, Mob mob, Set positions, float followRange, int distance, float rangeMultiplier) { +- this.openSet.clear(); +- this.nodeEvaluator.prepare(world, mob); +- Node node = this.nodeEvaluator.getStart(); ++ //this.openSet.clear(); // petal - it's always cleared in processPath ++ // petal start - use a generated evaluator if we have one otherwise run sync ++ var nodeEvaluator = this.nodeEvaluatorGenerator == null ? this.nodeEvaluator : host.bloom.pathfinding.NodeEvaluatorCache.takeNodeEvaluator(this.nodeEvaluatorGenerator); ++ nodeEvaluator.prepare(world, mob); ++ Node node = nodeEvaluator.getStart(); + if (node == null) { + return null; + } else { + // Paper start - remove streams - and optimize collection + List> map = Lists.newArrayList(); + for (BlockPos pos : positions) { +- map.add(new java.util.AbstractMap.SimpleEntry<>(this.nodeEvaluator.getGoal(pos.getX(), pos.getY(), pos.getZ()), pos)); ++ map.add(new java.util.AbstractMap.SimpleEntry<>(nodeEvaluator.getGoal(pos.getX(), pos.getY(), pos.getZ()), pos)); + } + // Paper end +- Path path = this.findPath(world.getProfiler(), node, map, followRange, distance, rangeMultiplier); +- this.nodeEvaluator.done(); +- return path; ++ ++ // petal start ++ if (this.nodeEvaluatorGenerator == null) { ++ // run sync :( ++ return this.findPath(world.getProfiler(), node, map, followRange, distance, rangeMultiplier); ++ } ++ ++ return new host.bloom.pathfinding.AsyncPath(Lists.newArrayList(), positions, () -> { ++ try { ++ return this.processPath(nodeEvaluator, node, map, followRange, distance, rangeMultiplier); ++ } finally { ++ nodeEvaluator.done(); ++ host.bloom.pathfinding.NodeEvaluatorCache.returnNodeEvaluator(nodeEvaluator); ++ } ++ }); ++ // petal end + } + } + +- @Nullable ++ // petal start - split pathfinding into the original sync method for compat and processing for delaying + // Paper start - optimize collection + private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { ++ // readd the profiler code for sync + profiler.push("find_path"); + profiler.markForCharting(MetricCategory.PATH_FINDING); ++ ++ try { ++ return this.processPath(this.nodeEvaluator, startNode, positions, followRange, distance, rangeMultiplier); ++ } finally { ++ this.nodeEvaluator.done(); ++ } ++ } ++ // petal end ++ ++ private synchronized @org.jetbrains.annotations.NotNull Path processPath(NodeEvaluator nodeEvaluator, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { // petal - sync to only use the caching functions in this class on a single thread ++ org.apache.commons.lang3.Validate.isTrue(!positions.isEmpty()); // ensure that we have at least one position, which means we'll always return a path ++ + // Set set = positions.keySet(); + startNode.g = 0.0F; + startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection +@@ -91,7 +128,7 @@ public class PathFinder { + } + + if (!(node.distanceTo(startNode) >= followRange)) { +- int k = this.nodeEvaluator.getNeighbors(this.neighbors, node); ++ int k = nodeEvaluator.getNeighbors(this.neighbors, node); + + for(int l = 0; l < k; ++l) { + Node node2 = this.neighbors[l]; +@@ -123,9 +160,14 @@ public class PathFinder { + if (best == null || comparator.compare(path, best) < 0) + best = path; + } ++ ++ // petal - ignore this warning, we know that the above loop always runs at least once since positions is not empty ++ //noinspection ConstantConditions + return best; + // Paper end ++ // petal end + } ++ // petal end + + protected float distance(Node a, Node b) { + return a.distanceTo(b); diff --git a/patches/server/0003-feat-multithreaded-tracker.patch b/patches/server/0003-feat-multithreaded-tracker.patch new file mode 100644 index 0000000..92d7855 --- /dev/null +++ b/patches/server/0003-feat-multithreaded-tracker.patch @@ -0,0 +1,279 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: peaches94 +Date: Sat, 2 Jul 2022 00:35:56 -0500 +Subject: [PATCH] feat: multithreaded tracker + +Co-authored-by: Paul Sauve + +based off the airplane multithreaded tracker this patch properly handles +concurrent accesses everywhere, as well as being much simpler to maintain + +some things are too unsafe to run off the main thread so we don't attempt to do +that. this multithreaded tracker remains accurate, non-breaking and fast + +diff --git a/src/main/java/host/bloom/tracker/MultithreadedTracker.java b/src/main/java/host/bloom/tracker/MultithreadedTracker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e5f3cf027534fe87e8b7d5e9fd14604b2327c701 +--- /dev/null ++++ b/src/main/java/host/bloom/tracker/MultithreadedTracker.java +@@ -0,0 +1,125 @@ ++package host.bloom.tracker; ++ ++import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet; ++import io.papermc.paper.world.ChunkEntitySlices; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.chunk.LevelChunk; ++ ++import java.util.concurrent.ConcurrentLinkedQueue; ++import java.util.concurrent.Executor; ++import java.util.concurrent.Executors; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++public class MultithreadedTracker { ++ ++ private static final int parallelism = Math.max(4, Runtime.getRuntime().availableProcessors()); ++ private static final Executor trackerExecutor = Executors.newFixedThreadPool(parallelism, new ThreadFactoryBuilder() ++ .setNameFormat("petal-tracker-%d") ++ .setPriority(Thread.NORM_PRIORITY - 2) ++ .build()); ++ ++ private final IteratorSafeOrderedReferenceSet entityTickingChunks; ++ private final AtomicInteger taskIndex = new AtomicInteger(); ++ ++ private final ConcurrentLinkedQueue mainThreadTasks; ++ private final AtomicInteger finishedTasks = new AtomicInteger(); ++ ++ public MultithreadedTracker(IteratorSafeOrderedReferenceSet entityTickingChunks, ConcurrentLinkedQueue mainThreadTasks) { ++ this.entityTickingChunks = entityTickingChunks; ++ this.mainThreadTasks = mainThreadTasks; ++ } ++ ++ public void tick() { ++ int iterator = this.entityTickingChunks.createRawIterator(); ++ ++ if (iterator == -1) { ++ return; ++ } ++ ++ try { ++ this.taskIndex.set(iterator); ++ this.finishedTasks.set(0); ++ ++ for (int i = 0; i < parallelism; i++) { ++ trackerExecutor.execute(this::run); ++ } ++ ++ while (this.taskIndex.get() < this.entityTickingChunks.getListSize()) { ++ this.runMainThreadTasks(); ++ this.handleTask(); // assist ++ } ++ ++ while (this.finishedTasks.get() != parallelism) { ++ this.runMainThreadTasks(); ++ } ++ ++ this.runMainThreadTasks(); // finish any remaining tasks ++ } finally { ++ this.entityTickingChunks.finishRawIterator(); ++ } ++ } ++ ++ private void runMainThreadTasks() { ++ try { ++ Runnable task; ++ while ((task = this.mainThreadTasks.poll()) != null) { ++ task.run(); ++ } ++ } catch (Throwable throwable) { ++ MinecraftServer.LOGGER.warn("Tasks failed while ticking track queue", throwable); ++ } ++ } ++ ++ private void run() { ++ try { ++ while (handleTask()) ; ++ } finally { ++ this.finishedTasks.incrementAndGet(); ++ } ++ } ++ ++ private boolean handleTask() { ++ int index; ++ while ((index = this.taskIndex.getAndIncrement()) < this.entityTickingChunks.getListSize()) { ++ LevelChunk chunk = this.entityTickingChunks.rawGet(index); ++ if (chunk != null) { ++ try { ++ this.processChunk(chunk); ++ } catch (Throwable throwable) { ++ MinecraftServer.LOGGER.warn("Ticking tracker failed", throwable); ++ } ++ ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ ++ private void processChunk(LevelChunk chunk) { ++ final ChunkEntitySlices entitySlices = chunk.level.entityManager.entitySliceManager.getChunk(chunk.locX, chunk.locZ); ++ if (entitySlices == null) { ++ return; ++ } ++ ++ final Entity[] rawEntities = entitySlices.entities.getRawData(); ++ final ChunkMap chunkMap = chunk.level.chunkSource.chunkMap; ++ ++ for (int i = 0; i < rawEntities.length; i++) { ++ Entity entity = rawEntities[i]; ++ if (entity != null) { ++ ChunkMap.TrackedEntity entityTracker = chunkMap.entityMap.get(entity.getId()); ++ if (entityTracker != null) { ++ entityTracker.updatePlayers(entityTracker.entity.getPlayersInTrackRange()); ++ ++ // run this on the main thread but queue it up here so we can run it while processing tracking at the same time ++ this.mainThreadTasks.offer(entityTracker.serverEntity::sendChanges); ++ } ++ } ++ } ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java +index 0fd814f1d65c111266a2b20f86561839a4cef755..169ac3ad1b1e8e3e1874ada2471e478233c6ada7 100644 +--- a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java ++++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java +@@ -15,7 +15,7 @@ public final class IteratorSafeOrderedReferenceSet { + + /* list impl */ + protected E[] listElements; +- protected int listSize; ++ protected int listSize; public int getListSize() { return this.listSize; } // petal - expose listSize + + protected final double maxFragFactor; + +diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +index 47b5f75d9f27cf3ab947fd1f69cbd609fb9f2749..85882eeb86d7b74db0219aa65783946d8083885d 100644 +--- a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java ++++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +@@ -27,7 +27,7 @@ public final class ChunkEntitySlices { + protected final EntityCollectionBySection allEntities; + protected final EntityCollectionBySection hardCollidingEntities; + protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; +- protected final EntityList entities = new EntityList(); ++ public final EntityList entities = new EntityList(); + + public ChunkHolder.FullChunkStatus status; + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 2daef87d5c1de8e6f9a8c7c6a7753d980b05178f..345f4ddee48da851cbabd9de878642fc57a2d7b0 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -2082,8 +2082,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + entity.tracker = null; // Paper - We're no longer tracked + } + ++ // petal start - multithreaded tracker ++ private @Nullable host.bloom.tracker.MultithreadedTracker multithreadedTracker; ++ private final java.util.concurrent.ConcurrentLinkedQueue trackerMainThreadTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); ++ ++ public void runOnTrackerMainThread(final Runnable runnable) { ++ this.trackerMainThreadTasks.add(runnable); ++ } ++ + // Paper start - optimised tracker + private final void processTrackQueue() { ++ if (true) { ++ if (this.multithreadedTracker == null) { ++ this.multithreadedTracker = new host.bloom.tracker.MultithreadedTracker(this.level.chunkSource.entityTickingChunks, this.trackerMainThreadTasks); ++ } ++ ++ this.multithreadedTracker.tick(); ++ return; ++ } ++ // petal end ++ + this.level.timings.tracker1.startTiming(); + try { + for (TrackedEntity tracker : this.entityMap.values()) { +@@ -2276,10 +2294,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public class TrackedEntity { + + public final ServerEntity serverEntity; // Purpur -> package -> public +- final Entity entity; ++ public final Entity entity; // petal -> public + private final int range; + SectionPos lastSectionPos; +- public final Set seenBy = new ReferenceOpenHashSet<>(); // Paper - optimise map impl ++ public final Set seenBy = it.unimi.dsi.fastutil.objects.ReferenceSets.synchronize(new ReferenceOpenHashSet<>()); // Paper - optimise map impl // petal - sync + + public TrackedEntity(Entity entity, int i, int j, boolean flag) { + this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit +@@ -2291,7 +2309,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper start - use distance map to optimise tracker + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet lastTrackerCandidates; + +- final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { ++ public final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { // petal -> public + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; + this.lastTrackerCandidates = newTrackerCandidates; + +@@ -2363,7 +2381,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void removePlayer(ServerPlayer player) { +- org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot ++ //org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot // petal - we can remove async too + if (this.seenBy.remove(player.connection)) { + this.serverEntity.removePairing(player); + } +@@ -2371,7 +2389,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void updatePlayer(ServerPlayer player) { +- org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot ++ //org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot // petal - we can update async + if (player != this.entity) { + // Paper start - remove allocation of Vec3D here + // Vec3 vec3d = player.position().subtract(this.entity.position()); +diff --git a/src/main/java/net/minecraft/server/level/ServerBossEvent.java b/src/main/java/net/minecraft/server/level/ServerBossEvent.java +index ca42c2642a729b90d22b968af7258f3aee72e14b..40261b80d947a6be43465013fae5532197cfe721 100644 +--- a/src/main/java/net/minecraft/server/level/ServerBossEvent.java ++++ b/src/main/java/net/minecraft/server/level/ServerBossEvent.java +@@ -13,7 +13,7 @@ import net.minecraft.util.Mth; + import net.minecraft.world.BossEvent; + + public class ServerBossEvent extends BossEvent { +- private final Set players = Sets.newHashSet(); ++ private final Set players = Sets.newConcurrentHashSet(); // petal - players can be removed in async tracking + private final Set unmodifiablePlayers = Collections.unmodifiableSet(this.players); + public boolean visible = true; + +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 3441339e1ba5efb0e25c16fa13cb65d2fbdafc42..9d0cb0468800cbd88f43424466e9a29a05d3945d 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -249,14 +249,18 @@ public class ServerEntity { + + public void removePairing(ServerPlayer player) { + this.entity.stopSeenByPlayer(player); +- player.connection.send(new ClientboundRemoveEntitiesPacket(new int[]{this.entity.getId()})); ++ // petal start - ensure main thread ++ ((ServerLevel) this.entity.level).chunkSource.chunkMap.runOnTrackerMainThread(() -> ++ player.connection.send(new ClientboundRemoveEntitiesPacket(new int[]{this.entity.getId()})) ++ ); ++ // petal end + } + + public void addPairing(ServerPlayer player) { + ServerGamePacketListenerImpl playerconnection = player.connection; + + Objects.requireNonNull(player.connection); +- this.sendPairingData(playerconnection::send, player); // CraftBukkit - add player ++ ((ServerLevel) this.entity.level).chunkSource.chunkMap.runOnTrackerMainThread(() -> this.sendPairingData(playerconnection::send, player)); // CraftBukkit - add player // petal - main thread + this.entity.startSeenByPlayer(player); + } + diff --git a/patches/server/0004-feat-reduce-work-done-by-game-event-system.patch b/patches/server/0004-feat-reduce-work-done-by-game-event-system.patch new file mode 100644 index 0000000..9966323 --- /dev/null +++ b/patches/server/0004-feat-reduce-work-done-by-game-event-system.patch @@ -0,0 +1,132 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: peaches94 +Date: Sun, 10 Jul 2022 13:29:20 -0500 +Subject: [PATCH] feat: reduce work done by game event system + +1. going into game event dispatching can be expensive so run the checks before dispatching + +2. euclideangameeventdispatcher is not used concurrently so we ban that usage for improved performance with allays + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 0d82ca8ba9f2b11213cfe1ab01dc6ef3f2f510c9..bad88fddd25a99fdbe35285f7694fed26f943296 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1672,6 +1672,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (chunk != null) { + for (int j2 = k; j2 <= j1; ++j2) { + flag |= chunk.getEventDispatcher(j2).walkListeners(event, emitterPos, emitter, (gameeventlistener, vec3d1) -> { ++ if (!gameeventlistener.listensToEvent(event, emitter)) return; // petal - if they don't listen, ignore + (gameeventlistener.handleEventsImmediately() ? list : this.gameEventMessages).add(new GameEvent.Message(event, emitterPos, emitter, gameeventlistener, vec3d1)); + }); + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +index 22c309343299e60ed8028229b7f134109001ff35..d5947d29295ddc93ba8ac1c0fc61f7badad582c4 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -85,6 +85,13 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi + } + } + ++ // petal start ++ @Override ++ public boolean listensToEvent(GameEvent gameEvent, GameEvent.Context context) { ++ return !this.isRemoved() && gameEvent == GameEvent.ENTITY_DIE && context.sourceEntity() instanceof LivingEntity; ++ } ++ // petal end ++ + public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) { + org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + blockEntity.sculkSpreader.updateCursors(world, pos, world.getRandom(), true); +diff --git a/src/main/java/net/minecraft/world/level/gameevent/EuclideanGameEventDispatcher.java b/src/main/java/net/minecraft/world/level/gameevent/EuclideanGameEventDispatcher.java +index 0dd708ebe81f73710de51215529c05ec61837dd3..f5b402efa86f824c460db8cac20c1c2b090f82d0 100644 +--- a/src/main/java/net/minecraft/world/level/gameevent/EuclideanGameEventDispatcher.java ++++ b/src/main/java/net/minecraft/world/level/gameevent/EuclideanGameEventDispatcher.java +@@ -13,8 +13,8 @@ import net.minecraft.world.phys.Vec3; + + public class EuclideanGameEventDispatcher implements GameEventDispatcher { + private final List listeners = Lists.newArrayList(); +- private final Set listenersToRemove = Sets.newHashSet(); +- private final List listenersToAdd = Lists.newArrayList(); ++ //private final Set listenersToRemove = Sets.newHashSet(); // petal - not necessary ++ //private final List listenersToAdd = Lists.newArrayList(); // petal + private boolean processing; + private final ServerLevel level; + +@@ -30,7 +30,7 @@ public class EuclideanGameEventDispatcher implements GameEventDispatcher { + @Override + public void register(GameEventListener listener) { + if (this.processing) { +- this.listenersToAdd.add(listener); ++ throw new java.util.ConcurrentModificationException(); // petal - disallow concurrent modification + } else { + this.listeners.add(listener); + } +@@ -41,7 +41,7 @@ public class EuclideanGameEventDispatcher implements GameEventDispatcher { + @Override + public void unregister(GameEventListener listener) { + if (this.processing) { +- this.listenersToRemove.add(listener); ++ throw new java.util.ConcurrentModificationException(); // petal - disallow concurrent modification + } else { + this.listeners.remove(listener); + } +@@ -58,7 +58,7 @@ public class EuclideanGameEventDispatcher implements GameEventDispatcher { + + while(iterator.hasNext()) { + GameEventListener gameEventListener = iterator.next(); +- if (this.listenersToRemove.remove(gameEventListener)) { ++ if (false) { // petal - disallow concurrent modification + iterator.remove(); + } else { + Optional optional = getPostableListenerPosition(this.level, pos, gameEventListener); +@@ -72,6 +72,8 @@ public class EuclideanGameEventDispatcher implements GameEventDispatcher { + this.processing = false; + } + ++ // petal start ++ /* + if (!this.listenersToAdd.isEmpty()) { + this.listeners.addAll(this.listenersToAdd); + this.listenersToAdd.clear(); +@@ -81,6 +83,8 @@ public class EuclideanGameEventDispatcher implements GameEventDispatcher { + this.listeners.removeAll(this.listenersToRemove); + this.listenersToRemove.clear(); + } ++ */ ++ // petal end + + return bl; + } +diff --git a/src/main/java/net/minecraft/world/level/gameevent/GameEventListener.java b/src/main/java/net/minecraft/world/level/gameevent/GameEventListener.java +index e5601afe8b739da518f36ae306f5e0cb252238f0..bc8f04424c5e8c416d6988f0e06d8cadbb400ca7 100644 +--- a/src/main/java/net/minecraft/world/level/gameevent/GameEventListener.java ++++ b/src/main/java/net/minecraft/world/level/gameevent/GameEventListener.java +@@ -12,4 +12,10 @@ public interface GameEventListener { + int getListenerRadius(); + + boolean handleGameEvent(ServerLevel world, GameEvent.Message event); ++ ++ // petal start - add check for seeing if this listener cares about an event ++ default boolean listensToEvent(net.minecraft.world.level.gameevent.GameEvent gameEvent, net.minecraft.world.level.gameevent.GameEvent.Context context) { ++ return true; ++ } ++ // petal end + } +diff --git a/src/main/java/net/minecraft/world/level/gameevent/vibrations/VibrationListener.java b/src/main/java/net/minecraft/world/level/gameevent/vibrations/VibrationListener.java +index e45f54534bbf054eaf0008546ff459d4c11ddd50..e49d0d1c2a539fcd7e75262c4010475193964287 100644 +--- a/src/main/java/net/minecraft/world/level/gameevent/vibrations/VibrationListener.java ++++ b/src/main/java/net/minecraft/world/level/gameevent/vibrations/VibrationListener.java +@@ -162,6 +162,13 @@ public class VibrationListener implements GameEventListener { + return true; + } + ++ // petal start ++ @Override ++ public boolean listensToEvent(GameEvent gameEvent, GameEvent.Context context) { ++ return this.receivingEvent == null && gameEvent.is(this.config.getListenableEvents()); ++ } ++ // petal end ++ + public interface VibrationListenerConfig { + + default TagKey getListenableEvents() { diff --git a/patches/server/0005-feat-reduce-sensor-work.patch b/patches/server/0005-feat-reduce-sensor-work.patch new file mode 100644 index 0000000..5ee57c9 --- /dev/null +++ b/patches/server/0005-feat-reduce-sensor-work.patch @@ -0,0 +1,76 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: peaches94 +Date: Sun, 10 Jul 2022 15:44:38 -0500 +Subject: [PATCH] feat: reduce sensor work + +this patch is focused around the sensors used for ai +delete the line of sight cache less often and use a faster nearby comparison + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 411593b1b105d62440d76b7bd1b8c74b701e3e75..aec8b3e33dd41978d3542634567a0ad6996f9647 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -1012,20 +1012,22 @@ public abstract class LivingEntity extends Entity { + } + + if (entity != null) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); ++ // petal start - only do itemstack lookup if we need to ++ //ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); + EntityType entitytypes = entity.getType(); + + // Purpur start +- if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL)) { ++ if (entitytypes == EntityType.SKELETON && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.SKELETON_SKULL)) { + d0 *= entity.level.purpurConfig.skeletonHeadVisibilityPercent; + } +- else if (entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD)) { ++ else if (entitytypes == EntityType.ZOMBIE && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.ZOMBIE_HEAD)) { + d0 *= entity.level.purpurConfig.zombieHeadVisibilityPercent; + } +- else if (entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { ++ else if (entitytypes == EntityType.CREEPER && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.CREEPER_HEAD)) { + d0 *= entity.level.purpurConfig.creeperHeadVisibilityPercent; + } + // Purpur end ++ // petal end + + // Purpur start + if (entity instanceof LivingEntity entityliving) { +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 34db1bd524bb97fbbe0f86b088a2ac343e730f5e..1df1e912194c4d2b934859d999e6d09e802a5978 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -878,10 +878,10 @@ public abstract class Mob extends LivingEntity { + return; + } + // Paper end ++ int i = this.level.getServer().getTickCount() + this.getId(); // petal - move up + this.level.getProfiler().push("sensing"); +- this.sensing.tick(); ++ if (i % 10 == 0) this.sensing.tick(); // petal - only refresh line of sight cache every half second + this.level.getProfiler().pop(); +- int i = this.level.getServer().getTickCount() + this.getId(); + + if (i % 2 != 0 && this.tickCount > 1) { + this.level.getProfiler().push("targetSelector"); +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +index d8cf99a3014a4b8152ae819fa663c2fdf34dce57..6cce3def9c52cb365c7e831be42eceb1e0710cb4 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +@@ -18,7 +18,14 @@ public class NearestLivingEntitySensor extends Sensor + List list = world.getEntitiesOfClass(LivingEntity.class, aABB, (e) -> { + return e != entity && e.isAlive(); + }); +- list.sort(Comparator.comparingDouble(entity::distanceToSqr)); ++ // petal start - use manual comparison because we don't need the accuracy of Double.compare ++ list.sort((e1, e2) -> { ++ double dist1 = entity.distanceToSqr(e1), dist2 = entity.distanceToSqr(e2); ++ if (dist1 < dist2) return -1; ++ if (dist1 > dist2) return 1; ++ return 0; ++ }); ++ // petal end + Brain brain = entity.getBrain(); + brain.setMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES, list); + brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, new NearestVisibleLivingEntities(entity, list)); diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c09704d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://papermc.io/repo/repository/maven-public/") + } +} + +rootProject.name = "petal" +include("petal-api", "petal-server")