diff options
Diffstat (limited to 'patches/server')
62 files changed, 47439 insertions, 0 deletions
diff --git a/patches/server/0001-Setup-Gradle-project.patch b/patches/server/0001-Setup-Gradle-project.patch new file mode 100644 index 0000000000..8cdbf973d7 --- /dev/null +++ b/patches/server/0001-Setup-Gradle-project.patch @@ -0,0 +1,770 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Kyle Wood <[email protected]> +Date: Thu, 10 Dec 2020 20:54:19 -0800 +Subject: [PATCH] Setup Gradle project + +The pom.xml file is deleted in this patch so the patch will fail to +apply if there are changes made to it from upstream - thus notifying us +that changes were made. + +diff --git a/.gitignore b/.gitignore +index 37dab9e868dbfb019c271a547d975a48ad1cb571..3811c0d849a3eb028ed1a6b7a2d4747f7f570448 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -1,3 +1,6 @@ ++.gradle/ ++build/ ++ + # Eclipse stuff + /.classpath + /.project +@@ -39,3 +42,7 @@ dependency-reduced-pom.xml + + /src/main/resources/achievement + /src/main/resources/lang ++ ++# vs code ++/.vscode ++/.factorypath +diff --git a/build.gradle.kts b/build.gradle.kts +new file mode 100644 +index 0000000000000000000000000000000000000000..9ea079d14a92367629dc8fb30d1be043c9290964 +--- /dev/null ++++ b/build.gradle.kts +@@ -0,0 +1,126 @@ ++import io.papermc.paperweight.util.* ++ ++plugins { ++ java ++ `maven-publish` ++} ++ ++dependencies { ++ implementation(project(":paper-api")) ++ implementation("jline:jline:2.12.1") ++ implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") { ++ exclude(group = "org.apache.logging.log4j", module = "log4j-api") ++ } ++ implementation("org.ow2.asm:asm-commons:9.7") ++ implementation("commons-lang:commons-lang:2.6") ++ runtimeOnly("org.xerial:sqlite-jdbc:3.46.0.0") ++ runtimeOnly("com.mysql:mysql-connector-j:8.4.0") ++ ++ runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6") ++ runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") ++ runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") ++ ++ testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") ++ testImplementation("org.hamcrest:hamcrest:2.2") ++ testImplementation("org.mockito:mockito-core:5.11.0") ++ testImplementation("org.ow2.asm:asm-tree:9.7") ++} ++ ++paperweight { ++ craftBukkitPackageVersion.set("v1_21_R1") // also needs to be updated in MappingEnvironment ++} ++ ++tasks.jar { ++ archiveClassifier.set("dev") ++ ++ manifest { ++ val git = Git(rootProject.layout.projectDirectory.path) ++ val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim() ++ val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\"" ++ val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper ++ attributes( ++ "Main-Class" to "org.bukkit.craftbukkit.Main", ++ "Implementation-Title" to "CraftBukkit", ++ "Implementation-Version" to "git-Paper-$implementationVersion", ++ "Implementation-Vendor" to date, // Paper ++ "Specification-Title" to "Bukkit", ++ "Specification-Version" to project.version, ++ "Specification-Vendor" to "Bukkit Team", ++ ) ++ for (tld in setOf("net", "com", "org")) { ++ attributes("$tld/bukkit", "Sealed" to true) ++ } ++ } ++} ++ ++publishing { ++ publications.create<MavenPublication>("maven") { ++ } ++} ++ ++tasks.test { ++ exclude("org/bukkit/craftbukkit/inventory/ItemStack*Test.class") ++ useJUnitPlatform() ++} ++ ++fun TaskContainer.registerRunTask( ++ name: String, ++ block: JavaExec.() -> Unit ++): TaskProvider<JavaExec> = register<JavaExec>(name) { ++ group = "paper" ++ mainClass.set("org.bukkit.craftbukkit.Main") ++ standardInput = System.`in` ++ workingDir = rootProject.layout.projectDirectory ++ .dir(providers.gradleProperty("paper.runWorkDir").getOrElse("run")) ++ .asFile ++ javaLauncher.set(project.javaToolchains.launcherFor { ++ languageVersion.set(JavaLanguageVersion.of(21)) ++ vendor.set(JvmVendorSpec.JETBRAINS) ++ }) ++ jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+AllowRedefinitionToAddDeleteMethods") ++ ++ if (rootProject.childProjects["test-plugin"] != null) { ++ val testPluginJar = rootProject.project(":test-plugin").tasks.jar.flatMap { it.archiveFile } ++ inputs.file(testPluginJar) ++ args("-add-plugin=${testPluginJar.get().asFile.absolutePath}") ++ } ++ ++ args("--nogui") ++ systemProperty("net.kyori.adventure.text.warnWhenLegacyFormattingDetected", true) ++ if (providers.gradleProperty("paper.runDisableWatchdog").getOrElse("false") == "true") { ++ systemProperty("disable.watchdog", true) ++ } ++ systemProperty("io.papermc.paper.suppress.sout.nags", true) ++ ++ val memoryGb = providers.gradleProperty("paper.runMemoryGb").getOrElse("2") ++ minHeapSize = "${memoryGb}G" ++ maxHeapSize = "${memoryGb}G" ++ ++ doFirst { ++ workingDir.mkdirs() ++ } ++ ++ block(this) ++} ++ ++val runtimeClasspathWithoutVanillaServer = configurations.runtimeClasspath.flatMap { it.elements } ++ .zip(configurations.vanillaServer.map { it.singleFile.absolutePath }) { runtime, vanilla -> ++ runtime.filterNot { it.asFile.absolutePath == vanilla } ++ } ++ ++tasks.registerRunTask("runServerJar") { ++ description = "Spin up a test server from the serverJar archiveFile" ++ classpath(tasks.serverJar.flatMap { it.archiveFile }) ++ classpath(runtimeClasspathWithoutVanillaServer) ++} ++ ++tasks.registerRunTask("runReobf") { ++ description = "Spin up a test server from the reobfJar output jar" ++ classpath(tasks.reobfJar.flatMap { it.outputJar }) ++ classpath(runtimeClasspathWithoutVanillaServer) ++} ++ ++tasks.registerRunTask("runDev") { ++ description = "Spin up a non-relocated Mojang-mapped test server" ++ classpath(sourceSets.main.map { it.runtimeClasspath }) ++} +diff --git a/pom.xml b/pom.xml +deleted file mode 100644 +index e6f88c20b3a3ba28b917bea9a671b2709d32b3cd..0000000000000000000000000000000000000000 +--- a/pom.xml ++++ /dev/null +@@ -1,604 +0,0 @@ +-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +- <modelVersion>4.0.0</modelVersion> +- <groupId>org.spigotmc</groupId> +- <artifactId>spigot</artifactId> +- <packaging>jar</packaging> +- <version>1.21-R0.1-SNAPSHOT</version> +- <name>Spigot</name> +- <url>https://www.spigotmc.org/</url> +- +- <parent> +- <groupId>org.spigotmc</groupId> +- <artifactId>spigot-parent</artifactId> +- <version>dev-SNAPSHOT</version> +- <relativePath>../pom.xml</relativePath> +- </parent> +- +- <properties> +- <skipTests>true</skipTests> +- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> +- <api.version>unknown</api.version> +- <bt.name>git</bt.name> +- <minecraft_version>1_21_R1</minecraft_version> +- <maven.compiler.release>21</maven.compiler.release> +- </properties> +- +- <repositories> +- <repository> +- <id>minecraft-libraries</id> +- <name>Minecraft Libraries</name> +- <url>https://libraries.minecraft.net/</url> +- </repository> +- </repositories> +- +- <dependencies> +- <dependency> +- <groupId>org.spigotmc</groupId> +- <artifactId>spigot-api</artifactId> +- <version>${project.version}</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.spigotmc</groupId> +- <artifactId>minecraft-server</artifactId> +- <version>${project.version}</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>jline</groupId> +- <artifactId>jline</artifactId> +- <version>2.12.1</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.apache.logging.log4j</groupId> +- <artifactId>log4j-iostreams</artifactId> +- <version>2.22.1</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.ow2.asm</groupId> +- <artifactId>asm-commons</artifactId> +- <version>9.7</version> +- <scope>compile</scope> +- </dependency> +- <!-- Mojang depends --> +- <dependency> +- <groupId>com.github.oshi</groupId> +- <artifactId>oshi-core</artifactId> +- <version>6.4.10</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>com.mojang</groupId> +- <artifactId>authlib</artifactId> +- <version>6.0.54</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>com.mojang</groupId> +- <artifactId>brigadier</artifactId> +- <version>1.2.9</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>com.mojang</groupId> +- <artifactId>datafixerupper</artifactId> +- <version>8.0.16</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>com.mojang</groupId> +- <artifactId>logging</artifactId> +- <version>1.2.7</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>commons-io</groupId> +- <artifactId>commons-io</artifactId> +- <version>2.15.1</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-buffer</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-codec</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-common</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-handler</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-resolver</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-transport</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-transport-classes-epoll</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-transport-native-epoll</artifactId> +- <version>4.1.97.Final</version> +- <classifier>linux-x86_64</classifier> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-transport-native-epoll</artifactId> +- <version>4.1.97.Final</version> +- <classifier>linux-aarch_64</classifier> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>io.netty</groupId> +- <artifactId>netty-transport-native-unix-common</artifactId> +- <version>4.1.97.Final</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>it.unimi.dsi</groupId> +- <artifactId>fastutil</artifactId> +- <version>8.5.12</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>net.java.dev.jna</groupId> +- <artifactId>jna</artifactId> +- <version>5.14.0</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>net.java.dev.jna</groupId> +- <artifactId>jna-platform</artifactId> +- <version>5.14.0</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>net.sf.jopt-simple</groupId> +- <artifactId>jopt-simple</artifactId> +- <version>5.0.4</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.apache.commons</groupId> +- <artifactId>commons-lang3</artifactId> +- <version>3.14.0</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.apache.logging.log4j</groupId> +- <artifactId>log4j-core</artifactId> +- <version>2.22.1</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.apache.logging.log4j</groupId> +- <artifactId>log4j-slf4j2-impl</artifactId> +- <version>2.22.1</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.slf4j</groupId> +- <artifactId>slf4j-api</artifactId> +- <version>2.0.9</version> +- <scope>compile</scope> +- </dependency> +- <dependency> +- <groupId>org.lz4</groupId> +- <artifactId>lz4-java</artifactId> +- <version>1.8.0</version> +- <scope>compile</scope> +- </dependency> +- <!-- deprecated API depend --> +- <dependency> +- <groupId>commons-lang</groupId> +- <artifactId>commons-lang</artifactId> +- <version>2.6</version> +- <scope>compile</scope> +- </dependency> +- <!-- deprecated API depend --> +- <dependency> +- <groupId>com.googlecode.json-simple</groupId> +- <artifactId>json-simple</artifactId> +- <version>1.1.1</version> +- <scope>runtime</scope> +- <exclusions> +- <exclusion> +- <groupId>junit</groupId> +- <artifactId>junit</artifactId> +- </exclusion> +- </exclusions> +- </dependency> +- <dependency> +- <groupId>org.xerial</groupId> +- <artifactId>sqlite-jdbc</artifactId> +- <version>3.46.0.0</version> +- <scope>runtime</scope> +- </dependency> +- <dependency> +- <groupId>com.mysql</groupId> +- <artifactId>mysql-connector-j</artifactId> +- <version>8.4.0</version> +- <scope>runtime</scope> +- </dependency> +- <!-- add these back in as they are not exposed by the API --> +- <dependency> +- <groupId>org.apache.maven</groupId> +- <artifactId>maven-resolver-provider</artifactId> +- <version>3.9.6</version> +- <scope>runtime</scope> +- </dependency> +- <dependency> +- <groupId>org.apache.maven.resolver</groupId> +- <artifactId>maven-resolver-connector-basic</artifactId> +- <version>1.9.18</version> +- <scope>runtime</scope> +- </dependency> +- <dependency> +- <groupId>org.apache.maven.resolver</groupId> +- <artifactId>maven-resolver-transport-http</artifactId> +- <version>1.9.18</version> +- <scope>runtime</scope> +- </dependency> +- <!-- annotations --> +- <dependency> +- <groupId>org.jetbrains</groupId> +- <artifactId>annotations-java5</artifactId> +- <version>24.1.0</version> +- <scope>provided</scope> +- </dependency> +- <!-- testing --> +- <dependency> +- <groupId>org.junit.jupiter</groupId> +- <artifactId>junit-jupiter</artifactId> +- <version>5.10.2</version> +- <scope>test</scope> +- </dependency> +- <dependency> +- <groupId>org.hamcrest</groupId> +- <artifactId>hamcrest</artifactId> +- <version>2.2</version> +- <scope>test</scope> +- </dependency> +- <dependency> +- <groupId>org.mockito</groupId> +- <artifactId>mockito-core</artifactId> +- <version>5.11.0</version> +- <scope>test</scope> +- </dependency> +- <dependency> +- <groupId>org.ow2.asm</groupId> +- <artifactId>asm-tree</artifactId> +- <version>9.7</version> +- <scope>test</scope> +- </dependency> +- </dependencies> +- +- <!-- This builds a completely 'ready to start' jar with all dependencies inside --> +- <build> +- <plugins> +- <plugin> +- <groupId>net.md-5</groupId> +- <artifactId>scriptus</artifactId> +- <version>0.5.0</version> +- <executions> +- <execution> +- <id>ex-spigot</id> +- <configuration> +- <format>${bt.name}-Spigot-%s</format> +- <scmDirectory>../</scmDirectory> +- <descriptionProperty>spigot.desc</descriptionProperty> +- </configuration> +- <phase>initialize</phase> +- <goals> +- <goal>describe</goal> +- </goals> +- </execution> +- <execution> +- <id>ex-craftbukkit</id> +- <configuration> +- <format>-%s</format> +- <scmDirectory>../../CraftBukkit</scmDirectory> +- <descriptionProperty>craftbukkit.desc</descriptionProperty> +- </configuration> +- <phase>initialize</phase> +- <goals> +- <goal>describe</goal> +- </goals> +- </execution> +- </executions> +- </plugin> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-clean-plugin</artifactId> +- <version>3.2.0</version> +- <executions> +- <execution> +- <phase>initialize</phase> +- <goals> +- <goal>clean</goal> +- </goals> +- </execution> +- </executions> +- </plugin> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-jar-plugin</artifactId> +- <version>3.4.1</version> +- <configuration> +- <archive> +- <manifest> +- <addDefaultEntries>false</addDefaultEntries> +- </manifest> +- <manifestEntries> +- <Main-Class>org.bukkit.craftbukkit.Main</Main-Class> +- <Implementation-Title>CraftBukkit</Implementation-Title> +- <Implementation-Version>${spigot.desc}${craftbukkit.desc}</Implementation-Version> +- <Implementation-Vendor>${project.build.outputTimestamp}</Implementation-Vendor> +- <Specification-Title>Bukkit</Specification-Title> +- <Specification-Version>${api.version}</Specification-Version> +- <Specification-Vendor>Bukkit Team</Specification-Vendor> +- <Multi-Release>true</Multi-Release> +- </manifestEntries> +- <manifestSections> +- <manifestSection> +- <name>net/bukkit/</name> +- <manifestEntries> +- <Sealed>true</Sealed> +- </manifestEntries> +- </manifestSection> +- <manifestSection> +- <name>com/bukkit/</name> +- <manifestEntries> +- <Sealed>true</Sealed> +- </manifestEntries> +- </manifestSection> +- <manifestSection> +- <name>org/bukkit/</name> +- <manifestEntries> +- <Sealed>true</Sealed> +- </manifestEntries> +- </manifestSection> +- </manifestSections> +- </archive> +- </configuration> +- </plugin> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-shade-plugin</artifactId> +- <version>3.5.3</version> +- <executions> +- <execution> +- <phase>package</phase> +- <goals> +- <goal>shade</goal> +- </goals> +- <configuration> +- <createSourcesJar>${shadeSourcesJar}</createSourcesJar> +- <artifactSet> +- <includes> +- <include>org.spigotmc:minecraft-server</include> +- </includes> +- </artifactSet> +- <relocations> +- <relocation> +- <pattern>org.bukkit.craftbukkit</pattern> +- <shadedPattern>org.bukkit.craftbukkit.v${minecraft_version}</shadedPattern> +- <excludes> +- <exclude>org.bukkit.craftbukkit.bootstrap.*</exclude> +- <exclude>org.bukkit.craftbukkit.Main*</exclude> +- </excludes> +- </relocation> +- </relocations> +- </configuration> +- </execution> +- </executions> +- </plugin> +- <plugin> +- <groupId>net.md-5</groupId> +- <artifactId>specialsource-maven-plugin</artifactId> +- <version>2.0.3</version> +- <executions> +- <execution> +- <phase>package</phase> +- <goals> +- <goal>remap</goal> +- </goals> +- <id>remap-members</id> +- <configuration> +- <useProjectDependencies>false</useProjectDependencies> +- <logFile>${project.build.directory}/server.txt</logFile> +- <srgIn>org.spigotmc:minecraft-server:${project.version}:csrg:maps-spigot-members</srgIn> +- <reverse>true</reverse> +- </configuration> +- </execution> +- </executions> +- </plugin> +- <plugin> +- <groupId>net.nicoulaj.maven.plugins</groupId> +- <artifactId>checksum-maven-plugin</artifactId> +- <version>1.11</version> +- <executions> +- <execution> +- <phase>package</phase> +- <goals> +- <goal>artifacts</goal> +- <goal>dependencies</goal> +- </goals> +- <configuration> +- <algorithms> +- <algorithm>SHA-256</algorithm> +- </algorithms> +- <quiet>true</quiet> +- <scopes> +- <scope>compile</scope> +- <scope>runtime</scope> +- </scopes> +- <shasumSummary>true</shasumSummary> +- <transitive>true</transitive> +- </configuration> +- </execution> +- </executions> +- </plugin> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-assembly-plugin</artifactId> +- <version>3.7.1</version> +- <executions> +- <execution> +- <phase>package</phase> +- <goals> +- <goal>single</goal> +- </goals> +- <configuration> +- <archive> +- <manifest> +- <addDefaultEntries>false</addDefaultEntries> +- </manifest> +- <manifestEntries> +- <Main-Class>org.bukkit.craftbukkit.bootstrap.Main</Main-Class> +- </manifestEntries> +- </archive> +- <attach>false</attach> +- <descriptors> +- <descriptor>${project.basedir}/src/assembly/bootstrap.xml</descriptor> +- </descriptors> +- </configuration> +- </execution> +- </executions> +- </plugin> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-compiler-plugin</artifactId> +- <version>3.13.0</version> +- <configuration> +- <!-- default changed with version 3.11.0 --> +- <showWarnings>false</showWarnings> +- </configuration> +- </plugin> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-surefire-plugin</artifactId> +- <version>3.2.5</version> +- <configuration> +- <workingDirectory>${basedir}/target/test-server</workingDirectory> +- <excludes> +- <exclude>org/bukkit/craftbukkit/inventory/ItemStack*Test.java</exclude> +- </excludes> +- </configuration> +- </plugin> +- </plugins> +- </build> +- +- <profiles> +- <profile> +- <id>shadeSourcesJar</id> +- <properties> +- <shadeSourcesJar>true</shadeSourcesJar> +- <shadeSourcesContent>true</shadeSourcesContent> +- </properties> +- </profile> +- <profile> +- <id>development</id> +- <properties> +- <skipTests>false</skipTests> +- </properties> +- <build> +- <plugins> +- <plugin> +- <groupId>org.apache.maven.plugins</groupId> +- <artifactId>maven-checkstyle-plugin</artifactId> +- <version>3.3.1</version> +- <executions> +- <execution> +- <phase>test-compile</phase> +- <goals> +- <goal>check</goal> +- </goals> +- </execution> +- </executions> +- <configuration> +- <configLocation>checkstyle.xml</configLocation> +- <includeTestSourceDirectory>true</includeTestSourceDirectory> +- </configuration> +- <dependencies> +- <dependency> +- <groupId>com.puppycrawl.tools</groupId> +- <artifactId>checkstyle</artifactId> +- <version>8.45.1</version> +- </dependency> +- </dependencies> +- </plugin> +- </plugins> +- </build> +- </profile> +- <profile> +- <id>remapped</id> +- <build> +- <plugins> +- <plugin> +- <groupId>net.md-5</groupId> +- <artifactId>specialsource-maven-plugin</artifactId> +- <executions> +- <execution> +- <phase>verify</phase> +- <goals> +- <goal>remap</goal> +- </goals> +- <id>remap-obf</id> +- <configuration> +- <useProjectDependencies>false</useProjectDependencies> +- <srgIn>org.spigotmc:minecraft-server:${project.version}:csrg:maps-spigot</srgIn> +- <reverse>true</reverse> +- <remappedArtifactAttached>true</remappedArtifactAttached> +- <remappedClassifierName>remapped-obf</remappedClassifierName> +- </configuration> +- </execution> +- <execution> +- <phase>verify</phase> +- <goals> +- <goal>remap</goal> +- </goals> +- <id>remap-mojang</id> +- <configuration> +- <useProjectDependencies>false</useProjectDependencies> +- <inputFile>${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf.jar</inputFile> +- <srgIn>org.spigotmc:minecraft-server:${project.version}:txt:maps-mojang</srgIn> +- <remappedArtifactAttached>true</remappedArtifactAttached> +- <remappedClassifierName>remapped-mojang</remappedClassifierName> +- </configuration> +- </execution> +- </executions> +- </plugin> +- </plugins> +- </build> +- </profile> +- </profiles> +-</project> diff --git a/patches/server/0002-Remap-fixes.patch b/patches/server/0002-Remap-fixes.patch new file mode 100644 index 0000000000..8a10b11f33 --- /dev/null +++ b/patches/server/0002-Remap-fixes.patch @@ -0,0 +1,211 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Kyle Wood <[email protected]> +Date: Fri, 11 Jun 2021 05:25:03 -0500 +Subject: [PATCH] Remap fixes + + +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index 766006fe7ff965d6ca17df1df457ecc629b3be18..0ef1a7b2a17e81144d594f29f7b5e54d5038dcf4 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -323,9 +323,11 @@ public class BlockPos extends Vec3i { + + public static Iterable<BlockPos> withinManhattan(BlockPos center, int rangeX, int rangeY, int rangeZ) { + int i = rangeX + rangeY + rangeZ; +- int j = center.getX(); +- int k = center.getY(); +- int l = center.getZ(); ++ // Paper start - rename variables to fix conflict with anonymous class (remap fix) ++ int centerX = center.getX(); ++ int centerY = center.getY(); ++ int centerZ = center.getZ(); ++ // Paper end + return () -> new AbstractIterator<BlockPos>() { + private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + private int currentDepth; +@@ -339,7 +341,7 @@ public class BlockPos extends Vec3i { + protected BlockPos computeNext() { + if (this.zMirror) { + this.zMirror = false; +- this.cursor.setZ(l - (this.cursor.getZ() - l)); ++ this.cursor.setZ(centerZ - (this.cursor.getZ() - centerZ)); // Paper - remap fix + return this.cursor; + } else { + BlockPos blockPos; +@@ -365,7 +367,7 @@ public class BlockPos extends Vec3i { + int k = this.currentDepth - Math.abs(i) - Math.abs(j); + if (k <= rangeZ) { + this.zMirror = k != 0; +- blockPos = this.cursor.set(j + i, k + j, l + k); ++ blockPos = this.cursor.set(centerX + i, centerY + j, centerZ + k); // Paper - remap fix + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java +index 7344cff32fa6fe3dedb74ed98126072c55b0abd2..d98b28e9488a5a7736719cf656736bb026ec8c7e 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java +@@ -169,10 +169,10 @@ public class BehaviorUtils { + + return optional.map((uuid) -> { + return ((ServerLevel) entity.level()).getEntity(uuid); +- }).map((entity) -> { ++ }).map((entity1) -> { // Paper - remap fix + LivingEntity entityliving1; + +- if (entity instanceof LivingEntity entityliving2) { ++ if (entity1 instanceof LivingEntity entityliving2) { // Paper - remap fix + entityliving1 = entityliving2; + } else { + entityliving1 = null; +diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java +index 9051f0186c09eeb8ecccf62b0116f6da1800a1df..b231f90317fe7df9133674b12d47873520b481cb 100644 +--- a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java ++++ b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java +@@ -259,8 +259,8 @@ public class LootTable { + + public static class Builder implements FunctionUserBuilder<LootTable.Builder> { + +- private final Builder<LootPool> pools = ImmutableList.builder(); +- private final Builder<LootItemFunction> functions = ImmutableList.builder(); ++ private final ImmutableList.Builder<LootPool> pools = ImmutableList.builder(); ++ private final ImmutableList.Builder<LootItemFunction> functions = ImmutableList.builder(); + private LootContextParamSet paramSet; + private Optional<ResourceLocation> randomSequence; + +diff --git a/src/test/java/org/bukkit/DyeColorsTest.java b/src/test/java/org/bukkit/DyeColorsTest.java +index dfc3e4f5a5cfee0456097a44d579587719a231a7..4e1b6c009fe82e970a32dbdb564763c3f9081b2c 100644 +--- a/src/test/java/org/bukkit/DyeColorsTest.java ++++ b/src/test/java/org/bukkit/DyeColorsTest.java +@@ -3,7 +3,6 @@ package org.bukkit; + import static org.bukkit.support.MatcherAssert.*; + import static org.hamcrest.Matchers.*; + +-import net.minecraft.world.item.DyeColor; + import org.bukkit.support.AbstractTestingBase; + import org.junit.jupiter.params.ParameterizedTest; + import org.junit.jupiter.params.provider.EnumSource; +@@ -14,7 +13,7 @@ public class DyeColorsTest extends AbstractTestingBase { + @EnumSource(DyeColor.class) + public void checkColor(DyeColor dye) { + Color color = dye.getColor(); +- int nmsColorArray = DyeColor.byId(dye.getWoolData()).getTextureDiffuseColor(); ++ int nmsColorArray = net.minecraft.world.item.DyeColor.byId(dye.getWoolData()).getTextureDiffuseColors(); // Paper - remap fix + Color nmsColor = Color.fromARGB(nmsColorArray); + assertThat(color, is(nmsColor)); + } +@@ -23,7 +22,7 @@ public class DyeColorsTest extends AbstractTestingBase { + @EnumSource(org.bukkit.DyeColor.class) + public void checkFireworkColor(org.bukkit.DyeColor dye) { + Color color = dye.getFireworkColor(); +- int nmsColor = DyeColor.byId(dye.getWoolData()).getFireworkColor(); ++ int nmsColor = net.minecraft.world.item.DyeColor.byId(dye.getWoolData()).getFireworkColor(); // Paper - remap fix + assertThat(color, is(Color.fromRGB(nmsColor))); + } + } +diff --git a/src/test/java/org/bukkit/ParticleTest.java b/src/test/java/org/bukkit/ParticleTest.java +index 9c9f5dd2351b3067b54d6cc5bdb572c46b12aaa8..307e8a3694c6f0b48d2df9792c3e5fdbaae1fd8e 100644 +--- a/src/test/java/org/bukkit/ParticleTest.java ++++ b/src/test/java/org/bukkit/ParticleTest.java +@@ -250,7 +250,7 @@ public class ParticleTest extends AbstractTestingBase { + Check in CraftParticle if the conversion is still correct. + """, bukkit.getKey())); + +- DataResult<Tag> encoded = assertDoesNotThrow(() -> minecraft.codec().codec().encodeStart(DynamicOpsNBT.INSTANCE, particleParam), ++ DataResult<Tag> encoded = assertDoesNotThrow(() -> minecraft.codec().codec().encodeStart(NbtOps.INSTANCE, particleParam), // Paper - remap fix + String.format(""" + Could not encoded particle param for particle %s. + This can indicated, that the wrong particle param is created in CraftParticle. +diff --git a/src/test/java/org/bukkit/entity/EntityTypesTest.java b/src/test/java/org/bukkit/entity/EntityTypesTest.java +index 32df0090aab65b551b524603cce0b96e461cc358..952924abae79cc504342bbdb6f6953ab8a6cc295 100644 +--- a/src/test/java/org/bukkit/entity/EntityTypesTest.java ++++ b/src/test/java/org/bukkit/entity/EntityTypesTest.java +@@ -6,7 +6,6 @@ import java.util.Set; + import java.util.stream.Collectors; + import net.minecraft.core.registries.BuiltInRegistries; + import net.minecraft.resources.ResourceLocation; +-import net.minecraft.world.entity.EntityType; + import org.bukkit.support.AbstractTestingBase; + import org.junit.jupiter.api.Test; + +@@ -16,8 +15,8 @@ public class EntityTypesTest extends AbstractTestingBase { + public void testMaps() { + Set<EntityType> allBukkit = Arrays.stream(EntityType.values()).filter((b) -> b.getName() != null).collect(Collectors.toSet()); + +- for (EntityType<?> nms : BuiltInRegistries.ENTITY_TYPE) { +- ResourceLocation key = EntityType.getKey(nms); ++ for (net.minecraft.world.entity.EntityType<?> nms : BuiltInRegistries.ENTITY_TYPE) { // Paper - remap fix ++ ResourceLocation key = net.minecraft.world.entity.EntityType.getKey(nms); // Paper - remap fix + + org.bukkit.entity.EntityType bukkit = org.bukkit.entity.EntityType.fromName(key.getPath()); + assertNotNull(bukkit, "Missing nms->bukkit " + key); +diff --git a/src/test/java/org/bukkit/entity/PandaGeneTest.java b/src/test/java/org/bukkit/entity/PandaGeneTest.java +index 5818bfa69a8573a2a8f350066f829d587cbc546b..8e421a1bee0c526e3024eab9ba4cc0b320842de2 100644 +--- a/src/test/java/org/bukkit/entity/PandaGeneTest.java ++++ b/src/test/java/org/bukkit/entity/PandaGeneTest.java +@@ -2,7 +2,6 @@ package org.bukkit.entity; + + import static org.junit.jupiter.api.Assertions.*; + +-import net.minecraft.world.entity.animal.Panda; + import org.bukkit.craftbukkit.entity.CraftPanda; + import org.junit.jupiter.api.Test; + +@@ -10,8 +9,8 @@ public class PandaGeneTest { + + @Test + public void testBukkit() { +- for (Panda.Gene gene : Panda.Gene.values()) { +- Panda.Gene nms = CraftPanda.toNms(gene); ++ for (Panda.Gene gene : Panda.Gene.values()) { // Paper - remap fix ++ net.minecraft.world.entity.animal.Panda.Gene nms = CraftPanda.toNms(gene); // Paper - remap fix + + assertNotNull(nms, "NMS gene null for " + gene); + assertEquals(gene.isRecessive(), nms.isRecessive(), "Recessive status did not match " + gene); +@@ -21,7 +20,7 @@ public class PandaGeneTest { + + @Test + public void testNMS() { +- for (Panda.Gene gene : Panda.Gene.values()) { ++ for (net.minecraft.world.entity.animal.Panda.Gene gene : net.minecraft.world.entity.animal.Panda.Gene.values()) { // Paper - remap fix + org.bukkit.entity.Panda.Gene bukkit = CraftPanda.fromNms(gene); + + assertNotNull(bukkit, "Bukkit gene null for " + gene); +diff --git a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java +index 6c84f1289c84d0aca1935c473ffa63dae1b52598..ebcb65cb74acdb9d1bcf2b4b3551a2dc6d809bc9 100644 +--- a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java ++++ b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java +@@ -29,17 +29,17 @@ public class RegistryConstantsTest extends AbstractTestingBase { + + @Test + public void testTrimMaterial() { +- this.testExcessConstants(TrimMaterial.class, Registry.TRIM_MATERIAL); ++ this.testExcessConstants(TrimMaterial.class, org.bukkit.Registry.TRIM_MATERIAL); // Paper - remap fix + this.testMissingConstants(TrimMaterial.class, Registries.TRIM_MATERIAL); + } + + @Test + public void testTrimPattern() { +- this.testExcessConstants(TrimPattern.class, Registry.TRIM_PATTERN); ++ this.testExcessConstants(TrimPattern.class, org.bukkit.Registry.TRIM_PATTERN); // Paper - remap fix + this.testMissingConstants(TrimPattern.class, Registries.TRIM_PATTERN); + } + +- private <T extends Keyed> void testExcessConstants(Class<T> clazz, Registry<T> registry) { ++ private <T extends Keyed> void testExcessConstants(Class<T> clazz, org.bukkit.Registry<T> registry) { // Paper - remap fix + List<NamespacedKey> excessKeys = new ArrayList<>(); + + for (Field field : clazz.getFields()) { +diff --git a/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java b/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java +index 31062b6d9ad685ea3750c6b5ddc6b295bb263e0a..5842cb5a6f3da42b8c40e6cbd5c5366572bf7684 100644 +--- a/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java ++++ b/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java +@@ -24,7 +24,7 @@ public class RegistryLoadOrderTest extends AbstractTestingBase { + + private static boolean initInterface = false; + private static boolean initAbstract = false; +- private static Registry<Keyed> registry; ++ private static org.bukkit.Registry<Keyed> registry; // Paper - remap fix + + public static Stream<Arguments> data() { + return Stream.of( diff --git a/patches/server/0003-Build-system-changes.patch b/patches/server/0003-Build-system-changes.patch new file mode 100644 index 0000000000..3642230481 --- /dev/null +++ b/patches/server/0003-Build-system-changes.patch @@ -0,0 +1,185 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Mon, 29 Feb 2016 20:40:33 -0600 +Subject: [PATCH] Build system changes + +== AT == +public net.minecraft.server.packs.VanillaPackResourcesBuilder safeGetPath(Ljava/net/URI;)Ljava/nio/file/Path; + +Co-authored-by: Jake Potrebic <[email protected]> + +diff --git a/build.gradle.kts b/build.gradle.kts +index 9ea079d14a92367629dc8fb30d1be043c9290964..f276414e9e81abf8f1f80991ebd5ab43472e07b1 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -8,9 +8,7 @@ plugins { + dependencies { + implementation(project(":paper-api")) + implementation("jline:jline:2.12.1") +- implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") { +- exclude(group = "org.apache.logging.log4j", module = "log4j-api") +- } ++ implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") // Paper - remove exclusion + implementation("org.ow2.asm:asm-commons:9.7") + implementation("commons-lang:commons-lang:2.6") + runtimeOnly("org.xerial:sqlite-jdbc:3.46.0.0") +@@ -38,6 +36,7 @@ tasks.jar { + val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim() + val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\"" + val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper ++ val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", + "Implementation-Title" to "CraftBukkit", +@@ -46,6 +45,9 @@ tasks.jar { + "Specification-Title" to "Bukkit", + "Specification-Version" to project.version, + "Specification-Vendor" to "Bukkit Team", ++ "Git-Branch" to gitBranch, // Paper ++ "Git-Commit" to gitHash, // Paper ++ "CraftBukkit-Package-Version" to paperweight.craftBukkitPackageVersion.get(), // Paper + ) + for (tld in setOf("net", "com", "org")) { + attributes("$tld/bukkit", "Sealed" to true) +@@ -58,6 +60,17 @@ publishing { + } + } + ++// Paper start ++val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks.ScanJarForBadCalls::class) { ++ badAnnotations.add("Lio/papermc/paper/annotation/DoNotUse;") ++ jarToScan.set(tasks.serverJar.flatMap { it.archiveFile }) ++ classpath.from(configurations.compileClasspath) ++} ++tasks.check { ++ dependsOn(scanJar) ++} ++// Paper end ++ + tasks.test { + exclude("org/bukkit/craftbukkit/inventory/ItemStack*Test.class") + useJUnitPlatform() +@@ -123,4 +136,5 @@ tasks.registerRunTask("runReobf") { + tasks.registerRunTask("runDev") { + description = "Spin up a non-relocated Mojang-mapped test server" + classpath(sourceSets.main.map { it.runtimeClasspath }) ++ jvmArgs("-DPaper.pushPaperAssetsRoot=true") + } +diff --git a/src/main/java/net/minecraft/resources/ResourceLocation.java b/src/main/java/net/minecraft/resources/ResourceLocation.java +index bfc8f152fa91dff1dcd5fd07fc067e8e5e480002..262660d115a5d5cbecfbae995955a24283e666b0 100644 +--- a/src/main/java/net/minecraft/resources/ResourceLocation.java ++++ b/src/main/java/net/minecraft/resources/ResourceLocation.java +@@ -32,6 +32,7 @@ public final class ResourceLocation implements Comparable<ResourceLocation> { + public static final char NAMESPACE_SEPARATOR = ':'; + public static final String DEFAULT_NAMESPACE = "minecraft"; + public static final String REALMS_NAMESPACE = "realms"; ++ public static final String PAPER_NAMESPACE = "paper"; // Paper + private final String namespace; + private final String path; + +diff --git a/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java b/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java +index 14fc03563daea531314c7ceba56dbb47884010ee..fcf95958ef659c7aa8e28026961fa1d6a5f8b28c 100644 +--- a/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java ++++ b/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java +@@ -138,6 +138,15 @@ public class VanillaPackResourcesBuilder { + + public VanillaPackResourcesBuilder applyDevelopmentConfig() { + developmentConfig.accept(this); ++ if (Boolean.getBoolean("Paper.pushPaperAssetsRoot")) { ++ try { ++ this.pushAssetPath(net.minecraft.server.packs.PackType.SERVER_DATA, net.minecraft.server.packs.VanillaPackResourcesBuilder.safeGetPath(java.util.Objects.requireNonNull( ++ // Important that this is a patched class ++ VanillaPackResourcesBuilder.class.getResource("/data/.paperassetsroot"), "Missing required .paperassetsroot file").toURI()).getParent()); ++ } catch (java.net.URISyntaxException | IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ } + return this; + } + +diff --git a/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java b/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java +index feca36209fd2405fab70f564f63e627b8b78ac18..396ec10a76bdadbf5be2f0e15e88eed47619004d 100644 +--- a/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java ++++ b/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java +@@ -48,7 +48,7 @@ public class ServerPacksSource extends BuiltInPackSource { + public static VanillaPackResources createVanillaPackSource() { + return new VanillaPackResourcesBuilder() + .setMetadata(BUILT_IN_METADATA) +- .exposeNamespace("minecraft") ++ .exposeNamespace("minecraft", ResourceLocation.PAPER_NAMESPACE) // Paper + .applyDevelopmentConfig() + .pushJarResources() + .build(VANILLA_PACK_INFO); +@@ -68,7 +68,18 @@ public class ServerPacksSource extends BuiltInPackSource { + @Nullable + @Override + protected Pack createBuiltinPack(String fileName, Pack.ResourcesSupplier packFactory, Component displayName) { +- return Pack.readMetaAndCreate(createBuiltInPackLocation(fileName, displayName), packFactory, PackType.SERVER_DATA, FEATURE_SELECTION_CONFIG); ++ // Paper start - custom built-in pack ++ final PackLocationInfo info; ++ final PackSelectionConfig packConfig; ++ if ("paper".equals(fileName)) { ++ info = new PackLocationInfo(fileName, displayName, PackSource.BUILT_IN, Optional.empty()); ++ packConfig = new PackSelectionConfig(true, Pack.Position.TOP, true); ++ } else { ++ info = createBuiltInPackLocation(fileName, displayName); ++ packConfig = FEATURE_SELECTION_CONFIG; ++ } ++ return Pack.readMetaAndCreate(info, packFactory, PackType.SERVER_DATA, packConfig); ++ // Paper end - custom built-in pack + } + + public static PackRepository createPackRepository(Path dataPacksPath, DirectoryValidator symlinkFinder) { +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 5595eb892fa868508c45448942da65d7c11d49fd..4b8602b168f9dd386aa72b4e5d189c441c93542e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -210,7 +210,7 @@ public class Main { + } + + if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { +- Date buildDate = new Date(Integer.parseInt(Main.class.getPackage().getImplementationVendor()) * 1000L); ++ Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper + + Calendar deadline = Calendar.getInstance(); + deadline.add(Calendar.DAY_OF_YEAR, -3); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index 93046379d0cefd5d3236fc59e698809acdc18f80..774556a62eb240da42e84db4502e2ed43495be17 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.spigotmc/spigot-api/pom.properties"); ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/resources/data/.paperassetsroot b/src/main/resources/data/.paperassetsroot +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +diff --git a/src/main/resources/data/minecraft/datapacks/paper/pack.mcmeta b/src/main/resources/data/minecraft/datapacks/paper/pack.mcmeta +new file mode 100644 +index 0000000000000000000000000000000000000000..288fbe68c6053f40e72f0feedef0ae0fed10fa67 +--- /dev/null ++++ b/src/main/resources/data/minecraft/datapacks/paper/pack.mcmeta +@@ -0,0 +1,6 @@ ++{ ++ "pack": { ++ "description": "Built-in Paper Datapack", ++ "pack_format": 41 ++ } ++} +diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java +index 1bdf0b2b8f8e6dace403695fece80ebb3dc9e039..544307fe34cbcfa286a7d7b30900ebea127d189e 100644 +--- a/src/test/java/org/bukkit/support/AbstractTestingBase.java ++++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java +@@ -43,6 +43,7 @@ public abstract class AbstractTestingBase { + public static final Registry<Biome> BIOMES; + + static { ++ System.setProperty("Paper.pushPaperAssetsRoot", "true"); // Paper + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + // Populate available packs diff --git a/patches/server/0004-Test-changes.patch b/patches/server/0004-Test-changes.patch new file mode 100644 index 0000000000..1703d167cb --- /dev/null +++ b/patches/server/0004-Test-changes.patch @@ -0,0 +1,372 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic <[email protected]> +Date: Mon, 13 Feb 2023 14:14:56 -0800 +Subject: [PATCH] Test changes + + +diff --git a/build.gradle.kts b/build.gradle.kts +index f276414e9e81abf8f1f80991ebd5ab43472e07b1..7a0f2391a464eeebc5e57856300bc000b8d35e52 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -22,6 +22,7 @@ dependencies { + testImplementation("org.hamcrest:hamcrest:2.2") + testImplementation("org.mockito:mockito-core:5.11.0") + testImplementation("org.ow2.asm:asm-tree:9.7") ++ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest + } + + paperweight { +@@ -55,6 +56,12 @@ tasks.jar { + } + } + ++// Paper start - compile tests with -parameters for better junit parameterized test names ++tasks.compileTestJava { ++ options.compilerArgs.add("-parameters") ++} ++// Paper end ++ + publishing { + publications.create<MavenPublication>("maven") { + } +diff --git a/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java b/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e070aa1bb69859224493d958621389ee757f8752 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java +@@ -0,0 +1,33 @@ ++package io.papermc.paper.registry; ++ ++import java.util.Optional; ++import java.util.stream.Stream; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.MethodSource; ++ ++import static org.junit.jupiter.api.Assertions.assertTrue; ++ ++class RegistryKeyTest extends AbstractTestingBase { ++ ++ @BeforeAll ++ static void before() throws ClassNotFoundException { ++ Class.forName(RegistryKey.class.getName()); // load all keys so they are found for the test ++ } ++ ++ static Stream<RegistryKey<?>> data() { ++ return RegistryKeyImpl.REGISTRY_KEYS.stream(); ++ } ++ ++ @ParameterizedTest ++ @MethodSource("data") ++ void testApiRegistryKeysExist(final RegistryKey<?> key) { ++ final Optional<Registry<Object>> registry = AbstractTestingBase.REGISTRY_CUSTOM.registry(ResourceKey.createRegistryKey(new ResourceLocation(key.key().asString()))); ++ assertTrue(registry.isPresent(), "Missing vanilla registry for " + key.key().asString()); ++ ++ } ++} +diff --git a/src/test/java/io/papermc/paper/util/EmptyTag.java b/src/test/java/io/papermc/paper/util/EmptyTag.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6eb95a5e2534974c0e52e2b78b04e7c2b2f28525 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/util/EmptyTag.java +@@ -0,0 +1,31 @@ ++package io.papermc.paper.util; ++ ++import java.util.Collections; ++import java.util.Set; ++import org.bukkit.Keyed; ++import org.bukkit.NamespacedKey; ++import org.bukkit.Tag; ++import org.jetbrains.annotations.NotNull; ++ ++public record EmptyTag(NamespacedKey key) implements Tag<Keyed> { ++ ++ @SuppressWarnings("deprecation") ++ public EmptyTag() { ++ this(NamespacedKey.randomKey()); ++ } ++ ++ @Override ++ public @NotNull NamespacedKey getKey() { ++ return this.key; ++ } ++ ++ @Override ++ public boolean isTagged(@NotNull final Keyed item) { ++ return false; ++ } ++ ++ @Override ++ public @NotNull Set<Keyed> getValues() { ++ return Collections.emptySet(); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/util/MethodParameterProvider.java b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3f58ef36df34cd15fcb72189eeff057654adf0c6 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java +@@ -0,0 +1,206 @@ ++/* ++ * Copyright 2015-2023 the original author or authors of https://github.com/junit-team/junit5/blob/6593317c15fb556febbde11914fa7afe00abf8cd/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java ++ * ++ * All rights reserved. This program and the accompanying materials are ++ * made available under the terms of the Eclipse Public License v2.0 which ++ * accompanies this distribution and is available at ++ * ++ * https://www.eclipse.org/legal/epl-v20.html ++ */ ++ ++package io.papermc.paper.util; ++ ++import java.lang.reflect.Method; ++import java.lang.reflect.Parameter; ++import java.util.List; ++import java.util.function.Predicate; ++import java.util.stream.Stream; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.TestFactory; ++import org.junit.jupiter.api.TestTemplate; ++import org.junit.jupiter.api.extension.ExtensionContext; ++import org.junit.jupiter.params.support.AnnotationConsumer; ++import org.junit.platform.commons.JUnitException; ++import org.junit.platform.commons.PreconditionViolationException; ++import org.junit.platform.commons.util.ClassLoaderUtils; ++import org.junit.platform.commons.util.CollectionUtils; ++import org.junit.platform.commons.util.Preconditions; ++import org.junit.platform.commons.util.ReflectionUtils; ++import org.junit.platform.commons.util.StringUtils; ++import org.junitpioneer.jupiter.cartesian.CartesianParameterArgumentsProvider; ++ ++import static java.lang.String.format; ++import static java.util.Arrays.stream; ++import static java.util.stream.Collectors.toList; ++import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; ++import static org.junit.platform.commons.util.CollectionUtils.isConvertibleToStream; ++ ++public class MethodParameterProvider implements CartesianParameterArgumentsProvider<Object>, AnnotationConsumer<MethodParameterSource> { ++ private MethodParameterSource source; ++ ++ MethodParameterProvider() { ++ } ++ ++ @Override ++ public void accept(final MethodParameterSource source) { ++ this.source = source; ++ } ++ ++ @Override ++ public Stream<Object> provideArguments(ExtensionContext context, Parameter parameter) { ++ return this.provideArguments(context, this.source); ++ } ++ ++ // Below is mostly copied from MethodArgumentsProvider ++ ++ private static final Predicate<Method> isFactoryMethod = // ++ method -> isConvertibleToStream(method.getReturnType()) && !isTestMethod(method); ++ ++ protected Stream<Object> provideArguments(ExtensionContext context, MethodParameterSource methodSource) { ++ Class<?> testClass = context.getRequiredTestClass(); ++ Method testMethod = context.getRequiredTestMethod(); ++ Object testInstance = context.getTestInstance().orElse(null); ++ String[] methodNames = methodSource.value(); ++ // @formatter:off ++ return stream(methodNames) ++ .map(factoryMethodName -> findFactoryMethod(testClass, testMethod, factoryMethodName)) ++ .map(factoryMethod -> validateFactoryMethod(factoryMethod, testInstance)) ++ .map(factoryMethod -> context.getExecutableInvoker().invoke(factoryMethod, testInstance)) ++ .flatMap(CollectionUtils::toStream); ++ // @formatter:on ++ } ++ ++ private static Method findFactoryMethod(Class<?> testClass, Method testMethod, String factoryMethodName) { ++ String originalFactoryMethodName = factoryMethodName; ++ ++ // If the user did not provide a factory method name, find a "default" local ++ // factory method with the same name as the parameterized test method. ++ if (StringUtils.isBlank(factoryMethodName)) { ++ factoryMethodName = testMethod.getName(); ++ return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName); ++ } ++ ++ // Convert local factory method name to fully-qualified method name. ++ if (!looksLikeAFullyQualifiedMethodName(factoryMethodName)) { ++ factoryMethodName = testClass.getName() + "#" + factoryMethodName; ++ } ++ ++ // Find factory method using fully-qualified name. ++ Method factoryMethod = findFactoryMethodByFullyQualifiedName(testClass, testMethod, factoryMethodName); ++ ++ // Ensure factory method has a valid return type and is not a test method. ++ Preconditions.condition(isFactoryMethod.test(factoryMethod), () -> format( ++ "Could not find valid factory method [%s] for test class [%s] but found the following invalid candidate: %s", ++ originalFactoryMethodName, testClass.getName(), factoryMethod)); ++ ++ return factoryMethod; ++ } ++ ++ private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodName) { ++ if (factoryMethodName.contains("#")) { ++ return true; ++ } ++ int indexOfFirstDot = factoryMethodName.indexOf('.'); ++ if (indexOfFirstDot == -1) { ++ return false; ++ } ++ int indexOfLastOpeningParenthesis = factoryMethodName.lastIndexOf('('); ++ if (indexOfLastOpeningParenthesis > 0) { ++ // Exclude simple/local method names with parameters ++ return indexOfFirstDot < indexOfLastOpeningParenthesis; ++ } ++ // If we get this far, we conclude the supplied factory method name "looks" ++ // like it was intended to be a fully-qualified method name, even if the ++ // syntax is invalid. We do this in order to provide better diagnostics for ++ // the user when a fully-qualified method name is in fact invalid. ++ return true; ++ } ++ ++ // package-private for testing ++ static Method findFactoryMethodByFullyQualifiedName( ++ Class<?> testClass, Method testMethod, ++ String fullyQualifiedMethodName ++ ) { ++ String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName); ++ String className = methodParts[0]; ++ String methodName = methodParts[1]; ++ String methodParameters = methodParts[2]; ++ ClassLoader classLoader = ClassLoaderUtils.getClassLoader(testClass); ++ Class<?> clazz = loadRequiredClass(className, classLoader); ++ ++ // Attempt to find an exact match first. ++ Method factoryMethod = ReflectionUtils.findMethod(clazz, methodName, methodParameters).orElse(null); ++ if (factoryMethod != null) { ++ return factoryMethod; ++ } ++ ++ boolean explicitParameterListSpecified = // ++ StringUtils.isNotBlank(methodParameters) || fullyQualifiedMethodName.endsWith("()"); ++ ++ // If we didn't find an exact match but an explicit parameter list was specified, ++ // that's a user configuration error. ++ Preconditions.condition(!explicitParameterListSpecified, ++ () -> format("Could not find factory method [%s(%s)] in class [%s]", methodName, methodParameters, ++ className)); ++ ++ // Otherwise, fall back to the same lenient search semantics that are used ++ // to locate a "default" local factory method. ++ return findFactoryMethodBySimpleName(clazz, testMethod, methodName); ++ } ++ ++ /** ++ * Find the factory method by searching for all methods in the given {@code clazz} ++ * with the desired {@code factoryMethodName} which have return types that can be ++ * converted to a {@link Stream}, ignoring the {@code testMethod} itself as well ++ * as any {@code @Test}, {@code @TestTemplate}, or {@code @TestFactory} methods ++ * with the same name. ++ * ++ * @return the single factory method matching the search criteria ++ * @throws PreconditionViolationException if the factory method was not found or ++ * multiple competing factory methods with the same name were found ++ */ ++ private static Method findFactoryMethodBySimpleName(Class<?> clazz, Method testMethod, String factoryMethodName) { ++ Predicate<Method> isCandidate = candidate -> factoryMethodName.equals(candidate.getName()) ++ && !testMethod.equals(candidate); ++ List<Method> candidates = ReflectionUtils.findMethods(clazz, isCandidate); ++ ++ List<Method> factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList()); ++ ++ Preconditions.condition(factoryMethods.size() > 0, () -> { ++ // If we didn't find the factory method using the isFactoryMethod Predicate, perhaps ++ // the specified factory method has an invalid return type or is a test method. ++ // In that case, we report the invalid candidates that were found. ++ if (candidates.size() > 0) { ++ return format( ++ "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s", ++ factoryMethodName, clazz.getName(), candidates); ++ } ++ // Otherwise, report that we didn't find anything. ++ return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName()); ++ }); ++ Preconditions.condition(factoryMethods.size() == 1, ++ () -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(), ++ factoryMethodName, clazz.getName(), factoryMethods)); ++ return factoryMethods.get(0); ++ } ++ ++ private static boolean isTestMethod(Method candidate) { ++ return isAnnotated(candidate, Test.class) || isAnnotated(candidate, TestTemplate.class) ++ || isAnnotated(candidate, TestFactory.class); ++ } ++ ++ private static Class<?> loadRequiredClass(String className, ClassLoader classLoader) { ++ return ReflectionUtils.tryToLoadClass(className, classLoader).getOrThrow( ++ cause -> new JUnitException(format("Could not load class [%s]", className), cause)); ++ } ++ ++ private static Method validateFactoryMethod(Method factoryMethod, Object testInstance) { ++ Preconditions.condition( ++ factoryMethod.getDeclaringClass().isInstance(testInstance) || ReflectionUtils.isStatic(factoryMethod), ++ () -> format("Method '%s' must be static: local factory methods must be static " ++ + "unless the PER_CLASS @TestInstance lifecycle mode is used; " ++ + "external factory methods must always be static.", ++ factoryMethod.toGenericString())); ++ return factoryMethod; ++ } ++} +diff --git a/src/test/java/io/papermc/paper/util/MethodParameterSource.java b/src/test/java/io/papermc/paper/util/MethodParameterSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6cbf11c898439834cffb99ef84e5df1494356809 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/util/MethodParameterSource.java +@@ -0,0 +1,14 @@ ++package io.papermc.paper.util; ++ ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource; ++ ++@Retention(RetentionPolicy.RUNTIME) ++@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) ++@CartesianArgumentsSource(MethodParameterProvider.class) ++public @interface MethodParameterSource { ++ String[] value() default {}; ++} +diff --git a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java +index ebcb65cb74acdb9d1bcf2b4b3551a2dc6d809bc9..7d9dbed7281099b78d7f898885b37cdcfe8b099f 100644 +--- a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java ++++ b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java +@@ -24,7 +24,7 @@ public class RegistryConstantsTest extends AbstractTestingBase { + @Test + public void testDamageType() { + this.testExcessConstants(DamageType.class, Registry.DAMAGE_TYPE); +- // this.testMissingConstants(DamageType.class, Registries.DAMAGE_TYPE); // WIND_CHARGE not registered ++ this.testMissingConstants(DamageType.class, Registries.DAMAGE_TYPE); // Paper - re-enable this one + } + + @Test +diff --git a/src/test/java/org/bukkit/support/DummyServer.java b/src/test/java/org/bukkit/support/DummyServer.java +index 4153866f3e630e54a23dc085aaac5e804344aa43..b8fe92fc75c611ee1efb82a8ab7089f28bf338ea 100644 +--- a/src/test/java/org/bukkit/support/DummyServer.java ++++ b/src/test/java/org/bukkit/support/DummyServer.java +@@ -50,6 +50,15 @@ public final class DummyServer { + return registers.computeIfAbsent(aClass, key -> CraftRegistry.createRegistry(aClass, AbstractTestingBase.REGISTRY_CUSTOM)); + }); + ++ // Paper start - testing additions ++ final Thread currentThread = Thread.currentThread(); ++ when(instance.isPrimaryThread()).thenAnswer(ignored -> Thread.currentThread().equals(currentThread)); ++ ++ final org.bukkit.plugin.PluginManager pluginManager = new org.bukkit.plugin.SimplePluginManager(instance, new org.bukkit.command.SimpleCommandMap(instance)); ++ when(instance.getPluginManager()).thenReturn(pluginManager); ++ when(instance.getTag(anyString(), any(org.bukkit.NamespacedKey.class), any())).thenAnswer(ignored -> new io.papermc.paper.util.EmptyTag()); ++ // paper end - testing additions ++ + Bukkit.setServer(instance); + } catch (Throwable t) { + throw new Error(t); diff --git a/patches/server/0005-Paper-config-files.patch b/patches/server/0005-Paper-config-files.patch new file mode 100644 index 0000000000..65cb89f76d --- /dev/null +++ b/patches/server/0005-Paper-config-files.patch @@ -0,0 +1,5148 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic <[email protected]> +Date: Wed, 8 Jun 2022 22:20:16 -0700 +Subject: [PATCH] Paper config files + +== AT == +public org.spigotmc.SpigotWorldConfig getBoolean(Ljava/lang/String;Z)Z +public org.spigotmc.SpigotWorldConfig getDouble(Ljava/lang/String;)D +public org.spigotmc.SpigotWorldConfig getDouble(Ljava/lang/String;D)D +public org.spigotmc.SpigotWorldConfig getInt(Ljava/lang/String;)I +public org.spigotmc.SpigotWorldConfig getInt(Ljava/lang/String;I)I +public org.spigotmc.SpigotWorldConfig getList(Ljava/lang/String;Ljava/lang/Object;)Ljava/util/List; +public org.spigotmc.SpigotWorldConfig getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +public net.minecraft.server.dedicated.DedicatedServerProperties reload(Lnet/minecraft/core/RegistryAccess;Ljava/util/Properties;Ljoptsimple/OptionSet;)Lnet/minecraft/server/dedicated/DedicatedServerProperties; +public net.minecraft.world.level.NaturalSpawner SPAWNING_CATEGORIES + +diff --git a/build.gradle.kts b/build.gradle.kts +index 7a0f2391a464eeebc5e57856300bc000b8d35e52..94dfc105b197c9eda481cbe90bb48b71c845e702 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -10,6 +10,7 @@ dependencies { + implementation("jline:jline:2.12.1") + implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") // Paper - remove exclusion + implementation("org.ow2.asm:asm-commons:9.7") ++ implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files + implementation("commons-lang:commons-lang:2.6") + runtimeOnly("org.xerial:sqlite-jdbc:3.46.0.0") + runtimeOnly("com.mysql:mysql-connector-j:8.4.0") +diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ef41cf3a7d1e6f2bfe81e0fb865d2f969bbc77c1 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java +@@ -0,0 +1,8 @@ ++package com.destroystokyo.paper; ++ ++/** ++ * @deprecated kept as a means to identify Paper in older plugins/PaperLib ++ */ ++@Deprecated(forRemoval = true) ++public class PaperConfig { ++} +diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c91f109b4cf64dc1b4ef09f38e1cb8bf5cb2be13 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +@@ -0,0 +1,8 @@ ++package com.destroystokyo.paper; ++ ++/** ++ * @deprecated kept as a means to identify Paper in older plugins/PaperLib ++ */ ++@Deprecated(forRemoval = true) ++public class PaperWorldConfig { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/Configuration.java b/src/main/java/io/papermc/paper/configuration/Configuration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..817fd26cc3591f9cae0f61f4036dde43c4ed60e8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/Configuration.java +@@ -0,0 +1,13 @@ ++package io.papermc.paper.configuration; ++ ++public final class Configuration { ++ public static final String VERSION_FIELD = "_version"; ++ @Deprecated ++ public static final String LEGACY_CONFIG_VERSION_FIELD = "config-version"; ++ ++ @Deprecated ++ public static final int FINAL_LEGACY_VERSION = 27; ++ ++ private Configuration() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java +new file mode 100644 +index 0000000000000000000000000000000000000000..227039a6c69c4c99bbd9c674b3aab0ef5e2c1374 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java +@@ -0,0 +1,27 @@ ++package io.papermc.paper.configuration; ++ ++import java.nio.file.Path; ++import org.spongepowered.configurate.loader.HeaderMode; ++import org.spongepowered.configurate.util.MapFactories; ++import org.spongepowered.configurate.yaml.NodeStyle; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++public final class ConfigurationLoaders { ++ private ConfigurationLoaders() { ++ } ++ ++ public static YamlConfigurationLoader.Builder naturallySorted() { ++ return YamlConfigurationLoader.builder() ++ .indent(2) ++ .nodeStyle(NodeStyle.BLOCK) ++ .headerMode(HeaderMode.PRESET) ++ .defaultOptions(options -> options.mapFactory(MapFactories.sortedNatural())); ++ } ++ ++ public static YamlConfigurationLoader naturallySortedWithoutHeader(final Path path) { ++ return naturallySorted() ++ .headerMode(HeaderMode.NONE) ++ .path(path) ++ .build(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java +new file mode 100644 +index 0000000000000000000000000000000000000000..042478cf7ce150f1f1bc5cddd7fa40f86ec773dd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java +@@ -0,0 +1,7 @@ ++package io.papermc.paper.configuration; ++ ++/** ++ * Marker interface for unique sections of a configuration. ++ */ ++public abstract class ConfigurationPart { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7684e71f802f3d19e5340713b45cc84860ce9495 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/Configurations.java +@@ -0,0 +1,360 @@ ++package io.papermc.paper.configuration; ++ ++import com.google.common.base.Preconditions; ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.constraint.Constraint; ++import io.papermc.paper.configuration.constraint.Constraints; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.GameRules; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.MustBeInvokedByOverriders; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.CommentedConfigurationNode; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.objectmapping.ObjectMapper; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.CheckedFunction; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.nio.file.AccessDeniedException; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.NoSuchElementException; ++import java.util.Objects; ++import java.util.function.UnaryOperator; ++ ++public abstract class Configurations<G, W> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ public static final String WORLD_DEFAULTS = "__world_defaults__"; ++ public static final ResourceLocation WORLD_DEFAULTS_KEY = new ResourceLocation("configurations", WORLD_DEFAULTS); ++ protected final Path globalFolder; ++ protected final Class<G> globalConfigClass; ++ protected final Class<W> worldConfigClass; ++ protected final String globalConfigFileName; ++ protected final String defaultWorldConfigFileName; ++ protected final String worldConfigFileName; ++ ++ public Configurations( ++ final Path globalFolder, ++ final Class<G> globalConfigType, ++ final Class<W> worldConfigClass, ++ final String globalConfigFileName, ++ final String defaultWorldConfigFileName, ++ final String worldConfigFileName ++ ) { ++ this.globalFolder = globalFolder; ++ this.globalConfigClass = globalConfigType; ++ this.worldConfigClass = worldConfigClass; ++ this.globalConfigFileName = globalConfigFileName; ++ this.defaultWorldConfigFileName = defaultWorldConfigFileName; ++ this.worldConfigFileName = worldConfigFileName; ++ } ++ ++ protected ObjectMapper.Factory.Builder createObjectMapper() { ++ return ObjectMapper.factoryBuilder() ++ .addConstraint(Constraint.class, new Constraint.Factory()) ++ .addConstraint(Constraints.Min.class, Number.class, new Constraints.Min.Factory()); ++ } ++ ++ protected YamlConfigurationLoader.Builder createLoaderBuilder() { ++ return ConfigurationLoaders.naturallySorted(); ++ } ++ ++ protected abstract boolean isConfigType(final Type type); ++ ++ protected abstract int globalConfigVersion(); ++ ++ protected abstract int worldConfigVersion(); ++ ++ protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() { ++ return this.createObjectMapper(); ++ } ++ ++ @MustBeInvokedByOverriders ++ protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() { ++ return this.createLoaderBuilder(); ++ } ++ ++ static <T> CheckedFunction<ConfigurationNode, T, SerializationException> creator(final Class<? extends T> type, final boolean refreshNode) { ++ return node -> { ++ final T instance = node.require(type); ++ if (refreshNode) { ++ node.set(type, instance); ++ } ++ return instance; ++ }; ++ } ++ ++ static <T> CheckedFunction<ConfigurationNode, T, SerializationException> reloader(Class<T> type, T instance) { ++ return node -> { ++ ObjectMapper.Factory factory = (ObjectMapper.Factory) Objects.requireNonNull(node.options().serializers().get(type)); ++ ObjectMapper.Mutable<T> mutable = (ObjectMapper.Mutable<T>) factory.get(type); ++ mutable.load(instance, node); ++ return instance; ++ }; ++ } ++ ++ public G initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException { ++ return this.initializeGlobalConfiguration(creator(this.globalConfigClass, true)); ++ } ++ ++ private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode node, String filename) throws ConfigurateException { ++ try { ++ loader.save(node); ++ } catch (ConfigurateException ex) { ++ if (ex.getCause() instanceof AccessDeniedException) { ++ LOGGER.warn("Could not save {}: Paper could not persist the full set of configuration settings in the configuration file. Any setting missing from the configuration file will be set with its default value in memory. Admins should make sure to review the configuration documentation at https://docs.papermc.io/paper/configuration for more details.", filename, ex); ++ } else throw ex; ++ } ++ } ++ ++ protected G initializeGlobalConfiguration(final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException { ++ final Path configFile = this.globalFolder.resolve(this.globalConfigFileName); ++ final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder() ++ .defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build())) ++ .path(configFile) ++ .build(); ++ final ConfigurationNode node; ++ if (Files.notExists(configFile)) { ++ node = CommentedConfigurationNode.root(loader.defaultOptions()); ++ node.node(Configuration.VERSION_FIELD).raw(this.globalConfigVersion()); ++ } else { ++ node = loader.load(); ++ this.verifyGlobalConfigVersion(node); ++ } ++ this.applyGlobalConfigTransformations(node); ++ final G instance = creator.apply(node); ++ trySaveFileNode(loader, node, configFile.toString()); ++ return instance; ++ } ++ ++ protected void verifyGlobalConfigVersion(final ConfigurationNode globalNode) { ++ final ConfigurationNode version = globalNode.node(Configuration.VERSION_FIELD); ++ if (version.virtual()) { ++ LOGGER.warn("The global config file didn't have a version set, assuming latest"); ++ version.raw(this.globalConfigVersion()); ++ } else if (version.getInt() > this.globalConfigVersion()) { ++ LOGGER.error("Loading a newer configuration than is supported ({} > {})! You may have to backup & delete your global config file to start the server.", version.getInt(), this.globalConfigVersion()); ++ } ++ } ++ ++ protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException { ++ } ++ ++ @MustBeInvokedByOverriders ++ protected ContextMap.Builder createDefaultContextMap(final RegistryAccess registryAccess) { ++ return ContextMap.builder() ++ .put(WORLD_NAME, WORLD_DEFAULTS) ++ .put(WORLD_KEY, WORLD_DEFAULTS_KEY) ++ .put(REGISTRY_ACCESS, registryAccess); ++ } ++ ++ public void initializeWorldDefaultsConfiguration(final RegistryAccess registryAccess) throws ConfigurateException { ++ final ContextMap contextMap = this.createDefaultContextMap(registryAccess) ++ .put(FIRST_DEFAULT) ++ .build(); ++ final Path configFile = this.globalFolder.resolve(this.defaultWorldConfigFileName); ++ final DefaultWorldLoader result = this.createDefaultWorldLoader(false, contextMap, configFile); ++ final YamlConfigurationLoader loader = result.loader(); ++ final ConfigurationNode node = loader.load(); ++ if (result.isNewFile()) { // add version to new files ++ node.node(Configuration.VERSION_FIELD).raw(this.worldConfigVersion()); ++ } else { ++ this.verifyWorldConfigVersion(contextMap, node); ++ } ++ this.applyWorldConfigTransformations(contextMap, node, null); ++ final W instance = node.require(this.worldConfigClass); ++ node.set(this.worldConfigClass, instance); ++ this.trySaveFileNode(loader, node, configFile.toString()); ++ } ++ ++ private DefaultWorldLoader createDefaultWorldLoader(final boolean requireFile, final ContextMap contextMap, final Path configFile) { ++ boolean willCreate = Files.notExists(configFile); ++ if (requireFile && willCreate) { ++ throw new IllegalStateException("World defaults configuration file '" + configFile + "' doesn't exist"); ++ } ++ return new DefaultWorldLoader( ++ this.createWorldConfigLoaderBuilder(contextMap) ++ .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build())) ++ .path(configFile) ++ .build(), ++ willCreate ++ ); ++ } ++ ++ private record DefaultWorldLoader(YamlConfigurationLoader loader, boolean isNewFile) { ++ } ++ ++ protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) { ++ return this.createObjectMapper(); ++ } ++ ++ @MustBeInvokedByOverriders ++ protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) { ++ return this.createLoaderBuilder(); ++ } ++ ++ // Make sure to run version transforms on the default world config first via #setupWorldDefaultsConfig ++ public W createWorldConfig(final ContextMap contextMap) throws IOException { ++ return this.createWorldConfig(contextMap, creator(this.worldConfigClass, false)); ++ } ++ ++ protected W createWorldConfig(final ContextMap contextMap, final CheckedFunction<ConfigurationNode, W, SerializationException> creator) throws IOException { ++ Preconditions.checkArgument(!contextMap.isDefaultWorldContext(), "cannot create world map with default world context"); ++ final Path defaultsConfigFile = this.globalFolder.resolve(this.defaultWorldConfigFileName); ++ final YamlConfigurationLoader defaultsLoader = this.createDefaultWorldLoader(true, this.createDefaultContextMap(contextMap.require(REGISTRY_ACCESS)).build(), defaultsConfigFile).loader(); ++ final ConfigurationNode defaultsNode = defaultsLoader.load(); ++ ++ boolean newFile = false; ++ final Path dir = contextMap.require(WORLD_DIRECTORY); ++ final Path worldConfigFile = dir.resolve(this.worldConfigFileName); ++ if (Files.notExists(worldConfigFile)) { ++ PaperConfigurations.createDirectoriesSymlinkAware(dir); ++ Files.createFile(worldConfigFile); // create empty file as template ++ newFile = true; ++ } ++ ++ final YamlConfigurationLoader worldLoader = this.createWorldConfigLoaderBuilder(contextMap) ++ .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build())) ++ .path(worldConfigFile) ++ .build(); ++ final ConfigurationNode worldNode = worldLoader.load(); ++ if (newFile) { // set the version field if new file ++ worldNode.node(Configuration.VERSION_FIELD).set(this.worldConfigVersion()); ++ } else { ++ this.verifyWorldConfigVersion(contextMap, worldNode); ++ } ++ this.applyWorldConfigTransformations(contextMap, worldNode, defaultsNode); ++ this.applyDefaultsAwareWorldConfigTransformations(contextMap, worldNode, defaultsNode); ++ this.trySaveFileNode(worldLoader, worldNode, worldConfigFile.toString()); // save before loading node NOTE: don't save the backing node after loading it, or you'll fill up the world-specific config ++ worldNode.mergeFrom(defaultsNode); ++ return creator.apply(worldNode); ++ } ++ ++ protected void verifyWorldConfigVersion(final ContextMap contextMap, final ConfigurationNode worldNode) { ++ final ConfigurationNode version = worldNode.node(Configuration.VERSION_FIELD); ++ final String worldName = contextMap.require(WORLD_NAME); ++ if (version.virtual()) { ++ if (worldName.equals(WORLD_DEFAULTS)) { ++ LOGGER.warn("The world defaults config file didn't have a version set, assuming latest"); ++ } else { ++ LOGGER.warn("The world config file for " + worldName + " didn't have a version set, assuming latest"); ++ } ++ version.raw(this.worldConfigVersion()); ++ } else if (version.getInt() > this.worldConfigVersion()) { ++ String msg = "Loading a newer configuration than is supported ({} > {})! "; ++ if (worldName.equals(WORLD_DEFAULTS)) { ++ msg += "You may have to backup & delete the world defaults config file to start the server."; ++ } else { ++ msg += "You may have to backup & delete the " + worldName + " config file to start the server."; ++ } ++ LOGGER.error(msg, version.getInt(), this.worldConfigVersion()); ++ } ++ } ++ ++ protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node, final @Nullable ConfigurationNode defaultsNode) throws ConfigurateException { ++ } ++ ++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException { ++ } ++ ++ private UnaryOperator<ConfigurationOptions> applyObjectMapperFactory(final ObjectMapper.Factory factory) { ++ return options -> options.serializers(builder -> builder ++ .register(this::isConfigType, factory.asTypeSerializer()) ++ .registerAnnotatedObjects(factory)); ++ } ++ ++ public Path getWorldConfigFile(ServerLevel level) { ++ return level.convertable.levelDirectory.path().resolve(this.worldConfigFileName); ++ } ++ ++ public static class ContextMap { ++ private static final Object VOID = new Object(); ++ ++ public static Builder builder() { ++ return new Builder(); ++ } ++ ++ private final Map<ContextKey<?>, Object> backingMap; ++ ++ private ContextMap(Map<ContextKey<?>, Object> map) { ++ this.backingMap = Map.copyOf(map); ++ } ++ ++ @SuppressWarnings("unchecked") ++ public <T> T require(ContextKey<T> key) { ++ final @Nullable Object value = this.backingMap.get(key); ++ if (value == null) { ++ throw new NoSuchElementException("No element found for " + key + " with type " + key.type()); ++ } else if (value == VOID) { ++ throw new IllegalArgumentException("Cannot get the value of a Void key"); ++ } ++ return (T) value; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public <T> @Nullable T get(ContextKey<T> key) { ++ return (T) this.backingMap.get(key); ++ } ++ ++ public boolean has(ContextKey<?> key) { ++ return this.backingMap.containsKey(key); ++ } ++ ++ public boolean isDefaultWorldContext() { ++ return this.require(WORLD_KEY).equals(WORLD_DEFAULTS_KEY); ++ } ++ ++ public static class Builder { ++ ++ private Builder() { ++ } ++ ++ private final Map<ContextKey<?>, Object> buildingMap = new HashMap<>(); ++ ++ public <T> Builder put(ContextKey<T> key, T value) { ++ this.buildingMap.put(key, value); ++ return this; ++ } ++ ++ public Builder put(ContextKey<Void> key) { ++ this.buildingMap.put(key, VOID); ++ return this; ++ } ++ ++ public ContextMap build() { ++ return new ContextMap(this.buildingMap); ++ } ++ } ++ } ++ ++ public static final ContextKey<Path> WORLD_DIRECTORY = new ContextKey<>(Path.class, "world directory"); ++ public static final ContextKey<String> WORLD_NAME = new ContextKey<>(String.class, "world name"); // TODO remove when we deprecate level names ++ public static final ContextKey<ResourceLocation> WORLD_KEY = new ContextKey<>(ResourceLocation.class, "world key"); ++ public static final ContextKey<Void> FIRST_DEFAULT = new ContextKey<>(Void.class, "first default"); ++ public static final ContextKey<RegistryAccess> REGISTRY_ACCESS = new ContextKey<>(RegistryAccess.class, "registry access"); ++ public static final ContextKey<GameRules> GAME_RULES = new ContextKey<>(GameRules.class, "game rules"); ++ ++ public record ContextKey<T>(TypeToken<T> type, String name) { ++ ++ public ContextKey(Class<T> type, String name) { ++ this(TypeToken.get(type), name); ++ } ++ ++ @Override ++ public String toString() { ++ return "ContextKey{" + this.name + "}"; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..892351c3b6397b3cde6a31a6594c0e16bbe1252b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -0,0 +1,311 @@ ++package io.papermc.paper.configuration; ++ ++import co.aikar.timings.MinecraftTimings; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.constraint.Constraints; ++import io.papermc.paper.configuration.type.number.IntOr; ++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.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.meta.Comment; ++import org.spongepowered.configurate.objectmapping.meta.PostProcess; ++import org.spongepowered.configurate.objectmapping.meta.Required; ++import org.spongepowered.configurate.objectmapping.meta.Setting; ++ ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++ ++@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) ++public class GlobalConfiguration extends ConfigurationPart { ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ static final int CURRENT_VERSION = 29; // (when you change the version, change the comment, so it conflicts on rebases): <insert changes here> ++ 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; ++ ++ public Messages messages; ++ ++ public class Messages extends ConfigurationPart { ++ public Kick kick; ++ ++ public class Kick extends ConfigurationPart { ++ public Component authenticationServersDown = Component.translatable("multiplayer.disconnect.authservers_down"); ++ public Component connectionThrottle = Component.text("Connection throttled! Please wait before reconnecting."); ++ public Component flyingPlayer = Component.translatable("multiplayer.disconnect.flying"); ++ public Component flyingVehicle = Component.translatable("multiplayer.disconnect.flying"); ++ } ++ ++ public Component noPermission = Component.text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", NamedTextColor.RED); ++ public boolean useDisplayNameInQuitMessage = false; ++ } ++ ++ @Deprecated(forRemoval = true) ++ public Timings timings; ++ ++ @Deprecated(forRemoval = true) ++ public class Timings extends ConfigurationPart { ++ public boolean enabled = true; ++ public boolean verbose = true; ++ public String url = "https://timings.aikar.co/"; ++ public boolean serverNamePrivacy = false; ++ public List<String> hiddenConfigEntries = List.of( ++ "database", ++ "proxies.velocity.secret" ++ ); ++ public int historyInterval = 300; ++ public int historyLength = 3600; ++ public String serverName = "Unknown Server"; ++ ++ @PostProcess ++ private void postProcess() { ++ MinecraftTimings.processConfig(this); ++ } ++ } ++ ++ public Proxies proxies; ++ ++ public class Proxies extends ConfigurationPart { ++ public BungeeCord bungeeCord; ++ ++ public class BungeeCord extends ConfigurationPart { ++ public boolean onlineMode = true; ++ } ++ ++ public Velocity velocity; ++ ++ public class Velocity extends ConfigurationPart { ++ public boolean enabled = false; ++ public boolean onlineMode = true; ++ public String secret = ""; ++ ++ @PostProcess ++ private void postProcess() { ++ if (this.enabled && this.secret.isEmpty()) { ++ LOGGER.error("Velocity is enabled, but no secret key was specified. A secret key is required. Disabling velocity..."); ++ this.enabled = false; ++ } ++ } ++ } ++ public boolean proxyProtocol = false; ++ public boolean isProxyOnlineMode() { ++ return org.bukkit.Bukkit.getOnlineMode() || (org.spigotmc.SpigotConfig.bungee && this.bungeeCord.onlineMode) || (this.velocity.enabled && this.velocity.onlineMode); ++ } ++ } ++ ++ public Console console; ++ ++ public class Console extends ConfigurationPart { ++ public boolean enableBrigadierHighlighting = true; ++ public boolean enableBrigadierCompletions = true; ++ public boolean hasAllPermissions = false; ++ } ++ ++ public Watchdog watchdog; ++ ++ public class Watchdog extends ConfigurationPart { ++ public int earlyWarningEvery = 5000; ++ public int earlyWarningDelay = 10000; ++ } ++ ++ public SpamLimiter spamLimiter; ++ ++ public class SpamLimiter extends ConfigurationPart { ++ public int tabSpamIncrement = 1; ++ public int tabSpamLimit = 500; ++ public int recipeSpamIncrement = 1; ++ public int recipeSpamLimit = 20; ++ public int incomingPacketThreshold = 300; ++ } ++ ++ public ChunkLoading chunkLoading; ++ ++ public class ChunkLoading extends ConfigurationPart { ++ public int minLoadRadius = 2; ++ public int maxConcurrentSends = 2; ++ public boolean autoconfigSendDistance = true; ++ public double targetPlayerChunkSendRate = 100.0; ++ public double globalMaxChunkSendRate = -1.0; ++ public boolean enableFrustumPriority = false; ++ public double globalMaxChunkLoadRate = -1.0; ++ public double playerMaxConcurrentLoads = 20.0; ++ public double globalMaxConcurrentLoads = 500.0; ++ public double playerMaxChunkLoadRate = -1.0; ++ } ++ ++ public UnsupportedSettings unsupportedSettings; ++ ++ public class UnsupportedSettings extends ConfigurationPart { ++ @Comment("This setting controls if the broken behavior of disarmed tripwires not breaking should be allowed. This also allows for dupes") ++ public boolean allowTripwireDisarmingExploits = false; ++ @Comment("This setting allows for exploits related to end portals, for example sand duping") ++ public boolean allowUnsafeEndPortalTeleportation = false; ++ @Comment("This setting controls if players should be able to break bedrock, end portals and other intended to be permanent blocks.") ++ public boolean allowPermanentBlockBreakExploits = false; ++ @Comment("This setting controls if player should be able to use TNT duplication, but this also allows duplicating carpet, rails and potentially other items") ++ public boolean allowPistonDuplication = false; ++ public boolean performUsernameValidation = true; ++ @Comment("This setting controls if players should be able to create headless pistons.") ++ public boolean allowHeadlessPistons = false; ++ @Comment("This setting controls what compression format is used for region files.") ++ public CompressionFormat compressionFormat = CompressionFormat.ZLIB; ++ ++ public enum CompressionFormat { ++ GZIP, ++ ZLIB, ++ NONE ++ } ++ } ++ ++ public Commands commands; ++ ++ public class Commands extends ConfigurationPart { ++ public boolean suggestPlayerNamesWhenNullTabCompletions = true; ++ public boolean fixTargetSelectorTagCompletion = true; ++ public boolean timeCommandAffectsAllWorlds = false; ++ } ++ ++ public Logging logging; ++ ++ public class Logging extends ConfigurationPart { ++ public boolean deobfuscateStacktraces = true; ++ } ++ ++ public Scoreboards scoreboards; ++ ++ public class Scoreboards extends ConfigurationPart { ++ public boolean trackPluginScoreboards = false; ++ public boolean saveEmptyScoreboardTeams = true; ++ } ++ ++ @SuppressWarnings("unused") // used in postProcess ++ public ChunkSystem chunkSystem; ++ ++ public class ChunkSystem extends ConfigurationPart { ++ ++ public int ioThreads = -1; ++ public int workerThreads = -1; ++ public String genParallelism = "default"; ++ ++ @PostProcess ++ private void postProcess() { ++ //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); ++ } ++ } ++ ++ public ItemValidation itemValidation; ++ ++ public class ItemValidation extends ConfigurationPart { ++ public int displayName = 8192; ++ public int loreLine = 8192; ++ public Book book; ++ ++ public class Book extends ConfigurationPart { ++ public int title = 8192; ++ public int author = 8192; ++ public int page = 16384; ++ } ++ ++ public BookSize bookSize; ++ ++ public class BookSize extends ConfigurationPart { ++ public int pageMax = 2560; // TODO this appears to be a duplicate setting with one above ++ public double totalMultiplier = 0.98D; // TODO this should probably be merged into the above inner class ++ } ++ public boolean resolveSelectorsInBooks = false; ++ } ++ ++ public PacketLimiter packetLimiter; ++ ++ public class PacketLimiter extends ConfigurationPart { ++ public Component kickMessage = Component.translatable("disconnect.exceeded_packet_rate", NamedTextColor.RED); ++ public PacketLimit allPackets = new PacketLimit(7.0, 500.0, PacketLimit.ViolateAction.KICK); ++ public Map<Class<? extends Packet<?>>, PacketLimit> overrides = Map.of(ServerboundPlaceRecipePacket.class, new PacketLimit(4.0, 5.0, PacketLimit.ViolateAction.DROP)); ++ ++ @ConfigSerializable ++ public record PacketLimit(@Required double interval, @Required double maxPacketRate, ViolateAction action) { ++ public PacketLimit(final double interval, final double maxPacketRate, final @Nullable ViolateAction action) { ++ this.interval = interval; ++ this.maxPacketRate = maxPacketRate; ++ this.action = Objects.requireNonNullElse(action, ViolateAction.KICK); ++ } ++ ++ public boolean isEnabled() { ++ return this.interval > 0.0 && this.maxPacketRate > 0.0; ++ } ++ ++ public enum ViolateAction { ++ KICK, ++ DROP; ++ } ++ } ++ } ++ ++ public Collisions collisions; ++ ++ public class Collisions extends ConfigurationPart { ++ public boolean enablePlayerCollisions = true; ++ public boolean sendFullPosForHardCollidingEntities = true; ++ } ++ ++ public PlayerAutoSave playerAutoSave; ++ ++ ++ public class PlayerAutoSave extends ConfigurationPart { ++ public int rate = -1; ++ private int maxPerTick = -1; ++ public int maxPerTick() { ++ if (this.maxPerTick < 0) { ++ return (this.rate == 1 || this.rate > 100) ? 10 : 20; ++ } ++ return this.maxPerTick; ++ } ++ } ++ ++ public Misc misc; ++ ++ public class Misc extends ConfigurationPart { ++ ++ @SuppressWarnings("unused") // used in postProcess ++ public ChatThreads chatThreads; ++ public class ChatThreads extends ConfigurationPart { ++ private int chatExecutorCoreSize = -1; ++ private int chatExecutorMaxSize = -1; ++ ++ @PostProcess ++ private void postProcess() { ++ // TODO: fill in separate patch ++ } ++ } ++ public int maxJoinsPerTick = 5; ++ public boolean fixEntityPositionDesync = true; ++ public boolean loadPermissionsYmlBeforePlugins = true; ++ @Constraints.Min(4) ++ public int regionFileCacheSize = 256; ++ @Comment("See https://luckformula.emc.gs") ++ public boolean useAlternativeLuckFormula = false; ++ public boolean useDimensionTypeForCustomSpawners = false; ++ public boolean strictAdvancementDimensionCheck = false; ++ public IntOr.Default compressionLevel = IntOr.Default.USE_DEFAULT; ++ } ++ ++ public BlockUpdates blockUpdates; ++ ++ public class BlockUpdates extends ConfigurationPart { ++ public boolean disableNoteblockUpdates = false; ++ public boolean disableTripwireUpdates = false; ++ public boolean disableChorusPlantUpdates = false; ++ public boolean disableMushroomBlockUpdates = false; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/NestedSetting.java b/src/main/java/io/papermc/paper/configuration/NestedSetting.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69add4a7f1147015806bc9b63a8340d1893356c1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/NestedSetting.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.configuration; ++ ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.meta.NodeResolver; ++ ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.AnnotatedElement; ++ ++@Documented ++@Retention(RetentionPolicy.RUNTIME) ++@Target(ElementType.FIELD) ++public @interface NestedSetting { ++ ++ String[] value(); ++ ++ class Factory implements NodeResolver.Factory { ++ @Override ++ public @Nullable NodeResolver make(String name, AnnotatedElement element) { ++ if (element.isAnnotationPresent(NestedSetting.class)) { ++ Object[] path = element.getAnnotation(NestedSetting.class).value(); ++ if (path.length > 0) { ++ return node -> node.node(path); ++ } ++ } ++ return null; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..83a726bcf8b7dce73a361b0d79dbd63a0afc7a12 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +@@ -0,0 +1,465 @@ ++package io.papermc.paper.configuration; ++ ++import com.google.common.base.Suppliers; ++import com.google.common.collect.Table; ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; ++import io.papermc.paper.configuration.mapping.InnerClassFieldDiscoverer; ++import io.papermc.paper.configuration.serializer.ComponentSerializer; ++import io.papermc.paper.configuration.serializer.EnumValueSerializer; ++import io.papermc.paper.configuration.serializer.NbtPathSerializer; ++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.transformation.global.LegacyPaperConfig; ++import io.papermc.paper.configuration.transformation.global.versioned.V29_LogIPs; ++import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; ++import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig; ++import io.papermc.paper.configuration.transformation.world.versioned.V29_ZeroWorldHeight; ++import io.papermc.paper.configuration.transformation.world.versioned.V30_RenameFilterNbtFromSpawnEgg; ++import io.papermc.paper.configuration.transformation.world.versioned.V31_SpawnLoadedRangeToGameRule; ++import io.papermc.paper.configuration.type.BooleanOrDefault; ++import io.papermc.paper.configuration.type.Duration; ++import io.papermc.paper.configuration.type.DurationOrDisabled; ++import io.papermc.paper.configuration.type.EngineMode; ++import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer; ++import io.papermc.paper.configuration.type.number.DoubleOr; ++import io.papermc.paper.configuration.type.number.IntOr; ++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 java.io.File; ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.util.List; ++import java.util.function.Function; ++import java.util.function.Supplier; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceLocation; ++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.GameRules; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; ++import org.apache.commons.lang3.RandomStringUtils; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.VisibleForTesting; ++import org.slf4j.Logger; ++import org.spigotmc.SpigotConfig; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.BasicConfigurationNode; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.NodePath; ++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 static com.google.common.base.Preconditions.checkState; ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++@SuppressWarnings("Convert2Diamond") ++public class PaperConfigurations extends Configurations<GlobalConfiguration, WorldConfiguration> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ static final String GLOBAL_CONFIG_FILE_NAME = "paper-global.yml"; ++ static final String WORLD_DEFAULTS_CONFIG_FILE_NAME = "paper-world-defaults.yml"; ++ static final String WORLD_CONFIG_FILE_NAME = "paper-world.yml"; ++ public static final String CONFIG_DIR = "config"; ++ private static final String BACKUP_DIR ="legacy-backup"; ++ ++ private static final String GLOBAL_HEADER = String.format(""" ++ This is the global configuration file for Paper. ++ 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 Paper, ++ join us in our Discord or check the docs page. ++ ++ The world configuration options have been moved inside ++ their respective world folder. The files are named %s ++ ++ Docs: https://docs.papermc.io/ ++ Discord: https://discord.gg/papermc ++ Website: https://papermc.io/""", WORLD_CONFIG_FILE_NAME); ++ ++ private static final String WORLD_DEFAULTS_HEADER = """ ++ This is the world defaults configuration file for Paper. ++ 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 Paper, ++ join us in our Discord or check the docs page. ++ ++ Configuration options here apply to all worlds, unless you specify overrides inside ++ the world-specific config file inside each world folder. ++ ++ Docs: https://docs.papermc.io/ ++ Discord: https://discord.gg/papermc ++ Website: https://papermc.io/"""; ++ ++ private static final Function<ContextMap, String> WORLD_HEADER = map -> String.format(""" ++ This is a world configuration file for Paper. ++ This file may start empty but can be filled with settings to override ones in the %s/%s ++ ++ World: %s (%s)""", ++ PaperConfigurations.CONFIG_DIR, ++ PaperConfigurations.WORLD_DEFAULTS_CONFIG_FILE_NAME, ++ map.require(WORLD_NAME), ++ map.require(WORLD_KEY) ++ ); ++ ++ private static final String MOVED_NOTICE = """ ++ The global and world default configuration files have moved to %s ++ and the world-specific configuration file has been moved inside ++ the respective world folder. ++ ++ See https://docs.papermc.io/paper/configuration for more information. ++ """; ++ ++ @VisibleForTesting ++ public static final Supplier<SpigotWorldConfig> SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255)) { ++ @Override // override to ensure "verbose" is false ++ public void init() { ++ SpigotConfig.readConfig(SpigotWorldConfig.class, this); ++ } ++ }); ++ public static final ContextKey<Supplier<SpigotWorldConfig>> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken<Supplier<SpigotWorldConfig>>() {}, "spigot world config"); ++ ++ ++ public PaperConfigurations(final Path globalFolder) { ++ super(globalFolder, GlobalConfiguration.class, WorldConfiguration.class, GLOBAL_CONFIG_FILE_NAME, WORLD_DEFAULTS_CONFIG_FILE_NAME, WORLD_CONFIG_FILE_NAME); ++ } ++ ++ @Override ++ protected int globalConfigVersion() { ++ return GlobalConfiguration.CURRENT_VERSION; ++ } ++ ++ @Override ++ protected int worldConfigVersion() { ++ return WorldConfiguration.CURRENT_VERSION; ++ } ++ ++ @Override ++ protected YamlConfigurationLoader.Builder createLoaderBuilder() { ++ return super.createLoaderBuilder() ++ .defaultOptions(PaperConfigurations::defaultOptions); ++ } ++ ++ 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 ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() { ++ return defaultGlobalFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder()); ++ } ++ ++ private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMapper.Factory.Builder builder) { ++ return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig()); ++ } ++ ++ @Override ++ protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() { ++ return super.createGlobalLoaderBuilder() ++ .defaultOptions(PaperConfigurations::defaultGlobalOptions); ++ } ++ ++ private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) { ++ return options ++ .header(GLOBAL_HEADER) ++ .serializers(builder -> builder ++ .register(new PacketClassSerializer()) ++ .register(IntOr.Default.SERIALIZER) ++ ); ++ } ++ ++ @Override ++ public GlobalConfiguration initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException { ++ GlobalConfiguration configuration = super.initializeGlobalConfiguration(registryAccess); ++ GlobalConfiguration.set(configuration); ++ return configuration; ++ } ++ ++ @Override ++ protected ContextMap.Builder createDefaultContextMap(final RegistryAccess registryAccess) { ++ return super.createDefaultContextMap(registryAccess) ++ .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, SPIGOT_WORLD_DEFAULTS); ++ } ++ ++ @Override ++ protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) { ++ return super.createWorldObjectMapperFactoryBuilder(contextMap) ++ .addNodeResolver(new RequiresSpigotInitialization.Factory(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get())) ++ .addNodeResolver(new NestedSetting.Factory()) ++ .addDiscoverer(InnerClassFieldDiscoverer.worldConfig(createWorldConfigInstance(contextMap))); ++ } ++ ++ private static WorldConfiguration createWorldConfigInstance(ContextMap contextMap) { ++ return new WorldConfiguration( ++ contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), ++ contextMap.require(Configurations.WORLD_KEY) ++ ); ++ } ++ ++ @Override ++ protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) { ++ final RegistryAccess access = contextMap.require(REGISTRY_ACCESS); ++ return super.createWorldConfigLoaderBuilder(contextMap) ++ .defaultOptions(options -> options ++ .header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap)) ++ .serializers(serializers -> serializers ++ .register(new TypeToken<Reference2IntMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2IntMap<?>>(Reference2IntOpenHashMap::new, Integer.TYPE)) ++ .register(new TypeToken<Reference2LongMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2LongMap<?>>(Reference2LongOpenHashMap::new, Long.TYPE)) ++ .register(new TypeToken<Table<?, ?, ?>>() {}, new TableSerializer()) ++ .register(StringRepresentableSerializer::isValidFor, new StringRepresentableSerializer()) ++ .register(IntOr.Default.SERIALIZER) ++ .register(IntOr.Disabled.SERIALIZER) ++ .register(DoubleOr.Default.SERIALIZER) ++ .register(BooleanOrDefault.SERIALIZER) ++ .register(Duration.SERIALIZER) ++ .register(DurationOrDisabled.SERIALIZER) ++ .register(EngineMode.SERIALIZER) ++ .register(NbtPathSerializer.SERIALIZER) ++ .register(FallbackValueSerializer.create(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), MinecraftServer::getServer)) ++ .register(new RegistryValueSerializer<>(new TypeToken<EntityType<?>>() {}, access, Registries.ENTITY_TYPE, true)) ++ .register(new RegistryValueSerializer<>(Item.class, access, Registries.ITEM, true)) ++ .register(new RegistryValueSerializer<>(Block.class, access, Registries.BLOCK, true)) ++ .register(new RegistryHolderSerializer<>(new TypeToken<ConfiguredFeature<?, ?>>() {}, access, Registries.CONFIGURED_FEATURE, false)) ++ ) ++ ); ++ } ++ ++ @Override ++ protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node, final @Nullable ConfigurationNode defaultsNode) throws ConfigurateException { ++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder(); ++ for (final NodePath path : RemovedConfigurations.REMOVED_WORLD_PATHS) { ++ builder.addAction(path, TransformAction.remove()); ++ } ++ builder.build().apply(node); ++ ++ final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder(); ++ V29_ZeroWorldHeight.apply(versionedBuilder); ++ V30_RenameFilterNbtFromSpawnEgg.apply(versionedBuilder); ++ V31_SpawnLoadedRangeToGameRule.apply(versionedBuilder, contextMap, defaultsNode); ++ // ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE ++ versionedBuilder.build().apply(node); ++ } ++ ++ @Override ++ protected void applyGlobalConfigTransformations(ConfigurationNode node) throws ConfigurateException { ++ ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder(); ++ for (NodePath path : RemovedConfigurations.REMOVED_GLOBAL_PATHS) { ++ builder.addAction(path, TransformAction.remove()); ++ } ++ builder.build().apply(node); ++ ++ final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder(); ++ V29_LogIPs.apply(versionedBuilder); ++ // ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE ++ versionedBuilder.build().apply(node); ++ } ++ ++ private static final List<Transformations.DefaultsAware> DEFAULT_AWARE_TRANSFORMATIONS = List.of( ++ FeatureSeedsGeneration::apply ++ ); ++ ++ @Override ++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException { ++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder(); ++ // ADD FUTURE TRANSFORMS HERE (these transforms run after the defaults have been merged into the node) ++ DEFAULT_AWARE_TRANSFORMATIONS.forEach(transform -> transform.apply(builder, contextMap, defaultsNode)); ++ builder.build().apply(worldNode); ++ } ++ ++ @Override ++ public WorldConfiguration createWorldConfig(final ContextMap contextMap) { ++ final String levelName = contextMap.require(WORLD_NAME); ++ try { ++ return super.createWorldConfig(contextMap); ++ } catch (IOException exception) { ++ throw new RuntimeException("Could not create world config for " + levelName, exception); ++ } ++ } ++ ++ @Override ++ protected boolean isConfigType(final Type type) { ++ return ConfigurationPart.class.isAssignableFrom(erase(type)); ++ } ++ ++ public void reloadConfigs(MinecraftServer server) { ++ try { ++ this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get())); ++ this.initializeWorldDefaultsConfiguration(server.registryAccess()); ++ for (ServerLevel level : server.getAllLevels()) { ++ this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig())); ++ } ++ } catch (Exception ex) { ++ throw new RuntimeException("Could not reload paper configuration files", ex); ++ } ++ } ++ ++ private static ContextMap createWorldContextMap(ServerLevel level) { ++ return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig, level.registryAccess(), level.getGameRules()); ++ } ++ ++ public static ContextMap createWorldContextMap(final Path dir, final String levelName, final ResourceLocation worldKey, final SpigotWorldConfig spigotConfig, final RegistryAccess registryAccess, final GameRules gameRules) { ++ return ContextMap.builder() ++ .put(WORLD_DIRECTORY, dir) ++ .put(WORLD_NAME, levelName) ++ .put(WORLD_KEY, worldKey) ++ .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, Suppliers.ofInstance(spigotConfig)) ++ .put(REGISTRY_ACCESS, registryAccess) ++ .put(GAME_RULES, gameRules) ++ .build(); ++ } ++ ++ public static PaperConfigurations setup(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception { ++ final Path legacy = Files.isSymbolicLink(legacyConfig) ? Files.readSymbolicLink(legacyConfig) : legacyConfig; ++ if (needsConverting(legacyConfig)) { ++ final String legacyFileName = legacyConfig.getFileName().toString(); ++ try { ++ if (Files.exists(configDir) && !Files.isDirectory(configDir)) { ++ throw new RuntimeException("Paper needs to create a '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + configDir.toAbsolutePath() + "'. Please remove it and restart the server."); ++ } ++ final Path backupDir = configDir.resolve(BACKUP_DIR); ++ if (Files.exists(backupDir) && !Files.isDirectory(backupDir)) { ++ throw new RuntimeException("Paper needs to create a '" + BACKUP_DIR + "' directory in the '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + BACKUP_DIR + "'. Please remove it and restart the server."); ++ } ++ createDirectoriesSymlinkAware(backupDir); ++ final String backupFileName = legacyFileName + ".old"; ++ final Path legacyConfigBackup = backupDir.resolve(backupFileName); ++ if (Files.exists(legacyConfigBackup) && !Files.isRegularFile(legacyConfigBackup)) { ++ throw new RuntimeException("Paper needs to create a '" + backupFileName + "' file in the '" + backupDir.toAbsolutePath() + "' folder. You already have a non-file named '" + backupFileName + "'. Please remove it and restart the server."); ++ } ++ Files.move(legacyConfig.toRealPath(), legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup ++ if (Files.isSymbolicLink(legacyConfig)) { ++ Files.delete(legacyConfig); ++ } ++ final Path replacementFile = legacy.resolveSibling(legacyFileName + "-README.txt"); ++ if (Files.notExists(replacementFile)) { ++ Files.createFile(replacementFile); ++ Files.writeString(replacementFile, String.format(MOVED_NOTICE, configDir.toAbsolutePath())); ++ } ++ convert(legacyConfigBackup, configDir, worldFolder, spigotConfig); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Could not convert '" + legacyFileName + "' to the new configuration format", ex); ++ } ++ } ++ try { ++ createDirectoriesSymlinkAware(configDir); ++ return new PaperConfigurations(configDir); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Could not setup PaperConfigurations", ex); ++ } ++ } ++ ++ private static void convert(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception { ++ createDirectoriesSymlinkAware(configDir); ++ ++ final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig); ++ final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME)); ++ final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME)); ++ ++ final ConfigurationNode legacy = legacyLoader.load(); ++ checkState(!legacy.virtual(), "can't be virtual"); ++ final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt(); ++ ++ final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy(); ++ checkState(!legacyWorldSettings.virtual(), "can't be virtual"); ++ legacy.removeChild("world-settings"); ++ ++ // Apply legacy transformations before settings flatten ++ final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation ++ LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy); ++ spigotConfiguration.save(spigotConfig); ++ legacy.mergeFrom(legacy.node("settings")); // flatten "settings" to root ++ legacy.removeChild("settings"); ++ LegacyPaperConfig.toNewFormat().apply(legacy); ++ globalLoader.save(legacy); // save converted node to new global location ++ ++ final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy(); ++ checkState(!worldDefaults.virtual()); ++ worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version); ++ legacyWorldSettings.removeChild("default"); ++ LegacyPaperWorldConfig.transformation().apply(worldDefaults); ++ LegacyPaperWorldConfig.toNewFormat().apply(worldDefaults); ++ worldDefaultsLoader.save(worldDefaults); ++ ++ legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> { ++ try { ++ legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version); ++ LegacyPaperWorldConfig.transformation().apply(legacyWorldNode); ++ LegacyPaperWorldConfig.toNewFormat().apply(legacyWorldNode); ++ ConfigurationLoaders.naturallySortedWithoutHeader(worldFolder.resolve(world.toString()).resolve(WORLD_CONFIG_FILE_NAME)).save(legacyWorldNode); // save converted node to new location ++ } catch (final ConfigurateException ex) { ++ ex.printStackTrace(); ++ } ++ }); ++ } ++ ++ private static boolean needsConverting(final Path legacyConfig) { ++ return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig); ++ } ++ ++ @Deprecated ++ public YamlConfiguration createLegacyObject(final MinecraftServer server) { ++ YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); ++ ConfigurationSection worlds = global.createSection("__________WORLDS__________"); ++ worlds.set("__defaults__", YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); ++ for (ServerLevel level : server.getAllLevels()) { ++ worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); ++ } ++ return global; ++ } ++ ++ @Deprecated ++ public static YamlConfiguration loadLegacyConfigFile(File configFile) throws Exception { ++ YamlConfiguration config = new YamlConfiguration(); ++ if (configFile.exists()) { ++ try { ++ config.load(configFile); ++ } catch (Exception ex) { ++ throw new Exception("Failed to load configuration file: " + configFile.getName(), ex); ++ } ++ } ++ return config; ++ } ++ ++ @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); ++ } ++ ++ // Symlinks are not correctly checked in createDirectories ++ static void createDirectoriesSymlinkAware(Path path) throws IOException { ++ if (!Files.isDirectory(path)) { ++ Files.createDirectories(path); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..990d1bb46e0f9719f4e9af928d80ac6f8dff23b5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java +@@ -0,0 +1,82 @@ ++package io.papermc.paper.configuration; ++ ++import org.spongepowered.configurate.NodePath; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++interface RemovedConfigurations { ++ ++ NodePath[] REMOVED_WORLD_PATHS = { ++ path("falling-blocks-collide-with-signs"), ++ path("fast-drain"), ++ path("lava-flow-speed"), ++ path("load-chunks"), ++ path("misc", "boats-drop-boats"), ++ path("player-exhaustion"), ++ path("remove-unloaded"), ++ path("tick-next-tick-list-cap"), ++ path("tick-next-tick-list-cap-ignores-redstone"), ++ path("elytra-hit-wall-damage"), ++ path("queue-light-updates"), ++ path("save-queue-limit-for-auto-save"), ++ path("max-chunk-sends-per-tick"), ++ path("max-chunk-gens-per-tick"), ++ path("fire-physics-event-for-redstone"), ++ path("fix-zero-tick-instant-grow-farms"), ++ path("bed-search-radius"), ++ path("lightning-strike-distance-limit"), ++ path("fix-wither-targeting-bug"), ++ path("remove-corrupt-tile-entities"), ++ path("allow-undead-horse-leashing"), ++ path("reset-arrow-despawn-timer-on-fall"), ++ path("seed-based-feature-search"), ++ path("seed-based-feature-search-loads-chunks"), ++ path("viewdistances", "no-tick-view-distance"), ++ path("seed-based-feature-search"), // unneeded as of 1.18 ++ path("seed-based-feature-search-loads-chunks"), // unneeded as of 1.18 ++ path("reset-arrow-despawn-timer-on-fall"), ++ path("squid-spawn-height"), ++ path("viewdistances"), ++ path("use-alternate-fallingblock-onGround-detection"), ++ path("skip-entity-ticking-in-chunks-scheduled-for-unload"), ++ path("tracker-update-distance"), ++ path("allow-block-location-tab-completion"), ++ path("cache-chunk-maps"), ++ path("disable-mood-sounds"), ++ path("fix-cannons"), ++ path("player-blocking-damage-multiplier"), ++ path("remove-invalid-mob-spawner-tile-entities"), ++ path("use-hopper-check"), ++ path("use-async-lighting"), ++ path("tnt-explosion-volume"), ++ path("entities", "spawning", "despawn-ranges", "soft"), ++ path("entities", "spawning", "despawn-ranges", "hard"), ++ path("fixes", "fix-curing-zombie-villager-discount-exploit"), ++ path("entities", "mob-effects", "undead-immune-to-certain-effects") ++ }; ++ // spawn.keep-spawn-loaded and spawn.keep-spawn-loaded-range are no longer used, but kept ++ // in the world default config for compatibility with old worlds being migrated to use the gamerule ++ ++ NodePath[] REMOVED_GLOBAL_PATHS = { ++ path("data-value-allowed-items"), ++ path("effect-modifiers"), ++ path("stackable-buckets"), ++ path("async-chunks"), ++ path("queue-light-updates-max-loss"), ++ path("sleep-between-chunk-saves"), ++ path("remove-invalid-statistics"), ++ path("min-chunk-load-threads"), ++ path("use-versioned-world"), ++ path("save-player-data"), // to spigot (converted) ++ path("log-named-entity-deaths"), // default in vanilla ++ path("chunk-tasks-per-tick"), // removed in tuinity merge ++ path("item-validation", "loc-name"), ++ path("commandErrorMessage"), ++ path("baby-zombie-movement-speed"), ++ path("limit-player-interactions"), ++ path("warnWhenSettingExcessiveVelocity"), ++ path("logging", "use-rgb-for-named-text-colors"), ++ path("unsupported-settings", "allow-grindstone-overstacking") ++ }; ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..efc91ff91827872c62b8bd060282549ccdcf67dd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +@@ -0,0 +1,550 @@ ++package io.papermc.paper.configuration; ++ ++import com.google.common.collect.HashBasedTable; ++import com.google.common.collect.Table; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer; ++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; ++import io.papermc.paper.configuration.mapping.MergeMap; ++import io.papermc.paper.configuration.serializer.NbtPathSerializer; ++import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; ++import io.papermc.paper.configuration.type.BooleanOrDefault; ++import io.papermc.paper.configuration.type.Duration; ++import io.papermc.paper.configuration.type.DurationOrDisabled; ++import io.papermc.paper.configuration.type.EngineMode; ++import io.papermc.paper.configuration.type.fallback.ArrowDespawnRate; ++import io.papermc.paper.configuration.type.fallback.AutosavePeriod; ++import io.papermc.paper.configuration.type.number.BelowZeroToEmpty; ++import io.papermc.paper.configuration.type.number.DoubleOr; ++import io.papermc.paper.configuration.type.number.IntOr; ++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 java.util.Arrays; ++import java.util.IdentityHashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.function.Function; ++import java.util.stream.Collectors; ++import net.minecraft.Util; ++import net.minecraft.commands.arguments.NbtPathArgument; ++import net.minecraft.core.Holder; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.world.Difficulty; ++import net.minecraft.world.entity.Display; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.ExperienceOrb; ++import net.minecraft.world.entity.MobCategory; ++import net.minecraft.world.entity.boss.enderdragon.EnderDragon; ++import net.minecraft.world.entity.decoration.HangingEntity; ++import net.minecraft.world.entity.item.ItemEntity; ++import net.minecraft.world.entity.monster.Vindicator; ++import net.minecraft.world.entity.monster.Zombie; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.NaturalSpawner; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; ++import org.slf4j.Logger; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.meta.PostProcess; ++import org.spongepowered.configurate.objectmapping.meta.Required; ++import org.spongepowered.configurate.objectmapping.meta.Setting; ++ ++@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) ++public class WorldConfiguration extends ConfigurationPart { ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ static final int CURRENT_VERSION = 31; // (when you change the version, change the comment, so it conflicts on rebases): migrate spawn loaded configs to gamerule ++ ++ private final transient SpigotWorldConfig spigotConfig; ++ private final transient ResourceLocation worldKey; ++ ++ WorldConfiguration(final SpigotWorldConfig spigotConfig, final ResourceLocation worldKey) { ++ this.spigotConfig = spigotConfig; ++ this.worldKey = worldKey; ++ } ++ ++ public boolean isDefault() { ++ return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY); ++ } ++ ++ @Setting(Configuration.VERSION_FIELD) ++ public int version = CURRENT_VERSION; ++ ++ public Anticheat anticheat; ++ ++ public class Anticheat extends ConfigurationPart { ++ ++ public Obfuscation obfuscation; ++ ++ public class Obfuscation extends ConfigurationPart { ++ public Items items = new Items(); ++ public class Items extends ConfigurationPart { ++ public boolean hideItemmeta = false; ++ public boolean hideDurability = false; ++ public boolean hideItemmetaWithVisualEffects = false; ++ } ++ } ++ ++ public AntiXray antiXray; ++ ++ public class AntiXray extends ConfigurationPart { ++ public boolean enabled = false; ++ public EngineMode engineMode = EngineMode.HIDE; ++ public int maxBlockHeight = 64; ++ public int updateRadius = 2; ++ public boolean lavaObscures = false; ++ public boolean usePermission = false; ++ public List<Block> hiddenBlocks = List.of( ++ //<editor-fold desc="Anti-Xray Hidden Blocks" defaultstate="collapsed"> ++ Blocks.COPPER_ORE, ++ Blocks.DEEPSLATE_COPPER_ORE, ++ Blocks.RAW_COPPER_BLOCK, ++ Blocks.GOLD_ORE, ++ Blocks.DEEPSLATE_GOLD_ORE, ++ Blocks.IRON_ORE, ++ Blocks.DEEPSLATE_IRON_ORE, ++ Blocks.RAW_IRON_BLOCK, ++ Blocks.COAL_ORE, ++ Blocks.DEEPSLATE_COAL_ORE, ++ Blocks.LAPIS_ORE, ++ Blocks.DEEPSLATE_LAPIS_ORE, ++ Blocks.MOSSY_COBBLESTONE, ++ Blocks.OBSIDIAN, ++ Blocks.CHEST, ++ Blocks.DIAMOND_ORE, ++ Blocks.DEEPSLATE_DIAMOND_ORE, ++ Blocks.REDSTONE_ORE, ++ Blocks.DEEPSLATE_REDSTONE_ORE, ++ Blocks.CLAY, ++ Blocks.EMERALD_ORE, ++ Blocks.DEEPSLATE_EMERALD_ORE, ++ Blocks.ENDER_CHEST ++ //</editor-fold> ++ ); ++ public List<Block> replacementBlocks = List.of(Blocks.STONE, Blocks.OAK_PLANKS, Blocks.DEEPSLATE); ++ } ++ } ++ ++ public Entities entities; ++ ++ public class Entities extends ConfigurationPart { ++ public boolean entitiesTargetWithFollowRange = false; ++ public MobEffects mobEffects; ++ ++ public class MobEffects extends ConfigurationPart { ++ public boolean spidersImmuneToPoisonEffect = true; ++ public ImmuneToWitherEffect immuneToWitherEffect; ++ ++ public class ImmuneToWitherEffect extends ConfigurationPart { ++ public boolean wither = true; ++ public boolean witherSkeleton = true; ++ } ++ } ++ ++ public ArmorStands armorStands; ++ ++ public class ArmorStands extends ConfigurationPart { ++ public boolean doCollisionEntityLookups = true; ++ public boolean tick = true; ++ } ++ ++ public Markers markers; ++ ++ public class Markers extends ConfigurationPart { ++ public boolean tick = true; ++ } ++ ++ public Sniffer sniffer; ++ ++ public class Sniffer extends ConfigurationPart { ++ public IntOr.Default hatchTime = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default boostedHatchTime = IntOr.Default.USE_DEFAULT; ++ } ++ ++ public Spawning spawning; ++ ++ public class Spawning extends ConfigurationPart { ++ public ArrowDespawnRate nonPlayerArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig); ++ public ArrowDespawnRate creativeArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig); ++ public boolean filterBadTileEntityNbtFromFallingBlocks = true; ++ public List<NbtPathArgument.NbtPath> filteredEntityTagNbtPaths = NbtPathSerializer.fromString(List.of("Pos", "Motion", "SleepingX", "SleepingY", "SleepingZ")); ++ public boolean disableMobSpawnerSpawnEggTransformation = false; ++ public boolean perPlayerMobSpawns = true; ++ public boolean scanForLegacyEnderDragon = true; ++ @MergeMap ++ public Reference2IntMap<MobCategory> spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1))); ++ @MergeMap ++ public Map<MobCategory, DespawnRange> despawnRanges = Arrays.stream(MobCategory.values()).collect(Collectors.toMap(Function.identity(), category -> new DespawnRange(category.getNoDespawnDistance(), category.getDespawnDistance()))); ++ @MergeMap ++ public Reference2IntMap<MobCategory> ticksPerSpawn = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1))); ++ ++ @ConfigSerializable ++ public record DespawnRange(@Required int soft, @Required int hard) { ++ } ++ ++ public WaterAnimalSpawnHeight wateranimalSpawnHeight; ++ ++ public class WaterAnimalSpawnHeight extends ConfigurationPart { ++ public IntOr.Default maximum = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default minimum = IntOr.Default.USE_DEFAULT; ++ } ++ ++ public SlimeSpawnHeight slimeSpawnHeight; ++ ++ public class SlimeSpawnHeight extends ConfigurationPart { ++ ++ public SurfaceSpawnableSlimeBiome surfaceBiome; ++ ++ public class SurfaceSpawnableSlimeBiome extends ConfigurationPart { ++ public double maximum = 70; ++ public double minimum = 50; ++ } ++ ++ public SlimeChunk slimeChunk; ++ ++ public class SlimeChunk extends ConfigurationPart { ++ public double maximum = 40; ++ } ++ } ++ ++ public WanderingTrader wanderingTrader; ++ ++ public class WanderingTrader extends ConfigurationPart { ++ public int spawnMinuteLength = 1200; ++ public int spawnDayLength = 24000; ++ public int spawnChanceFailureIncrement = 25; ++ public int spawnChanceMin = 25; ++ public int spawnChanceMax = 75; ++ } ++ ++ public boolean allChunksAreSlimeChunks = false; ++ @BelowZeroToEmpty ++ public DoubleOr.Default skeletonHorseThunderSpawnChance = DoubleOr.Default.USE_DEFAULT; ++ public boolean ironGolemsCanSpawnInAir = false; ++ public boolean countAllMobsForSpawning = false; ++ @BelowZeroToEmpty ++ public IntOr.Default monsterSpawnMaxLightLevel = IntOr.Default.USE_DEFAULT; ++ public DuplicateUUID duplicateUuid; ++ ++ public class DuplicateUUID extends ConfigurationPart { ++ public DuplicateUUIDMode mode = DuplicateUUIDMode.SAFE_REGEN; ++ public int safeRegenDeleteRange = 32; ++ ++ public enum DuplicateUUIDMode { ++ SAFE_REGEN, DELETE, NOTHING, WARN; ++ } ++ } ++ public AltItemDespawnRate altItemDespawnRate; ++ ++ public class AltItemDespawnRate extends ConfigurationPart { ++ public boolean enabled = false; ++ public Reference2IntMap<Item> items = new Reference2IntOpenHashMap<>(Map.of(Items.COBBLESTONE, 300)); ++ } ++ } ++ ++ public Behavior behavior; ++ ++ public class Behavior extends ConfigurationPart { ++ public boolean disableChestCatDetection = false; ++ public boolean spawnerNerfedMobsShouldJump = false; ++ public int experienceMergeMaxValue = -1; ++ public boolean shouldRemoveDragon = false; ++ public boolean zombiesTargetTurtleEggs = true; ++ public boolean piglinsGuardChests = true; ++ public double babyZombieMovementModifier = 0.5; ++ public boolean allowSpiderWorldBorderClimbing = true; ++ ++ private static final List<EntityType<?>> ZOMBIE_LIKE = List.of(EntityType.ZOMBIE, EntityType.HUSK, EntityType.ZOMBIE_VILLAGER, EntityType.ZOMBIFIED_PIGLIN); ++ @MergeMap ++ public Map<EntityType<?>, List<Difficulty>> doorBreakingDifficulty = Util.make(new IdentityHashMap<>(), map -> { ++ for (final EntityType<?> type : ZOMBIE_LIKE) { ++ map.put(type, Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList()); ++ } ++ map.put(EntityType.VINDICATOR, Arrays.stream(Difficulty.values()).filter(Vindicator.DOOR_BREAKING_PREDICATE).toList()); ++ }); ++ ++ public boolean disableCreeperLingeringEffect = false; ++ public boolean enderDragonsDeathAlwaysPlacesDragonEgg = false; ++ public boolean phantomsDoNotSpawnOnCreativePlayers = true; ++ public boolean phantomsOnlyAttackInsomniacs = true; ++ public int playerInsomniaStartTicks = 72000; ++ public int phantomsSpawnAttemptMinSeconds = 60; ++ public int phantomsSpawnAttemptMaxSeconds = 119; ++ public boolean parrotsAreUnaffectedByPlayerMovement = false; ++ @BelowZeroToEmpty ++ public DoubleOr.Default zombieVillagerInfectionChance = DoubleOr.Default.USE_DEFAULT; ++ public MobsCanAlwaysPickUpLoot mobsCanAlwaysPickUpLoot; ++ ++ public class MobsCanAlwaysPickUpLoot extends ConfigurationPart { ++ public boolean zombies = false; ++ public boolean skeletons = false; ++ } ++ ++ public boolean disablePlayerCrits = false; ++ public boolean nerfPigmenFromNetherPortals = false; ++ public PillagerPatrols pillagerPatrols; ++ ++ public class PillagerPatrols extends ConfigurationPart { ++ public boolean disable = false; ++ public double spawnChance = 0.2; ++ public SpawnDelay spawnDelay; ++ public Start start; ++ ++ public class SpawnDelay extends ConfigurationPart { ++ public boolean perPlayer = false; ++ public int ticks = 12000; ++ } ++ ++ public class Start extends ConfigurationPart { ++ public boolean perPlayer = false; ++ public int day = 5; ++ } ++ } ++ } ++ ++ public TrackingRangeY trackingRangeY; ++ ++ public class TrackingRangeY extends ConfigurationPart { ++ public boolean enabled = false; ++ public IntOr.Default player = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default animal = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default monster = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default misc = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default display = IntOr.Default.USE_DEFAULT; ++ public IntOr.Default other = IntOr.Default.USE_DEFAULT; ++ ++ public int get(Entity entity, int def) { ++ if (entity instanceof EnderDragon) { ++ return -1; // Ender dragon is exempt ++ } else if (entity instanceof Display) { ++ return display.or(def); ++ } else if (entity instanceof Player) { ++ return player.or(def); ++ } else if (entity instanceof HangingEntity || entity instanceof ItemEntity || entity instanceof ExperienceOrb) { ++ return misc.or(def); ++ } ++ switch (entity.activationType) { ++ case ANIMAL, WATER, VILLAGER -> { ++ return animal.or(def); ++ } ++ case MONSTER, FLYING_MONSTER, RAIDER -> { ++ return monster.or(def); ++ } ++ default -> { ++ return other.or(def); ++ } ++ } ++ } ++ } ++ } ++ ++ public Lootables lootables; ++ ++ public class Lootables extends ConfigurationPart { ++ public boolean autoReplenish = false; ++ public boolean restrictPlayerReloot = true; ++ public DurationOrDisabled restrictPlayerRelootTime = DurationOrDisabled.USE_DISABLED; ++ public boolean resetSeedOnFill = true; ++ public int maxRefills = -1; ++ public Duration refreshMin = Duration.of("12h"); ++ public Duration refreshMax = Duration.of("2d"); ++ } ++ ++ public MaxGrowthHeight maxGrowthHeight; ++ ++ public class MaxGrowthHeight extends ConfigurationPart { ++ public int cactus = 3; ++ public int reeds = 3; ++ public Bamboo bamboo; ++ ++ public class Bamboo extends ConfigurationPart { ++ public int max = 16; ++ public int min = 11; ++ } ++ } ++ ++ public Scoreboards scoreboards; ++ ++ public class Scoreboards extends ConfigurationPart { ++ public boolean allowNonPlayerEntitiesOnScoreboards = true; ++ public boolean useVanillaWorldScoreboardNameColoring = false; ++ } ++ ++ public Environment environment; ++ ++ public class Environment extends ConfigurationPart { ++ public boolean disableThunder = false; ++ public boolean disableIceAndSnow = false; ++ public boolean optimizeExplosions = false; ++ public boolean disableExplosionKnockback = false; ++ public boolean generateFlatBedrock = false; ++ public FrostedIce frostedIce; ++ ++ public class FrostedIce extends ConfigurationPart { ++ public boolean enabled = true; ++ public Delay delay; ++ ++ public class Delay extends ConfigurationPart { ++ public int min = 20; ++ public int max = 40; ++ } ++ } ++ ++ public TreasureMaps treasureMaps; ++ public class TreasureMaps extends ConfigurationPart { ++ public boolean enabled = true; ++ @NestedSetting({"find-already-discovered", "villager-trade"}) ++ public boolean findAlreadyDiscoveredVillager = false; ++ @NestedSetting({"find-already-discovered", "loot-tables"}) ++ public BooleanOrDefault findAlreadyDiscoveredLootTable = BooleanOrDefault.USE_DEFAULT; ++ } ++ ++ public int fireTickDelay = 30; ++ public int waterOverLavaFlowSpeed = 5; ++ public int portalSearchRadius = 128; ++ public int portalCreateRadius = 16; ++ public boolean portalSearchVanillaDimensionScaling = true; ++ public boolean disableTeleportationSuffocationCheck = false; ++ public IntOr.Disabled netherCeilingVoidDamageHeight = IntOr.Disabled.DISABLED; ++ public int maxFluidTicks = 65536; ++ public int maxBlockTicks = 65536; ++ public boolean locateStructuresOutsideWorldBorder = false; ++ } ++ ++ public Spawn spawn; ++ ++ public class Spawn extends ConfigurationPart { ++ public boolean allowUsingSignsInsideSpawnProtection = false; ++ } ++ ++ public Maps maps; ++ ++ public class Maps extends ConfigurationPart { ++ public int itemFrameCursorLimit = 128; ++ public int itemFrameCursorUpdateInterval = 10; ++ } ++ ++ public Fixes fixes; ++ ++ public class Fixes extends ConfigurationPart { ++ public boolean fixItemsMergingThroughWalls = false; ++ public boolean disableUnloadedChunkEnderpearlExploit = true; ++ public boolean preventTntFromMovingInWater = false; ++ public boolean splitOverstackedLoot = true; ++ public IntOr.Disabled fallingBlockHeightNerf = IntOr.Disabled.DISABLED; ++ public IntOr.Disabled tntEntityHeightNerf = IntOr.Disabled.DISABLED; ++ } ++ ++ public UnsupportedSettings unsupportedSettings; ++ ++ public class UnsupportedSettings extends ConfigurationPart { ++ public boolean fixInvulnerableEndCrystalExploit = true; ++ public boolean disableWorldTickingWhenEmpty = false; ++ } ++ ++ public Hopper hopper; ++ ++ public class Hopper extends ConfigurationPart { ++ public boolean cooldownWhenFull = true; ++ public boolean disableMoveEvent = false; ++ public boolean ignoreOccludingBlocks = false; ++ } ++ ++ public Collisions collisions; ++ ++ public class Collisions extends ConfigurationPart { ++ public boolean onlyPlayersCollide = false; ++ public boolean allowVehicleCollisions = true; ++ public boolean fixClimbingBypassingCrammingRule = false; ++ @RequiresSpigotInitialization(MaxEntityCollisionsInitializer.class) ++ public int maxEntityCollisions = 8; ++ public boolean allowPlayerCrammingDamage = false; ++ } ++ ++ public Chunks chunks; ++ ++ public class Chunks extends ConfigurationPart { ++ public AutosavePeriod autoSaveInterval = AutosavePeriod.def(); ++ public int maxAutoSaveChunksPerTick = 24; ++ public int fixedChunkInhabitedTime = -1; ++ public boolean preventMovingIntoUnloadedChunks = false; ++ public Duration delayChunkUnloadsBy = Duration.of("10s"); ++ public Reference2IntMap<EntityType<?>> entityPerChunkSaveLimit = Util.make(new Reference2IntOpenHashMap<>(BuiltInRegistries.ENTITY_TYPE.size()), map -> { ++ map.defaultReturnValue(-1); ++ map.put(EntityType.EXPERIENCE_ORB, -1); ++ map.put(EntityType.SNOWBALL, -1); ++ map.put(EntityType.ENDER_PEARL, -1); ++ map.put(EntityType.ARROW, -1); ++ map.put(EntityType.FIREBALL, -1); ++ map.put(EntityType.SMALL_FIREBALL, -1); ++ }); ++ public boolean flushRegionsOnSave = false; ++ } ++ ++ public FishingTimeRange fishingTimeRange; ++ ++ public class FishingTimeRange extends ConfigurationPart { ++ public int minimum = 100; ++ public int maximum = 600; ++ } ++ ++ public TickRates tickRates; ++ ++ public class TickRates extends ConfigurationPart { ++ public int grassSpread = 1; ++ public int containerUpdate = 1; ++ public int mobSpawner = 1; ++ public int wetFarmland = 1; ++ public int dryFarmland = 1; ++ public Table<EntityType<?>, String, Integer> sensor = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "secondarypoisensor", 40)); ++ public Table<EntityType<?>, String, Integer> behavior = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "validatenearbypoi", -1)); ++ } ++ ++ @Setting(FeatureSeedsGeneration.FEATURE_SEEDS_KEY) ++ public FeatureSeeds featureSeeds; ++ ++ public class FeatureSeeds extends ConfigurationPart { ++ @SuppressWarnings("unused") // Is used in FeatureSeedsGeneration ++ @Setting(FeatureSeedsGeneration.GENERATE_KEY) ++ public boolean generateRandomSeedsForAll = false; ++ @Setting(FeatureSeedsGeneration.FEATURES_KEY) ++ public Reference2LongMap<Holder<ConfiguredFeature<?, ?>>> features = new Reference2LongOpenHashMap<>(); ++ ++ @PostProcess ++ private void postProcess() { ++ this.features.defaultReturnValue(-1); ++ } ++ } ++ ++ public CommandBlocks commandBlocks; ++ ++ public class CommandBlocks extends ConfigurationPart { ++ public int permissionsLevel = 2; ++ public boolean forceFollowPermLevel = true; ++ } ++ ++ public Misc misc; ++ ++ public class Misc extends ConfigurationPart { ++ public int lightQueueSize = 20; ++ public boolean updatePathfindingOnBlockUpdate = true; ++ public boolean showSignClickCommandFailureMsgsToPlayer = false; ++ public RedstoneImplementation redstoneImplementation = RedstoneImplementation.VANILLA; ++ public boolean disableEndCredits = false; ++ public float maxLeashDistance = 10f; ++ public boolean disableSprintInterruptionOnAttack = false; ++ public int shieldBlockingDelay = 5; ++ public boolean disableRelativeProjectileVelocity = false; ++ ++ public enum RedstoneImplementation { ++ VANILLA, EIGENCRAFT, ALTERNATE_CURRENT ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java +new file mode 100644 +index 0000000000000000000000000000000000000000..514be9a11e2ca368ea72dd2bac1b84bff5468814 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.configuration.constraint; ++ ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.Constructor; ++import java.lang.reflect.Type; ++ ++@Documented ++@Retention(RetentionPolicy.RUNTIME) ++@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER}) ++public @interface Constraint { ++ Class<? extends org.spongepowered.configurate.objectmapping.meta.Constraint<?>> value(); ++ ++ class Factory implements org.spongepowered.configurate.objectmapping.meta.Constraint.Factory<Constraint, Object> { ++ @SuppressWarnings("unchecked") ++ @Override ++ public org.spongepowered.configurate.objectmapping.meta.Constraint<Object> make(final Constraint data, final Type type) { ++ try { ++ final Constructor<? extends org.spongepowered.configurate.objectmapping.meta.Constraint<?>> constructor = data.value().getDeclaredConstructor(); ++ constructor.trySetAccessible(); ++ return (org.spongepowered.configurate.objectmapping.meta.Constraint<Object>) constructor.newInstance(); ++ } catch (final ReflectiveOperationException e) { ++ throw new RuntimeException("Could not create constraint", e); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2d8c91007d5ebc051623bb308cf973bdad3f3273 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.configuration.constraint; ++ ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.Type; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.meta.Constraint; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public final class Constraints { ++ private Constraints() { ++ } ++ ++ public static final class Positive implements Constraint<Number> { ++ @Override ++ public void validate(@Nullable Number value) throws SerializationException { ++ if (value != null && value.doubleValue() <= 0) { ++ throw new SerializationException(value + " should be positive"); ++ } ++ } ++ } ++ ++ @Documented ++ @Retention(RetentionPolicy.RUNTIME) ++ @Target(ElementType.FIELD) ++ public @interface Min { ++ int value(); ++ ++ final class Factory implements Constraint.Factory<Min, Number> { ++ @Override ++ public Constraint<Number> make(Min data, Type type) { ++ return value -> { ++ if (value != null && value.intValue() < data.value()) { ++ throw new SerializationException(value + " is less than the min " + data.value()); ++ } ++ }; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java b/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..62b43280f59163f7910f79cc901b50d05cdd024e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java +@@ -0,0 +1,29 @@ ++package io.papermc.paper.configuration.legacy; ++ ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.objectmapping.meta.NodeResolver; ++import org.spongepowered.configurate.util.NamingSchemes; ++ ++public class MaxEntityCollisionsInitializer implements NodeResolver { ++ ++ private final String name; ++ private final SpigotWorldConfig spigotConfig; ++ ++ public MaxEntityCollisionsInitializer(String name, SpigotWorldConfig spigotConfig) { ++ this.name = name; ++ this.spigotConfig = spigotConfig; ++ } ++ ++ @Override ++ public @Nullable ConfigurationNode resolve(ConfigurationNode parent) { ++ final String key = NamingSchemes.LOWER_CASE_DASHED.coerce(this.name); ++ final ConfigurationNode node = parent.node(key); ++ final int old = this.spigotConfig.getInt("max-entity-collisions", -1, false); ++ if (node.virtual() && old > -1) { ++ node.raw(old); ++ } ++ return node; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java b/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java +new file mode 100644 +index 0000000000000000000000000000000000000000..611bdbcef3d52e09179aa8b1677ab1e198c70b02 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java +@@ -0,0 +1,51 @@ ++package io.papermc.paper.configuration.legacy; ++ ++import com.google.common.collect.HashBasedTable; ++import com.google.common.collect.Table; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.objectmapping.meta.NodeResolver; ++ ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.AnnotatedElement; ++import java.lang.reflect.Constructor; ++import java.util.Map; ++import java.util.concurrent.ConcurrentHashMap; ++ ++@Documented ++@Retention(RetentionPolicy.RUNTIME) ++@Target(ElementType.FIELD) ++public @interface RequiresSpigotInitialization { ++ ++ Class<? extends NodeResolver> value(); ++ ++ final class Factory implements NodeResolver.Factory { ++ ++ private final SpigotWorldConfig spigotWorldConfig; ++ private final Table<Class<? extends NodeResolver>, String, NodeResolver> cache = HashBasedTable.create(); ++ ++ public Factory(SpigotWorldConfig spigotWorldConfig) { ++ this.spigotWorldConfig = spigotWorldConfig; ++ } ++ ++ @Override ++ public @Nullable NodeResolver make(String name, AnnotatedElement element) { ++ if (element.isAnnotationPresent(RequiresSpigotInitialization.class)) { ++ return this.cache.row(element.getAnnotation(RequiresSpigotInitialization.class).value()).computeIfAbsent(name, key -> { ++ try { ++ final Constructor<? extends NodeResolver> constructor = element.getAnnotation(RequiresSpigotInitialization.class).value().getDeclaredConstructor(String.class, SpigotWorldConfig.class); ++ constructor.trySetAccessible(); ++ return constructor.newInstance(key, this.spigotWorldConfig); ++ } catch (final ReflectiveOperationException e) { ++ throw new RuntimeException("Could not create constraint", e); ++ } ++ }); ++ } ++ return null; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java b/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fe5cc1c097f8d8c135e6ead6f458426bb84a8ebe +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java +@@ -0,0 +1,27 @@ ++package io.papermc.paper.configuration.legacy; ++ ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.objectmapping.meta.NodeResolver; ++import org.spongepowered.configurate.util.NamingSchemes; ++ ++public final class SpawnLoadedRangeInitializer implements NodeResolver { ++ ++ private final String name; ++ private final SpigotWorldConfig spigotConfig; ++ ++ public SpawnLoadedRangeInitializer(String name, SpigotWorldConfig spigotConfig) { ++ this.name = name; ++ this.spigotConfig = spigotConfig; ++ } ++ ++ @Override ++ public ConfigurationNode resolve(ConfigurationNode parent) { ++ final String key = NamingSchemes.LOWER_CASE_DASHED.coerce(this.name); ++ final ConfigurationNode node = parent.node(key); ++ if (node.virtual()) { ++ node.raw(Math.min(spigotConfig.viewDistance, 10)); ++ } ++ return node; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8f23276796037d048eb114952891a01a40971b3e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java +@@ -0,0 +1,54 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import io.papermc.paper.configuration.ConfigurationPart; ++import io.papermc.paper.configuration.Configurations; ++import io.papermc.paper.configuration.PaperConfigurations; ++import io.papermc.paper.configuration.WorldConfiguration; ++import java.lang.reflect.AnnotatedType; ++import java.lang.reflect.Field; ++import java.util.Collections; ++import java.util.Map; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.FieldDiscoverer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++public final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> { ++ ++ private final InnerClassInstanceSupplier instanceSupplier; ++ private final FieldDiscoverer<Map<Field, Object>> delegate; ++ ++ @SuppressWarnings("unchecked") ++ public InnerClassFieldDiscoverer(final Map<Class<?>, Object> initialOverrides) { ++ this.instanceSupplier = new InnerClassInstanceSupplier(initialOverrides); ++ this.delegate = (FieldDiscoverer<Map<Field, Object>>) FieldDiscoverer.object(this.instanceSupplier); ++ } ++ ++ @Override ++ public @Nullable <V> InstanceFactory<Map<Field, Object>> discover(final AnnotatedType target, final FieldCollector<Map<Field, Object>, V> collector) throws SerializationException { ++ final Class<?> clazz = erase(target.getType()); ++ if (ConfigurationPart.class.isAssignableFrom(clazz)) { ++ final FieldDiscoverer.@Nullable InstanceFactory<Map<Field, Object>> instanceFactoryDelegate = this.delegate.<V>discover(target, (name, type, annotations, deserializer, serializer) -> { ++ if (!erase(type.getType()).equals(clazz.getEnclosingClass())) { // don't collect synth fields for inner classes ++ collector.accept(name, type, annotations, deserializer, serializer); ++ } ++ }); ++ if (instanceFactoryDelegate instanceof MutableInstanceFactory<Map<Field, Object>> mutableInstanceFactoryDelegate) { ++ return new InnerClassInstanceFactory(this.instanceSupplier, mutableInstanceFactoryDelegate, target); ++ } ++ } ++ return null; ++ } ++ ++ public static FieldDiscoverer<?> worldConfig(WorldConfiguration worldConfiguration) { ++ final Map<Class<?>, Object> overrides = Map.of( ++ WorldConfiguration.class, worldConfiguration ++ ); ++ return new InnerClassFieldDiscoverer(overrides); ++ } ++ ++ public static FieldDiscoverer<?> globalConfig() { ++ return new InnerClassFieldDiscoverer(Collections.emptyMap()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cec678ae24a7d99a46fa672be907f4c28fe4da96 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import java.lang.reflect.AnnotatedType; ++import java.lang.reflect.Field; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.Objects; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.FieldDiscoverer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++final class InnerClassInstanceFactory implements FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> { ++ ++ private final InnerClassInstanceSupplier instanceSupplier; ++ private final FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> fallback; ++ private final AnnotatedType targetType; ++ ++ InnerClassInstanceFactory(final InnerClassInstanceSupplier instanceSupplier, final FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> fallback, final AnnotatedType targetType) { ++ this.instanceSupplier = instanceSupplier; ++ this.fallback = fallback; ++ this.targetType = targetType; ++ } ++ ++ @Override ++ public Map<Field, Object> begin() { ++ return this.fallback.begin(); ++ } ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ public void complete(final Object instance, final Map<Field, Object> intermediate) throws SerializationException { ++ final Iterator<Map.Entry<Field, Object>> iter = intermediate.entrySet().iterator(); ++ try { ++ while (iter.hasNext()) { // manually merge any mergeable maps ++ Map.Entry<Field, Object> entry = iter.next(); ++ if (entry.getKey().isAnnotationPresent(MergeMap.class) && Map.class.isAssignableFrom(entry.getKey().getType()) && intermediate.get(entry.getKey()) instanceof Map<?, ?> map) { ++ iter.remove(); ++ @Nullable Map<Object, Object> existingMap = (Map<Object, Object>) entry.getKey().get(instance); ++ if (existingMap != null) { ++ existingMap.putAll(map); ++ } else { ++ entry.getKey().set(instance, entry.getValue()); ++ } ++ } ++ } ++ } catch (final IllegalAccessException e) { ++ throw new SerializationException(this.targetType.getType(), e); ++ } ++ this.fallback.complete(instance, intermediate); ++ } ++ ++ @Override ++ public Object complete(final Map<Field, Object> intermediate) throws SerializationException { ++ final Object targetInstance = Objects.requireNonNull(this.instanceSupplier.instanceMap().get(erase(this.targetType.getType())), () -> this.targetType.getType() + " must already have an instance created"); ++ this.complete(targetInstance, intermediate); ++ return targetInstance; ++ } ++ ++ @Override ++ public boolean canCreateInstances() { ++ return this.fallback.canCreateInstances(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8d8bc050441c02cf65dfcb6400978363d6b8ef10 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java +@@ -0,0 +1,72 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import io.papermc.paper.configuration.ConfigurationPart; ++import java.lang.reflect.AnnotatedType; ++import java.lang.reflect.Constructor; ++import java.lang.reflect.Modifier; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Supplier; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.CheckedFunction; ++import org.spongepowered.configurate.util.CheckedSupplier; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++/** ++ * This instance factory handles creating non-static inner classes by tracking all instances of objects that extend ++ * {@link ConfigurationPart}. Only 1 instance of each {@link ConfigurationPart} should be present for each instance ++ * of the field discoverer this is used in. ++ */ ++final class InnerClassInstanceSupplier implements CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> { ++ ++ private final Map<Class<?>, Object> instanceMap = new HashMap<>(); ++ private final Map<Class<?>, Object> initialOverrides; ++ ++ /** ++ * @param initialOverrides map of types to objects to preload the config objects with. ++ */ ++ InnerClassInstanceSupplier(final Map<Class<?>, Object> initialOverrides) { ++ this.initialOverrides = initialOverrides; ++ } ++ ++ @Override ++ public Supplier<Object> apply(final AnnotatedType target) throws SerializationException { ++ final Class<?> type = erase(target.getType()); ++ if (this.initialOverrides.containsKey(type)) { ++ this.instanceMap.put(type, this.initialOverrides.get(type)); ++ return () -> this.initialOverrides.get(type); ++ } ++ if (ConfigurationPart.class.isAssignableFrom(type) && !this.instanceMap.containsKey(type)) { ++ try { ++ final Constructor<?> constructor; ++ final CheckedSupplier<Object, ReflectiveOperationException> instanceSupplier; ++ if (type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers())) { ++ final @Nullable Object instance = this.instanceMap.get(type.getEnclosingClass()); ++ if (instance == null) { ++ throw new SerializationException("Cannot create a new instance of an inner class " + type.getName() + " without an instance of its enclosing class " + type.getEnclosingClass().getName()); ++ } ++ constructor = type.getDeclaredConstructor(type.getEnclosingClass()); ++ instanceSupplier = () -> constructor.newInstance(instance); ++ } else { ++ constructor = type.getDeclaredConstructor(); ++ instanceSupplier = constructor::newInstance; ++ } ++ constructor.setAccessible(true); ++ final Object instance = instanceSupplier.get(); ++ this.instanceMap.put(type, instance); ++ return () -> instance; ++ } catch (ReflectiveOperationException e) { ++ throw new SerializationException(ConfigurationPart.class, target + " must be a valid ConfigurationPart", e); ++ } ++ } else { ++ throw new SerializationException(target + " must be a valid ConfigurationPart"); ++ } ++ } ++ ++ Map<Class<?>, Object> instanceMap() { ++ return this.instanceMap; ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java b/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..471b161ac51900672434c6608595bb73c02d8180 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import io.papermc.paper.configuration.ConfigurationPart; ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++ ++/** ++ * For use in maps inside {@link ConfigurationPart}s that have default keys that shouldn't be removed by users ++ * <p> ++ * Note that when the config is reloaded, the maps will be merged again, so make sure this map can't accumulate ++ * keys overtime. ++ */ ++@Documented ++@Target(ElementType.FIELD) ++@Retention(RetentionPolicy.RUNTIME) ++public @interface MergeMap { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/package-info.java b/src/main/java/io/papermc/paper/configuration/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4e3bcd7c478096384fcc643d48771ab94318deb3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/package-info.java +@@ -0,0 +1,5 @@ ++@DefaultQualifier(NonNull.class) ++package io.papermc.paper.configuration; ++ ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; +\ No newline at end of file +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9c339ef178ebc3b0251095f320e4a7a3656d3521 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.function.Predicate; ++ ++public class ComponentSerializer extends ScalarSerializer<Component> { ++ ++ public ComponentSerializer() { ++ super(Component.class); ++ } ++ ++ @Override ++ public Component deserialize(Type type, Object obj) throws SerializationException { ++ return MiniMessage.miniMessage().deserialize(obj.toString()); ++ } ++ ++ @Override ++ protected Object serialize(Component component, Predicate<Class<?>> typeSupported) { ++ return MiniMessage.miniMessage().serialize(component); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..27c0679d376bb31ab52131dfea74b3b580ca92b5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java +@@ -0,0 +1,33 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import io.papermc.paper.configuration.type.EngineMode; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.function.Predicate; ++ ++public final class EngineModeSerializer extends ScalarSerializer<EngineMode> { ++ ++ public EngineModeSerializer() { ++ super(EngineMode.class); ++ } ++ ++ @Override ++ public EngineMode deserialize(Type type, Object obj) throws SerializationException { ++ if (obj instanceof Integer id) { ++ try { ++ return EngineMode.valueOf(id); ++ } catch (IllegalArgumentException e) { ++ throw new SerializationException(id + " is not a valid id for type " + type + " for this node"); ++ } ++ } ++ ++ throw new SerializationException(obj + " is not of a valid type " + type + " for this node"); ++ } ++ ++ @Override ++ protected Object serialize(EngineMode item, Predicate<Class<?>> typeSupported) { ++ return item.getId(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8535c748a5e355362e77e6c5103e11c4c318a138 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.EnumLookup; ++ ++import java.lang.reflect.Type; ++import java.util.Arrays; ++import java.util.List; ++import java.util.function.Predicate; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++/** ++ * Enum serializer that lists options if fails and accepts `-` as `_`. ++ */ ++public class EnumValueSerializer extends ScalarSerializer<Enum<?>> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public EnumValueSerializer() { ++ super(new TypeToken<Enum<?>>() {}); ++ } ++ ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ @Override ++ public @Nullable Enum<?> deserialize(final Type type, final Object obj) throws SerializationException { ++ final String enumConstant = obj.toString(); ++ final Class<? extends Enum> typeClass = erase(type).asSubclass(Enum.class); ++ @Nullable Enum<?> ret = EnumLookup.lookupEnum(typeClass, enumConstant); ++ if (ret == null) { ++ ret = EnumLookup.lookupEnum(typeClass, enumConstant.replace("-", "_")); ++ } ++ if (ret == null) { ++ boolean longer = typeClass.getEnumConstants().length > 10; ++ List<String> options = Arrays.stream(typeClass.getEnumConstants()).limit(10L).map(Enum::name).toList(); ++ LOGGER.error("Invalid enum constant provided, expected one of [" + String.join(", " ,options) + (longer ? ", ..." : "") + "], but got " + enumConstant); ++ } ++ return ret; ++ } ++ ++ @Override ++ public Object serialize(final Enum<?> item, final Predicate<Class<?>> typeSupported) { ++ return item.name(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/NbtPathSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/NbtPathSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b44b2dc28f619594e302417848e95c0087acbcea +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/NbtPathSerializer.java +@@ -0,0 +1,52 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import com.destroystokyo.paper.util.SneakyThrow; ++import com.mojang.brigadier.StringReader; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import java.lang.reflect.Type; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Predicate; ++import net.minecraft.commands.arguments.NbtPathArgument; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public class NbtPathSerializer extends ScalarSerializer<NbtPathArgument.NbtPath> { ++ ++ public static final NbtPathSerializer SERIALIZER = new NbtPathSerializer(); ++ private static final NbtPathArgument DUMMY_ARGUMENT = new NbtPathArgument(); ++ ++ private NbtPathSerializer() { ++ super(NbtPathArgument.NbtPath.class); ++ } ++ ++ @Override ++ public NbtPathArgument.NbtPath deserialize(final Type type, final Object obj) throws SerializationException { ++ return fromString(obj.toString()); ++ } ++ ++ @Override ++ protected Object serialize(final NbtPathArgument.NbtPath item, final Predicate<Class<?>> typeSupported) { ++ return item.toString(); ++ } ++ ++ public static List<NbtPathArgument.NbtPath> fromString(final List<String> tags) { ++ List<NbtPathArgument.NbtPath> paths = new ArrayList<>(); ++ try { ++ for (final String tag : tags) { ++ paths.add(fromString(tag)); ++ } ++ } catch (SerializationException ex) { ++ SneakyThrow.sneaky(ex); ++ } ++ return List.copyOf(paths); ++ } ++ ++ private static NbtPathArgument.NbtPath fromString(final String tag) throws SerializationException { ++ try { ++ return DUMMY_ARGUMENT.parse(new StringReader(tag)); ++ } catch (CommandSyntaxException e) { ++ throw new SerializationException(NbtPathArgument.NbtPath.class, e); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..893ad5e7c2d32ccd64962d95d146bbd317c28ab8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java +@@ -0,0 +1,86 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import com.google.common.collect.BiMap; ++import com.google.common.collect.ImmutableBiMap; ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.serializer.collections.MapSerializer; ++import io.papermc.paper.util.ObfHelper; ++import net.minecraft.network.protocol.Packet; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.List; ++import java.util.Map; ++import java.util.function.Predicate; ++ ++@SuppressWarnings("Convert2Diamond") ++public final class PacketClassSerializer extends ScalarSerializer<Class<? extends Packet<?>>> implements MapSerializer.WriteBack { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private static final TypeToken<Class<? extends Packet<?>>> TYPE = new TypeToken<Class<? extends Packet<?>>>() {}; ++ private static final List<String> SUBPACKAGES = List.of("game", "handshake", "login", "status"); ++ private static final BiMap<String, String> MOJANG_TO_OBF; ++ ++ static { ++ final ImmutableBiMap.Builder<String, String> builder = ImmutableBiMap.builder(); ++ final @Nullable Map<String, ObfHelper.ClassMapping> classMappingMap = ObfHelper.INSTANCE.mappingsByMojangName(); ++ if (classMappingMap != null) { ++ classMappingMap.forEach((mojMap, classMapping) -> { ++ if (mojMap.startsWith("net.minecraft.network.protocol.")) { ++ builder.put(classMapping.mojangName(), classMapping.obfName()); ++ } ++ }); ++ } ++ MOJANG_TO_OBF = builder.build(); ++ } ++ ++ public PacketClassSerializer() { ++ super(TYPE); ++ } ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ public Class<? extends Packet<?>> deserialize(final Type type, final Object obj) throws SerializationException { ++ @Nullable Class<?> packetClass = null; ++ for (final String subpackage : SUBPACKAGES) { ++ final String fullClassName = "net.minecraft.network.protocol." + subpackage + "." + obj; ++ try { ++ packetClass = Class.forName(fullClassName); ++ break; ++ } catch (final ClassNotFoundException ex) { ++ final @Nullable String spigotClassName = MOJANG_TO_OBF.get(fullClassName); ++ if (spigotClassName != null) { ++ try { ++ packetClass = Class.forName(spigotClassName); ++ } catch (final ClassNotFoundException ignore) {} ++ } ++ } ++ } ++ if (packetClass == null || !Packet.class.isAssignableFrom(packetClass)) { ++ throw new SerializationException("Could not deserialize a packet from " + obj); ++ } ++ return (Class<? extends Packet<?>>) packetClass; ++ } ++ ++ @Override ++ protected @Nullable Object serialize(final Class<? extends Packet<?>> packetClass, final Predicate<Class<?>> typeSupported) { ++ final String name = packetClass.getName(); ++ @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server ++ if (mojName == null && MOJANG_TO_OBF.containsKey(name)) { ++ mojName = name; ++ } ++ if (mojName != null) { ++ int pos = mojName.lastIndexOf('.'); ++ if (pos != -1 && pos != mojName.length() - 1) { ++ return mojName.substring(pos + 1); ++ } ++ } ++ ++ LOGGER.error("Could not serialize {} into a mojang-mapped packet class name", packetClass); ++ return null; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7fc0905fc6b8f5df762b4cea573f935dc00b8bc1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java +@@ -0,0 +1,52 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import net.minecraft.util.StringRepresentable; ++import net.minecraft.world.entity.MobCategory; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.Collections; ++import java.util.Map; ++import java.util.function.Function; ++import java.util.function.Predicate; ++ ++public final class StringRepresentableSerializer extends ScalarSerializer<StringRepresentable> { ++ private static final Map<Type, Function<String, StringRepresentable>> TYPES = Collections.synchronizedMap(Map.ofEntries( ++ createEntry(MobCategory.class) ++ )); ++ ++ public StringRepresentableSerializer() { ++ super(StringRepresentable.class); ++ } ++ ++ public static boolean isValidFor(final Type type) { ++ return TYPES.containsKey(type); ++ } ++ ++ private static <E extends Enum<E> & StringRepresentable> Map.Entry<Type, Function<String, @Nullable StringRepresentable>> createEntry(Class<E> type) { ++ return Map.entry(type, s -> { ++ for (E value : type.getEnumConstants()) { ++ if (value.getSerializedName().equals(s)) { ++ return value; ++ } ++ } ++ return null; ++ }); ++ } ++ ++ @Override ++ public StringRepresentable deserialize(Type type, Object obj) throws SerializationException { ++ Function<String, StringRepresentable> function = TYPES.get(type); ++ if (function == null) { ++ throw new SerializationException(type + " isn't registered"); ++ } ++ return function.apply(obj.toString()); ++ } ++ ++ @Override ++ protected Object serialize(StringRepresentable item, Predicate<Class<?>> typeSupported) { ++ return item.getSerializedName(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/FastutilMapSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/FastutilMapSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4af710e144b70933d750c22edfe484c18e4a3540 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/FastutilMapSerializer.java +@@ -0,0 +1,69 @@ ++package io.papermc.paper.configuration.serializer.collections; ++ ++import io.leangen.geantyref.GenericTypeReflector; ++import io.leangen.geantyref.TypeFactory; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.serialize.TypeSerializer; ++ ++import java.lang.reflect.ParameterizedType; ++import java.lang.reflect.Type; ++import java.util.Collections; ++import java.util.Map; ++import java.util.function.Function; ++ ++@SuppressWarnings("rawtypes") ++public abstract class FastutilMapSerializer<M extends Map<?, ?>> implements TypeSerializer<M> { ++ private final Function<Map, ? extends M> factory; ++ ++ protected FastutilMapSerializer(final Function<Map, ? extends M> factory) { ++ this.factory = factory; ++ } ++ ++ @Override ++ public M deserialize(final Type type, final ConfigurationNode node) throws SerializationException { ++ @Nullable final Map map = (Map) node.get(this.createBaseMapType((ParameterizedType) type)); ++ return this.factory.apply(map == null ? Collections.emptyMap() : map); ++ } ++ ++ @Override ++ public void serialize(final Type type, @Nullable final M obj, final ConfigurationNode node) throws SerializationException { ++ if (obj == null || obj.isEmpty()) { ++ node.raw(null); ++ } else { ++ final Type baseMapType = this.createBaseMapType((ParameterizedType) type); ++ node.set(baseMapType, obj); ++ } ++ } ++ ++ protected abstract Type createBaseMapType(final ParameterizedType type); ++ ++ public static final class SomethingToPrimitive<M extends Map<?, ?>> extends FastutilMapSerializer<M> { ++ private final Type primitiveType; ++ ++ public SomethingToPrimitive(final Function<Map, ? extends M> factory, final Type primitiveType) { ++ super(factory); ++ this.primitiveType = primitiveType; ++ } ++ ++ @Override ++ protected Type createBaseMapType(final ParameterizedType type) { ++ return TypeFactory.parameterizedClass(Map.class, type.getActualTypeArguments()[0], GenericTypeReflector.box(this.primitiveType)); ++ } ++ } ++ ++ public static final class PrimitiveToSomething<M extends Map<?, ?>> extends FastutilMapSerializer<M> { ++ private final Type primitiveType; ++ ++ public PrimitiveToSomething(final Function<Map, ? extends M> factory, final Type primitiveType) { ++ super(factory); ++ this.primitiveType = primitiveType; ++ } ++ ++ @Override ++ protected Type createBaseMapType(final ParameterizedType type) { ++ return TypeFactory.parameterizedClass(Map.class, GenericTypeReflector.box(this.primitiveType), type.getActualTypeArguments()[0]); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9b5075c39a4ef931cdb99e9aee8a33c6d88c2a2e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java +@@ -0,0 +1,162 @@ ++package io.papermc.paper.configuration.serializer.collections; ++ ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.BasicConfigurationNode; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.serialize.TypeSerializer; ++ ++import java.lang.reflect.ParameterizedType; ++import java.lang.reflect.Type; ++import java.util.Collections; ++import java.util.HashSet; ++import java.util.LinkedHashMap; ++import java.util.Map; ++import java.util.Set; ++ ++import static java.util.Objects.requireNonNull; ++ ++/** ++ * Map serializer that does not throw errors on individual entry serialization failures. ++ */ ++public class MapSerializer implements TypeSerializer<Map<?, ?>> { ++ ++ public static final TypeToken<Map<?, ?>> TYPE = new TypeToken<Map<?, ?>>() {}; ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private final boolean clearInvalids; ++ ++ public MapSerializer(boolean clearInvalids) { ++ this.clearInvalids = clearInvalids; ++ } ++ ++ @Override ++ public Map<?, ?> deserialize(Type type, ConfigurationNode node) throws SerializationException { ++ final Map<Object, Object> map = new LinkedHashMap<>(); ++ if (node.isMap()) { ++ if (!(type instanceof ParameterizedType parameterizedType)) { ++ throw new SerializationException(type, "Raw types are not supported for collections"); ++ } ++ if (parameterizedType.getActualTypeArguments().length != 2) { ++ throw new SerializationException(type, "Map expected two type arguments!"); ++ } ++ final Type key = parameterizedType.getActualTypeArguments()[0]; ++ final Type value = parameterizedType.getActualTypeArguments()[1]; ++ final @Nullable TypeSerializer<?> keySerializer = node.options().serializers().get(key); ++ final @Nullable TypeSerializer<?> valueSerializer = node.options().serializers().get(value); ++ if (keySerializer == null) { ++ throw new SerializationException(type, "No type serializer available for key type " + key); ++ } ++ if (valueSerializer == null) { ++ throw new SerializationException(type, "No type serializer available for value type " + value); ++ } ++ ++ final BasicConfigurationNode keyNode = BasicConfigurationNode.root(node.options()); ++ final Set<Object> keysToClear = new HashSet<>(); ++ for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) { ++ final @Nullable Object deserializedKey = deserialize(key, keySerializer, "key", keyNode.set(ent.getKey()), node.path()); ++ final @Nullable Object deserializedValue = deserialize(value, valueSerializer, "value", ent.getValue(), ent.getValue().path()); ++ if (deserializedKey == null || deserializedValue == null) { ++ continue; ++ } ++ if (keySerializer instanceof WriteBack) { ++ if (serialize(key, keySerializer, deserializedKey, "key", keyNode, node.path()) && !ent.getKey().equals(requireNonNull(keyNode.raw(), "Key must not be null!"))) { ++ keysToClear.add(ent.getKey()); ++ } ++ } ++ map.put(deserializedKey, deserializedValue); ++ } ++ if (keySerializer instanceof WriteBack) { // supports cleaning keys which deserialize to the same value ++ for (Object keyToClear : keysToClear) { ++ node.node(keyToClear).raw(null); ++ } ++ } ++ } ++ return map; ++ } ++ ++ private @Nullable Object deserialize(Type type, TypeSerializer<?> serializer, String mapPart, ConfigurationNode node, NodePath path) { ++ try { ++ return serializer.deserialize(type, node); ++ } catch (SerializationException ex) { ++ ex.initPath(node::path); ++ LOGGER.error("Could not deserialize {} {} into {} at {}", mapPart, node.raw(), type, path); ++ } ++ return null; ++ } ++ ++ @Override ++ public void serialize(Type type, @Nullable Map<?, ?> obj, ConfigurationNode node) throws SerializationException { ++ if (!(type instanceof ParameterizedType parameterizedType)) { ++ throw new SerializationException(type, "Raw types are not supported for collections"); ++ } ++ if (parameterizedType.getActualTypeArguments().length != 2) { ++ throw new SerializationException(type, "Map expected two type arguments!"); ++ } ++ final Type key = parameterizedType.getActualTypeArguments()[0]; ++ final Type value = parameterizedType.getActualTypeArguments()[1]; ++ final @Nullable TypeSerializer<?> keySerializer = node.options().serializers().get(key); ++ final @Nullable TypeSerializer<?> valueSerializer = node.options().serializers().get(value); ++ ++ if (keySerializer == null) { ++ throw new SerializationException(type, "No type serializer available for key type " + key); ++ } ++ ++ if (valueSerializer == null) { ++ throw new SerializationException(type, "No type serializer available for value type " + value); ++ } ++ ++ if (obj == null || obj.isEmpty()) { ++ node.set(Collections.emptyMap()); ++ } else { ++ final Set<Object> unvisitedKeys; ++ if (node.empty()) { ++ node.raw(Collections.emptyMap()); ++ unvisitedKeys = Collections.emptySet(); ++ } else { ++ unvisitedKeys = new HashSet<>(node.childrenMap().keySet()); ++ } ++ final BasicConfigurationNode keyNode = BasicConfigurationNode.root(node.options()); ++ for (Map.Entry<?, ?> ent : obj.entrySet()) { ++ if (!serialize(key, keySerializer, ent.getKey(), "key", keyNode, node.path())) { ++ continue; ++ } ++ final Object keyObj = requireNonNull(keyNode.raw(), "Key must not be null!"); ++ final ConfigurationNode child = node.node(keyObj); ++ serialize(value, valueSerializer, ent.getValue(), "value", child, child.path()); ++ unvisitedKeys.remove(keyObj); ++ } ++ if (this.clearInvalids) { ++ for (Object unusedChild : unvisitedKeys) { ++ node.removeChild(unusedChild); ++ } ++ } ++ } ++ } ++ ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ private boolean serialize(Type type, TypeSerializer serializer, Object object, String mapPart, ConfigurationNode node, NodePath path) { ++ try { ++ serializer.serialize(type, object, node); ++ return true; ++ } catch (SerializationException ex) { ++ ex.initPath(node::path); ++ LOGGER.error("Could not serialize {} {} from {} at {}", mapPart, object, type, path); ++ } ++ return false; ++ } ++ ++ @Override ++ public @Nullable Map<?, ?> emptyValue(Type specificType, ConfigurationOptions options) { ++ return new LinkedHashMap<>(); ++ } ++ ++ public interface WriteBack { // marker interface ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/TableSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/TableSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..36ca88b677e1b55b41c52750948d5b6de7ecd007 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/TableSerializer.java +@@ -0,0 +1,89 @@ ++package io.papermc.paper.configuration.serializer.collections; ++ ++import com.google.common.collect.HashBasedTable; ++import com.google.common.collect.ImmutableTable; ++import com.google.common.collect.Table; ++import io.leangen.geantyref.TypeFactory; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.BasicConfigurationNode; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.serialize.TypeSerializer; ++ ++import java.lang.reflect.ParameterizedType; ++import java.lang.reflect.Type; ++import java.util.Map; ++import java.util.Objects; ++ ++public class TableSerializer implements TypeSerializer<Table<?, ?, ?>> { ++ private static final int ROW_TYPE_ARGUMENT_INDEX = 0; ++ private static final int COLUMN_TYPE_ARGUMENT_INDEX = 1; ++ private static final int VALUE_TYPE_ARGUMENT_INDEX = 2; ++ ++ @Override ++ public Table<?, ?, ?> deserialize(final Type type, final ConfigurationNode node) throws SerializationException { ++ final Table<?, ?, ?> table = HashBasedTable.create(); ++ if (!node.empty() && node.isMap()) { ++ this.deserialize0(table, (ParameterizedType) type, node); ++ } ++ return table; ++ } ++ ++ @SuppressWarnings("unchecked") ++ private <R, C, V> void deserialize0(final Table<R, C, V> table, final ParameterizedType type, final ConfigurationNode node) throws SerializationException { ++ final Type rowType = type.getActualTypeArguments()[ROW_TYPE_ARGUMENT_INDEX]; ++ final Type columnType = type.getActualTypeArguments()[COLUMN_TYPE_ARGUMENT_INDEX]; ++ final Type valueType = type.getActualTypeArguments()[VALUE_TYPE_ARGUMENT_INDEX]; ++ ++ final @Nullable TypeSerializer<R> rowKeySerializer = (TypeSerializer<R>) node.options().serializers().get(rowType); ++ if (rowKeySerializer == null) { ++ throw new SerializationException("Could not find serializer for table row type " + rowType); ++ } ++ ++ final Type mapType = TypeFactory.parameterizedClass(Map.class, columnType, valueType); ++ final @Nullable TypeSerializer<Map<C, V>> columnValueSerializer = (TypeSerializer<Map<C, V>>) node.options().serializers().get(mapType); ++ if (columnValueSerializer == null) { ++ throw new SerializationException("Could not find serializer for table column-value map " + type); ++ } ++ ++ final BasicConfigurationNode rowKeyNode = BasicConfigurationNode.root(node.options()); ++ ++ for (final Object key : node.childrenMap().keySet()) { ++ final R rowKey = rowKeySerializer.deserialize(rowType, rowKeyNode.set(key)); ++ final Map<C, V> map = columnValueSerializer.deserialize(mapType, node.node(rowKeyNode.raw())); ++ map.forEach((column, value) -> table.put(rowKey, column, value)); ++ } ++ } ++ ++ @Override ++ public void serialize(final Type type, @Nullable final Table<?, ?, ?> table, final ConfigurationNode node) throws SerializationException { ++ if (table != null) { ++ this.serialize0(table, (ParameterizedType) type, node); ++ } ++ } ++ ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ private <R, C, V> void serialize0(final Table<R, C, V> table, final ParameterizedType type, final ConfigurationNode node) throws SerializationException { ++ final Type rowType = type.getActualTypeArguments()[ROW_TYPE_ARGUMENT_INDEX]; ++ final Type columnType = type.getActualTypeArguments()[COLUMN_TYPE_ARGUMENT_INDEX]; ++ final Type valueType = type.getActualTypeArguments()[VALUE_TYPE_ARGUMENT_INDEX]; ++ ++ final @Nullable TypeSerializer rowKeySerializer = node.options().serializers().get(rowType); ++ if (rowKeySerializer == null) { ++ throw new SerializationException("Could not find a serializer for table row type " + rowType); ++ } ++ ++ final BasicConfigurationNode rowKeyNode = BasicConfigurationNode.root(node.options()); ++ for (final R key : table.rowKeySet()) { ++ rowKeySerializer.serialize(rowType, key, rowKeyNode.set(key)); ++ final Object keyObj = Objects.requireNonNull(rowKeyNode.raw()); ++ node.node(keyObj).set(TypeFactory.parameterizedClass(Map.class, columnType, valueType), table.row(key)); ++ } ++ } ++ ++ @Override ++ public @Nullable Table<?, ?, ?> emptyValue(Type specificType, ConfigurationOptions options) { ++ return ImmutableTable.of(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9073c619f14feb7a14bf32a504eb935f6d4cfe2e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java +@@ -0,0 +1,64 @@ ++package io.papermc.paper.configuration.serializer.registry; ++ ++import io.leangen.geantyref.TypeToken; ++import java.lang.reflect.Type; ++import java.util.function.Predicate; ++import net.minecraft.core.Registry; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++abstract class RegistryEntrySerializer<T, R> extends ScalarSerializer<T> { ++ ++ private final RegistryAccess registryAccess; ++ private final ResourceKey<? extends Registry<R>> registryKey; ++ private final boolean omitMinecraftNamespace; ++ ++ protected RegistryEntrySerializer(TypeToken<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<R>> registryKey, boolean omitMinecraftNamespace) { ++ super(type); ++ this.registryAccess = registryAccess; ++ this.registryKey = registryKey; ++ this.omitMinecraftNamespace = omitMinecraftNamespace; ++ } ++ ++ protected RegistryEntrySerializer(Class<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<R>> registryKey, boolean omitMinecraftNamespace) { ++ super(type); ++ this.registryAccess = registryAccess; ++ this.registryKey = registryKey; ++ this.omitMinecraftNamespace = omitMinecraftNamespace; ++ } ++ ++ protected final Registry<R> registry() { ++ return this.registryAccess.registryOrThrow(this.registryKey); ++ } ++ ++ protected abstract T convertFromResourceKey(ResourceKey<R> key) throws SerializationException; ++ ++ @Override ++ public final T deserialize(Type type, Object obj) throws SerializationException { ++ return this.convertFromResourceKey(this.deserializeKey(obj)); ++ } ++ ++ protected abstract ResourceKey<R> convertToResourceKey(T value); ++ ++ @Override ++ protected final Object serialize(T item, Predicate<Class<?>> typeSupported) { ++ final ResourceKey<R> key = this.convertToResourceKey(item); ++ if (this.omitMinecraftNamespace && key.location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { ++ return key.location().getPath(); ++ } else { ++ return key.location().toString(); ++ } ++ } ++ ++ private ResourceKey<R> deserializeKey(final Object input) throws SerializationException { ++ final @Nullable ResourceLocation key = ResourceLocation.tryParse(input.toString()); ++ if (key == null) { ++ throw new SerializationException("Could not create a key from " + input); ++ } ++ return ResourceKey.create(this.registryKey, key); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eeae35ede747e473ddba4ca1688f2f6cbc35ce7d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.configuration.serializer.registry; ++ ++import com.google.common.base.Preconditions; ++import io.leangen.geantyref.TypeFactory; ++import io.leangen.geantyref.TypeToken; ++import java.util.function.Function; ++import net.minecraft.core.Holder; ++import net.minecraft.core.Registry; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.resources.ResourceKey; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public final class RegistryHolderSerializer<T> extends RegistryEntrySerializer<Holder<T>, T> { ++ ++ @SuppressWarnings("unchecked") ++ public RegistryHolderSerializer(TypeToken<T> typeToken, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) { ++ super((TypeToken<Holder<T>>) TypeToken.get(TypeFactory.parameterizedClass(Holder.class, typeToken.getType())), registryAccess, registryKey, omitMinecraftNamespace); ++ } ++ ++ public RegistryHolderSerializer(Class<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) { ++ this(TypeToken.get(type), registryAccess, registryKey, omitMinecraftNamespace); ++ Preconditions.checkArgument(type.getTypeParameters().length == 0, "%s must have 0 type parameters", type); ++ } ++ ++ @Override ++ protected Holder<T> convertFromResourceKey(ResourceKey<T> key) throws SerializationException { ++ return this.registry().getHolder(key).orElseThrow(() -> new SerializationException("Missing holder in " + this.registry().key() + " with key " + key)); ++ } ++ ++ @Override ++ protected ResourceKey<T> convertToResourceKey(Holder<T> value) { ++ return value.unwrap().map(Function.identity(), r -> this.registry().getResourceKey(r).orElseThrow()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..718377ce91a010a48b2b4a5e59e02ee8a42107a7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java +@@ -0,0 +1,35 @@ ++package io.papermc.paper.configuration.serializer.registry; ++ ++import io.leangen.geantyref.TypeToken; ++import net.minecraft.core.Registry; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.resources.ResourceKey; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++/** ++ * Use {@link RegistryHolderSerializer} for datapack-configurable things. ++ */ ++public final class RegistryValueSerializer<T> extends RegistryEntrySerializer<T, T> { ++ ++ public RegistryValueSerializer(TypeToken<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) { ++ super(type, registryAccess, registryKey, omitMinecraftNamespace); ++ } ++ ++ public RegistryValueSerializer(Class<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) { ++ super(type, registryAccess, registryKey, omitMinecraftNamespace); ++ } ++ ++ @Override ++ protected T convertFromResourceKey(ResourceKey<T> key) throws SerializationException { ++ final T value = this.registry().get(key); ++ if (value == null) { ++ throw new SerializationException("Missing value in " + this.registry() + " with key " + key.location()); ++ } ++ return value; ++ } ++ ++ @Override ++ protected ResourceKey<T> convertToResourceKey(T value) { ++ return this.registry().getResourceKey(value).orElseThrow(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java b/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..96e8d03bd4a4d43633a94bb251054610ac07315a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java +@@ -0,0 +1,41 @@ ++package io.papermc.paper.configuration.transformation; ++ ++import io.papermc.paper.configuration.Configuration; ++import io.papermc.paper.configuration.Configurations; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++public final class Transformations { ++ private Transformations() { ++ } ++ ++ public static void moveFromRoot(final ConfigurationTransformation.Builder builder, final String key, final String... parents) { ++ moveFromRootAndRename(builder, key, key, parents); ++ } ++ ++ public static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) { ++ moveFromRootAndRename(builder, path(oldKey), newKey, parents); ++ } ++ ++ public static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final NodePath oldKey, final String newKey, final String... parents) { ++ builder.addAction(oldKey, (path, value) -> { ++ final Object[] newPath = new Object[parents.length + 1]; ++ newPath[parents.length] = newKey; ++ System.arraycopy(parents, 0, newPath, 0, parents.length); ++ return newPath; ++ }); ++ } ++ ++ public static ConfigurationTransformation.VersionedBuilder versionedBuilder() { ++ return ConfigurationTransformation.versionedBuilder().versionKey(Configuration.VERSION_FIELD); ++ } ++ ++ @FunctionalInterface ++ public interface DefaultsAware { ++ void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ef0e834c164b0ccc1a61b349348e6799733d66d9 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java +@@ -0,0 +1,223 @@ ++package io.papermc.paper.configuration.transformation.global; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.Configuration; ++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.serializer.legacy.LegacyComponentSerializer; ++import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; ++import org.bukkit.ChatColor; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import java.util.function.Predicate; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++public final class LegacyPaperConfig { ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private LegacyPaperConfig() { ++ } ++ ++ public static ConfigurationTransformation transformation(final YamlConfiguration spigotConfiguration) { ++ return ConfigurationTransformation.chain(versioned(), notVersioned(spigotConfiguration)); ++ } ++ ++ // Represents version transforms lifted directly from the old PaperConfig class ++ // must be run BEFORE the "settings" flatten ++ private static ConfigurationTransformation.Versioned versioned() { ++ return ConfigurationTransformation.versionedBuilder() ++ .versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD) ++ .addVersion(11, ConfigurationTransformation.builder().addAction(path("settings", "play-in-use-item-spam-threshold"), TransformAction.rename("incoming-packet-spam-threshold")).build()) ++ .addVersion(14, ConfigurationTransformation.builder().addAction(path("settings", "spam-limiter", "tab-spam-increment"), (path, value) -> { ++ if (value.getInt() == 10) { ++ value.set(2); ++ } ++ return null; ++ }).build()) ++ .addVersion(15, ConfigurationTransformation.builder().addAction(path("settings"), (path, value) -> { ++ value.node("async-chunks", "threads").set(-1); ++ return null; ++ }).build()) ++ .addVersion(21, ConfigurationTransformation.builder().addAction(path("use-display-name-in-quit-message"), (path, value) -> new Object[]{"settings", "use-display-name-in-quit-message"}).build()) ++ .addVersion(23, ConfigurationTransformation.builder().addAction(path("settings", "chunk-loading", "global-max-chunk-load-rate"), (path, value) -> { ++ if (value.getDouble() == 300.0) { ++ value.set(-1.0); ++ } ++ return null; ++ }).build()) ++ .addVersion(25, ConfigurationTransformation.builder().addAction(path("settings", "chunk-loading", "player-max-concurrent-loads"), (path, value) -> { ++ if (value.getDouble() == 4.0) { ++ value.set(20.0); ++ } ++ return null; ++ }).build()) ++ .build(); ++ } ++ ++ // other non-versioned transforms found in PaperConfig ++ // must be run BEFORE the "settings" flatten ++ private static ConfigurationTransformation notVersioned(final YamlConfiguration spigotConfiguration) { ++ return ConfigurationTransformation.builder() ++ .addAction(path("settings"), (path, value) -> { ++ final ConfigurationNode node = value.node("async-chunks"); ++ if (node.hasChild("load-threads")) { ++ if (!node.hasChild("threads")) { ++ node.node("threads").set(node.node("load-threads").getInt()); ++ } ++ node.removeChild("load-threads"); ++ } ++ node.removeChild("generation"); ++ node.removeChild("enabled"); ++ node.removeChild("thread-per-world-generation"); ++ return null; ++ }) ++ .addAction(path("allow-perm-block-break-exploits"), (path, value) -> new Object[]{"settings", "unsupported-settings", "allow-permanent-block-break-exploits"}) ++ .addAction(path("settings", "unsupported-settings", "allow-tnt-duplication"), TransformAction.rename("allow-piston-duplication")) ++ .addAction(path("settings", "save-player-data"), (path, value) -> { ++ final @Nullable Object val = value.raw(); ++ if (val instanceof Boolean bool) { ++ spigotConfiguration.set("players.disable-saving", !bool); ++ } ++ value.raw(null); ++ return null; ++ }) ++ .addAction(path("settings", "log-named-entity-deaths"), (path, value) -> { ++ final @Nullable Object val = value.raw(); ++ if (val instanceof Boolean bool && !bool) { ++ spigotConfiguration.set("settings.log-named-deaths", false); ++ } ++ value.raw(null); ++ return null; ++ }) ++ .build(); ++ } ++ ++ // transforms to new format with configurate ++ // must be run AFTER the "settings" flatten ++ public static ConfigurationTransformation toNewFormat() { ++ return ConfigurationTransformation.chain( ++ ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD).addVersion(Configuration.FINAL_LEGACY_VERSION + 1, newFormatTransformation()).build(), ++ ConfigurationTransformation.builder().addAction(path(Configuration.LEGACY_CONFIG_VERSION_FIELD), TransformAction.rename(Configuration.VERSION_FIELD)).build() // rename to _version to place at the top ++ ); ++ } ++ ++ private static ConfigurationTransformation newFormatTransformation() { ++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder() ++ .addAction(path("verbose"), TransformAction.remove()) // not needed ++ .addAction(path("unsupported-settings", "allow-headless-pistons-readme"), TransformAction.remove()) ++ .addAction(path("unsupported-settings", "allow-permanent-block-break-exploits-readme"), TransformAction.remove()) ++ .addAction(path("unsupported-settings", "allow-piston-duplication-readme"), TransformAction.remove()) ++ .addAction(path("packet-limiter", "limits", "all"), (path, value) -> new Object[]{"packet-limiter", "all-packets"}) ++ .addAction(path("packet-limiter", "limits"), (path, value) -> new Object[]{"packet-limiter", "overrides"}) ++ .addAction(path("packet-limiter", "overrides", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> { ++ final @Nullable Object keyValue = value.key(); ++ if (keyValue != null && keyValue.toString().equals("PacketPlayInAutoRecipe")) { // add special cast to handle the default for moj-mapped servers that upgrade the config ++ return path.with(path.size() - 1, ServerboundPlaceRecipePacket.class.getSimpleName()).array(); ++ } ++ return null; ++ }).addAction(path("loggers"), TransformAction.rename("logging")); ++ ++ moveFromRootAndRename(builder, "incoming-packet-spam-threshold", "incoming-packet-threshold", "spam-limiter"); ++ ++ moveFromRoot(builder, "save-empty-scoreboard-teams", "scoreboards"); ++ moveFromRoot(builder, "track-plugin-scoreboards", "scoreboards"); ++ ++ moveFromRoot(builder, "suggest-player-names-when-null-tab-completions", "commands"); ++ moveFromRoot(builder, "time-command-affects-all-worlds", "commands"); ++ moveFromRoot(builder, "fix-target-selector-tag-completion", "commands"); ++ ++ moveFromRoot(builder, "log-player-ip-addresses", "loggers"); ++ ++ moveFromRoot(builder, "use-display-name-in-quit-message", "messages"); ++ ++ moveFromRootAndRename(builder, "console-has-all-permissions", "has-all-permissions", "console"); ++ ++ moveFromRootAndRename(builder, "bungee-online-mode", "online-mode", "proxies", "bungee-cord"); ++ moveFromRootAndRename(builder, "velocity-support", "velocity", "proxies"); ++ ++ moveFromRoot(builder, "book-size", "item-validation"); ++ moveFromRoot(builder, "resolve-selectors-in-books", "item-validation"); ++ ++ moveFromRoot(builder, "enable-player-collisions", "collisions"); ++ moveFromRoot(builder, "send-full-pos-for-hard-colliding-entities", "collisions"); ++ ++ moveFromRootAndRename(builder, "player-auto-save-rate", "rate", "player-auto-save"); ++ moveFromRootAndRename(builder, "max-player-auto-save-per-tick", "max-per-tick", "player-auto-save"); ++ ++ moveFromRootToMisc(builder, "max-joins-per-tick"); ++ moveFromRootToMisc(builder, "fix-entity-position-desync"); ++ moveFromRootToMisc(builder, "load-permissions-yml-before-plugins"); ++ moveFromRootToMisc(builder, "region-file-cache-size"); ++ moveFromRootToMisc(builder, "use-alternative-luck-formula"); ++ moveFromRootToMisc(builder, "lag-compensate-block-breaking"); ++ moveFromRootToMisc(builder, "use-dimension-type-for-custom-spawners"); ++ ++ moveFromRoot(builder, "proxy-protocol", "proxies"); ++ ++ miniMessageWithTranslatable(builder, String::isBlank, "multiplayer.disconnect.authservers_down", "messages", "kick", "authentication-servers-down"); ++ miniMessageWithTranslatable(builder, Predicate.isEqual("Flying is not enabled on this server"), "multiplayer.disconnect.flying", "messages", "kick", "flying-player"); ++ miniMessageWithTranslatable(builder, Predicate.isEqual("Flying is not enabled on this server"), "multiplayer.disconnect.flying", "messages", "kick", "flying-vehicle"); ++ miniMessage(builder, "messages", "kick", "connection-throttle"); ++ miniMessage(builder, "messages", "no-permission"); ++ miniMessageWithTranslatable(builder, Predicate.isEqual("&cSent too many packets"), Component.translatable("disconnect.exceeded_packet_rate", NamedTextColor.RED), "packet-limiter", "kick-message"); ++ ++ return builder.build(); ++ } ++ ++ private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final String i18nKey, final String... strPath) { ++ miniMessageWithTranslatable(builder, englishCheck, Component.translatable(i18nKey), strPath); ++ } ++ private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final Component component, final String... strPath) { ++ builder.addAction(path((Object[]) strPath), (path, value) -> { ++ final @Nullable Object val = value.raw(); ++ if (val != null) { ++ final String strVal = val.toString(); ++ if (!englishCheck.test(strVal)) { ++ value.set(miniMessage(strVal)); ++ return null; ++ } ++ } ++ value.set(MiniMessage.miniMessage().serialize(component)); ++ return null; ++ }); ++ } ++ ++ private static void miniMessage(final ConfigurationTransformation.Builder builder, final String... strPath) { ++ builder.addAction(path((Object[]) strPath), (path, value) -> { ++ final @Nullable Object val = value.raw(); ++ if (val != null) { ++ value.set(miniMessage(val.toString())); ++ } ++ return null; ++ }); ++ } ++ ++ @SuppressWarnings("deprecation") // valid use to convert legacy string to mini-message in legacy migration ++ private static String miniMessage(final String input) { ++ return MiniMessage.miniMessage().serialize(LegacyComponentSerializer.legacySection().deserialize(ChatColor.translateAlternateColorCodes('&', input))); ++ } ++ ++ private static void moveFromRootToMisc(final ConfigurationTransformation.Builder builder, final String key) { ++ moveFromRoot(builder, key, "misc"); ++ } ++ ++ private static void moveFromRoot(final ConfigurationTransformation.Builder builder, final String key, final String... parents) { ++ moveFromRootAndRename(builder, key, key, parents); ++ } ++ ++ private static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) { ++ builder.addAction(path(oldKey), (path, value) -> { ++ final Object[] newPath = new Object[parents.length + 1]; ++ newPath[parents.length] = newKey; ++ System.arraycopy(parents, 0, newPath, 0, parents.length); ++ return newPath; ++ }); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/V29_LogIPs.java b/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/V29_LogIPs.java +new file mode 100644 +index 0000000000000000000000000000000000000000..66073f7a6a96405348cc4044ad1e6922158b13ba +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/V29_LogIPs.java +@@ -0,0 +1,44 @@ ++package io.papermc.paper.configuration.transformation.global.versioned; ++ ++import java.util.Properties; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.dedicated.DedicatedServer; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++public class V29_LogIPs implements TransformAction { ++ ++ private static final int VERSION = 29; ++ private static final NodePath PATH = path("logging", "log-player-ip-addresses"); ++ private static final V29_LogIPs INSTANCE = new V29_LogIPs(); ++ ++ private V29_LogIPs() { ++ } ++ ++ public static void apply(final ConfigurationTransformation.VersionedBuilder builder) { ++ builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build()); ++ } ++ ++ @Override ++ public Object @Nullable [] visitPath(final NodePath path, final ConfigurationNode value) throws ConfigurateException { ++ final DedicatedServer server = ((DedicatedServer) MinecraftServer.getServer()); ++ ++ final boolean val = value.getBoolean(server.settings.getProperties().logIPs); ++ server.settings.update((config) -> { ++ final Properties newProps = new Properties(config.properties); ++ newProps.setProperty("log-ips", String.valueOf(val)); ++ return config.reload(server.registryAccess(), newProps, server.options); ++ }); ++ ++ value.raw(null); ++ ++ return null; ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java b/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6cdc40cb4a5f94654c874f9dbdb106fa0e4d41f3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java +@@ -0,0 +1,71 @@ ++package io.papermc.paper.configuration.transformation.world; ++ ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.Configurations; ++import it.unimi.dsi.fastutil.objects.Reference2LongMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; ++import net.minecraft.core.Holder; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import java.security.SecureRandom; ++import java.util.Objects; ++import java.util.Random; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++public final class FeatureSeedsGeneration implements TransformAction { ++ ++ public static final String FEATURE_SEEDS_KEY = "feature-seeds"; ++ public static final String GENERATE_KEY = "generate-random-seeds-for-all"; ++ public static final String FEATURES_KEY = "features"; ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private final ResourceLocation worldKey; ++ ++ private FeatureSeedsGeneration(ResourceLocation worldKey) { ++ this.worldKey = worldKey; ++ } ++ ++ @Override ++ public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException { ++ ConfigurationNode featureNode = value.node(FEATURE_SEEDS_KEY, FEATURES_KEY); ++ final Reference2LongMap<Holder<ConfiguredFeature<?, ?>>> features = Objects.requireNonNullElseGet(featureNode.get(new TypeToken<Reference2LongMap<Holder<ConfiguredFeature<?, ?>>>>() {}), Reference2LongOpenHashMap::new); ++ final Random random = new SecureRandom(); ++ AtomicInteger counter = new AtomicInteger(0); ++ MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).holders().forEach(holder -> { ++ if (features.containsKey(holder)) { ++ return; ++ } ++ ++ final long seed = random.nextLong(); ++ features.put(holder, seed); ++ counter.incrementAndGet(); ++ }); ++ if (counter.get() > 0) { ++ LOGGER.info("Generated {} random feature seeds for {}", counter.get(), this.worldKey); ++ featureNode.raw(null); ++ featureNode.set(new TypeToken<Reference2LongMap<Holder<ConfiguredFeature<?, ?>>>>() {}, features); ++ } ++ return null; ++ } ++ ++ ++ public static void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode) { ++ if (defaultsNode.node(FEATURE_SEEDS_KEY, GENERATE_KEY).getBoolean(false)) { ++ builder.addAction(path(), new FeatureSeedsGeneration(contextMap.require(Configurations.WORLD_KEY))); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java b/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f7520ba86c1a650d3bd9b902d2a59ec8eb6cde5d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java +@@ -0,0 +1,322 @@ ++package io.papermc.paper.configuration.transformation.world; ++ ++import io.papermc.paper.configuration.Configuration; ++import io.papermc.paper.configuration.WorldConfiguration; ++import net.minecraft.core.Holder; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.world.entity.MobCategory; ++import net.minecraft.world.item.Item; ++import org.bukkit.Material; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.Optional; ++ ++import static io.papermc.paper.configuration.transformation.Transformations.moveFromRoot; ++import static io.papermc.paper.configuration.transformation.Transformations.moveFromRootAndRename; ++import static org.spongepowered.configurate.NodePath.path; ++ ++public final class LegacyPaperWorldConfig { ++ ++ private LegacyPaperWorldConfig() { ++ } ++ ++ public static ConfigurationTransformation transformation() { ++ return ConfigurationTransformation.chain(versioned(), notVersioned()); ++ } ++ ++ private static ConfigurationTransformation.Versioned versioned() { ++ return ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD) ++ .addVersion(13, ConfigurationTransformation.builder().addAction(path("enable-old-tnt-cannon-behaviors"), TransformAction.rename("prevent-tnt-from-moving-in-water")).build()) ++ .addVersion(16, ConfigurationTransformation.builder().addAction(path("use-chunk-inhabited-timer"), (path, value) -> { ++ if (!value.getBoolean(true)) { ++ value.raw(0); ++ } else { ++ value.raw(-1); ++ } ++ final Object[] newPath = path.array(); ++ newPath[newPath.length - 1] = "fixed-chunk-inhabited-time"; ++ return newPath; ++ }).build()) ++ .addVersion(18, ConfigurationTransformation.builder().addAction(path("nether-ceiling-void-damage"), (path, value) -> { ++ if (value.getBoolean(false)) { ++ value.raw(128); ++ } else { ++ value.raw(0); ++ } ++ final Object[] newPath = path.array(); ++ newPath[newPath.length - 1] = "nether-ceiling-void-damage-height"; ++ return newPath; ++ }).build()) ++ .addVersion(19, ConfigurationTransformation.builder() ++ .addAction(path("anti-xray", "hidden-blocks"), (path, value) -> { ++ @Nullable final List<String> hiddenBlocks = value.getList(String.class); ++ if (hiddenBlocks != null) { ++ hiddenBlocks.remove("lit_redstone_ore"); ++ } ++ return null; ++ }) ++ .addAction(path("anti-xray", "replacement-blocks"), (path, value) -> { ++ @Nullable final List<String> replacementBlocks = value.getList(String.class); ++ if (replacementBlocks != null) { ++ final int index = replacementBlocks.indexOf("planks"); ++ if (index != -1) { ++ replacementBlocks.set(index, "oak_planks"); ++ } ++ } ++ value.raw(replacementBlocks); ++ return null; ++ }).build()) ++ .addVersion(20, ConfigurationTransformation.builder().addAction(path("baby-zombie-movement-speed"), TransformAction.rename("baby-zombie-movement-modifier")).build()) ++ .addVersion(22, ConfigurationTransformation.builder().addAction(path("per-player-mob-spawns"), (path, value) -> { ++ value.raw(true); ++ return null; ++ }).build()) ++ .addVersion(24, ++ ConfigurationTransformation.builder() ++ .addAction(path("spawn-limits", "monsters"), TransformAction.rename("monster")) ++ .addAction(path("spawn-limits", "animals"), TransformAction.rename("creature")) ++ .addAction(path("spawn-limits", "water-animals"), TransformAction.rename("water_creature")) ++ .addAction(path("spawn-limits", "water-ambient"), TransformAction.rename("water_ambient")) ++ .build(), ++ ConfigurationTransformation.builder().addAction(path("despawn-ranges"), (path, value) -> { ++ final int softDistance = value.node("soft").getInt(32); ++ final int hardDistance = value.node("hard").getInt(128); ++ value.node("soft").raw(null); ++ value.node("hard").raw(null); ++ for (final MobCategory category : MobCategory.values()) { ++ if (softDistance != 32) { ++ value.node(category.getName(), "soft").raw(softDistance); ++ } ++ if (hardDistance != 128) { ++ value.node(category.getName(), "hard").raw(hardDistance); ++ } ++ } ++ return null; ++ }).build() ++ ) ++ .addVersion(26, ConfigurationTransformation.builder().addAction(path("alt-item-despawn-rate", "items", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> { ++ String itemName = path.get(path.size() - 1).toString(); ++ final Optional<Holder.Reference<Item>> item = BuiltInRegistries.ITEM.getHolder(ResourceKey.create(Registries.ITEM, new ResourceLocation(itemName.toLowerCase(Locale.ROOT)))); ++ if (item.isEmpty()) { ++ itemName = Material.valueOf(itemName).getKey().getKey(); ++ } ++ final Object[] newPath = path.array(); ++ newPath[newPath.length - 1] = itemName; ++ return newPath; ++ }).build()) ++ .addVersion(27, ConfigurationTransformation.builder().addAction(path("use-faster-eigencraft-redstone"), (path, value) -> { ++ final WorldConfiguration.Misc.RedstoneImplementation redstoneImplementation = value.getBoolean(false) ? WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT : WorldConfiguration.Misc.RedstoneImplementation.VANILLA; ++ value.set(redstoneImplementation); ++ final Object[] newPath = path.array(); ++ newPath[newPath.length - 1] = "redstone-implementation"; ++ return newPath; ++ }).build()) ++ .build(); ++ } ++ ++ // other transformations found in PaperWorldConfig that aren't versioned ++ private static ConfigurationTransformation notVersioned() { ++ return ConfigurationTransformation.builder() ++ .addAction(path("treasure-maps-return-already-discovered"), (path, value) -> { ++ boolean prevValue = value.getBoolean(false); ++ value.node("villager-trade").set(prevValue); ++ value.node("loot-tables").set(prevValue); ++ return path.with(path.size() - 1, "treasure-maps-find-already-discovered").array(); ++ }) ++ .addAction(path("alt-item-despawn-rate", "items"), (path, value) -> { ++ if (value.isMap()) { ++ Map<String, Integer> rebuild = new HashMap<>(); ++ value.childrenMap().forEach((key, node) -> { ++ String itemName = key.toString(); ++ final Optional<Holder.Reference<Item>> itemHolder = BuiltInRegistries.ITEM.getHolder(ResourceKey.create(Registries.ITEM, new ResourceLocation(itemName.toLowerCase(Locale.ROOT)))); ++ final @Nullable String item; ++ if (itemHolder.isEmpty()) { ++ final @Nullable Material bukkitMat = Material.matchMaterial(itemName); ++ item = bukkitMat != null ? bukkitMat.getKey().getKey() : null; ++ } else { ++ item = itemHolder.get().unwrapKey().orElseThrow().location().getPath(); ++ } ++ if (item != null) { ++ rebuild.put(item, node.getInt()); ++ } ++ }); ++ value.set(rebuild); ++ } ++ return null; ++ }) ++ .build(); ++ } ++ ++ public static ConfigurationTransformation toNewFormat() { ++ return ConfigurationTransformation.chain(ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD).addVersion(Configuration.FINAL_LEGACY_VERSION + 1, newFormatTransformation()).build(), ConfigurationTransformation.builder().addAction(path(Configuration.LEGACY_CONFIG_VERSION_FIELD), TransformAction.rename(Configuration.VERSION_FIELD)).build()); ++ } ++ ++ private static ConfigurationTransformation newFormatTransformation() { ++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder() ++ .addAction(path("verbose"), TransformAction.remove()); // not needed ++ ++ moveFromRoot(builder, "anti-xray", "anticheat"); ++ ++ moveFromRootAndRename(builder, "armor-stands-do-collision-entity-lookups", "do-collision-entity-lookups", "entities", "armor-stands"); ++ moveFromRootAndRename(builder, "armor-stands-tick", "tick", "entities", "armor-stands"); ++ ++ moveFromRoot(builder, "auto-save-interval", "chunks"); ++ moveFromRoot(builder, "delay-chunk-unloads-by", "chunks"); ++ moveFromRoot(builder, "entity-per-chunk-save-limit", "chunks"); ++ moveFromRoot(builder, "fixed-chunk-inhabited-time", "chunks"); ++ moveFromRoot(builder, "max-auto-save-chunks-per-tick", "chunks"); ++ moveFromRoot(builder, "prevent-moving-into-unloaded-chunks", "chunks"); ++ ++ moveFromRoot(builder, "entities-target-with-follow-range", "entities"); ++ moveFromRoot(builder, "mob-effects", "entities"); ++ ++ moveFromRoot(builder, "filter-nbt-data-from-spawn-eggs-and-related", "entities", "spawning"); ++ moveFromGameMechanics(builder, "disable-mob-spawner-spawn-egg-transformation", "entities", "spawning"); ++ moveFromRoot(builder, "per-player-mob-spawns", "entities", "spawning"); ++ moveFromGameMechanics(builder, "scan-for-legacy-ender-dragon", "entities", "spawning"); ++ moveFromRoot(builder, "spawn-limits", "entities", "spawning"); ++ moveFromRoot(builder, "despawn-ranges", "entities", "spawning"); ++ moveFromRoot(builder, "wateranimal-spawn-height", "entities", "spawning"); ++ builder.addAction(path("slime-spawn-height", "swamp-biome"), TransformAction.rename("surface-biome")); ++ moveFromRoot(builder, "slime-spawn-height", "entities", "spawning"); ++ moveFromRoot(builder, "wandering-trader", "entities", "spawning"); ++ moveFromRoot(builder, "all-chunks-are-slime-chunks", "entities", "spawning"); ++ moveFromRoot(builder, "skeleton-horse-thunder-spawn-chance", "entities", "spawning"); ++ moveFromRoot(builder, "iron-golems-can-spawn-in-air", "entities", "spawning"); ++ moveFromRoot(builder, "alt-item-despawn-rate", "entities", "spawning"); ++ moveFromRoot(builder, "count-all-mobs-for-spawning", "entities", "spawning"); ++ moveFromRoot(builder, "creative-arrow-despawn-rate", "entities", "spawning"); ++ moveFromRoot(builder, "non-player-arrow-despawn-rate", "entities", "spawning"); ++ moveFromRoot(builder, "monster-spawn-max-light-level", "entities", "spawning"); ++ ++ ++ moveFromRootAndRename(builder, "duplicate-uuid-saferegen-delete-range", "safe-regen-delete-range", "entities", "spawning", "duplicate-uuid"); ++ ++ moveFromRoot(builder, "baby-zombie-movement-modifier", "entities", "behavior"); ++ moveFromRoot(builder, "disable-creeper-lingering-effect", "entities", "behavior"); ++ moveFromRoot(builder, "door-breaking-difficulty", "entities", "behavior"); ++ moveFromGameMechanics(builder, "disable-chest-cat-detection", "entities", "behavior"); ++ moveFromGameMechanics(builder, "disable-player-crits", "entities", "behavior"); ++ moveFromRoot(builder, "experience-merge-max-value", "entities", "behavior"); ++ moveFromRoot(builder, "mobs-can-always-pick-up-loot", "entities", "behavior"); ++ moveFromGameMechanics(builder, "nerf-pigmen-from-nether-portals", "entities", "behavior"); ++ moveFromRoot(builder, "parrots-are-unaffected-by-player-movement", "entities", "behavior"); ++ moveFromRoot(builder, "phantoms-do-not-spawn-on-creative-players", "entities", "behavior"); ++ moveFromRoot(builder, "phantoms-only-attack-insomniacs", "entities", "behavior"); ++ moveFromRoot(builder, "piglins-guard-chests", "entities", "behavior"); ++ moveFromRoot(builder, "spawner-nerfed-mobs-should-jump", "entities", "behavior"); ++ moveFromRoot(builder, "zombie-villager-infection-chance", "entities", "behavior"); ++ moveFromRoot(builder, "zombies-target-turtle-eggs", "entities", "behavior"); ++ moveFromRoot(builder, "ender-dragons-death-always-places-dragon-egg", "entities", "behavior"); ++ moveFromGameMechanicsAndRename(builder, "disable-pillager-patrols", "disable", "game-mechanics", "pillager-patrols"); ++ moveFromGameMechanics(builder, "pillager-patrols", "entities", "behavior"); ++ moveFromRoot(builder, "should-remove-dragon", "entities", "behavior"); ++ ++ moveFromRootAndRename(builder, "map-item-frame-cursor-limit", "item-frame-cursor-limit", "maps"); ++ moveFromRootAndRename(builder, "map-item-frame-cursor-update-interval", "item-frame-cursor-update-interval", "maps"); ++ ++ moveFromRootAndRename(builder, "mob-spawner-tick-rate", "mob-spawner", "tick-rates"); ++ moveFromRootAndRename(builder, "container-update-tick-rate", "container-update", "tick-rates"); ++ moveFromRootAndRename(builder, "grass-spread-tick-rate", "grass-spread", "tick-rates"); ++ ++ moveFromRoot(builder, "allow-non-player-entities-on-scoreboards", "scoreboards"); ++ moveFromRoot(builder, "use-vanilla-world-scoreboard-name-coloring", "scoreboards"); ++ ++ moveFromRoot(builder, "disable-thunder", "environment"); ++ moveFromRoot(builder, "disable-ice-and-snow", "environment"); ++ moveFromRoot(builder, "optimize-explosions", "environment"); ++ moveFromRoot(builder, "disable-explosion-knockback", "environment"); ++ moveFromRoot(builder, "frosted-ice", "environment"); ++ moveFromRoot(builder, "disable-teleportation-suffocation-check", "environment"); ++ moveFromRoot(builder, "portal-create-radius", "environment"); ++ moveFromRoot(builder, "portal-search-radius", "environment"); ++ moveFromRoot(builder, "portal-search-vanilla-dimension-scaling", "environment"); ++ moveFromRootAndRename(builder, "enable-treasure-maps", "enabled", "environment", "treasure-maps"); ++ moveFromRootAndRename(builder, "treasure-maps-find-already-discovered", "find-already-discovered", "environment", "treasure-maps"); ++ moveFromRoot(builder, "water-over-lava-flow-speed", "environment"); ++ moveFromRoot(builder, "nether-ceiling-void-damage-height", "environment"); ++ ++ moveFromRoot(builder, "keep-spawn-loaded", "spawn"); ++ moveFromRoot(builder, "keep-spawn-loaded-range", "spawn"); ++ moveFromRoot(builder, "allow-using-signs-inside-spawn-protection", "spawn"); ++ ++ moveFromRoot(builder, "max-entity-collisions", "collisions"); ++ moveFromRoot(builder, "allow-vehicle-collisions", "collisions"); ++ moveFromRoot(builder, "fix-climbing-bypassing-cramming-rule", "collisions"); ++ moveFromRoot(builder, "only-players-collide", "collisions"); ++ moveFromRoot(builder, "allow-player-cramming-damage", "collisions"); ++ ++ moveFromRoot(builder, "falling-block-height-nerf", "fixes"); ++ moveFromRoot(builder, "fix-items-merging-through-walls", "fixes"); ++ moveFromRoot(builder, "prevent-tnt-from-moving-in-water", "fixes"); ++ moveFromRoot(builder, "remove-corrupt-tile-entities", "fixes"); ++ moveFromRoot(builder, "split-overstacked-loot", "fixes"); ++ moveFromRoot(builder, "tnt-entity-height-nerf", "fixes"); ++ moveFromRoot(builder, "fix-wither-targeting-bug", "fixes"); ++ moveFromGameMechanics(builder, "disable-unloaded-chunk-enderpearl-exploit", "fixes"); ++ moveFromGameMechanics(builder, "fix-curing-zombie-villager-discount-exploit", "fixes"); ++ ++ builder.addAction(path("fishing-time-range", "MaximumTicks"), TransformAction.rename("maximum")); ++ builder.addAction(path("fishing-time-range", "MinimumTicks"), TransformAction.rename("minimum")); ++ ++ builder.addAction(path("generator-settings", "flat-bedrock"), (path, value) -> new Object[]{"environment", "generate-flat-bedrock"}); ++ builder.addAction(path("generator-settings"), TransformAction.remove()); ++ ++ builder.addAction(path("game-mechanics", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> new Object[]{"misc", path.array()[1]}); ++ builder.addAction(path("game-mechanics"), TransformAction.remove()); ++ ++ builder.addAction(path("feature-seeds", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> { ++ final String key = path.array()[path.size() - 1].toString(); ++ if (!key.equals("generate-random-seeds-for-all")) { ++ return new Object[]{"feature-seeds", "features", key}; ++ } ++ return null; ++ }); ++ ++ builder.addAction(path("duplicate-uuid-resolver"), (path, value) -> { ++ final WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode duplicateUUIDMode = switch (value.require(String.class)) { ++ case "regen", "regenerate", "saferegen", "saferegenerate" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN; ++ case "remove", "delete" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE; ++ case "silent", "nothing" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.NOTHING; ++ default -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN; ++ }; ++ value.set(duplicateUUIDMode); ++ return new Object[]{"entities", "spawning", "duplicate-uuid", "mode"}; ++ }); ++ ++ builder.addAction(path("redstone-implementation"), (path, value) -> { ++ if (value.require(String.class).equalsIgnoreCase("alternate-current")) { ++ value.set("alternate_current"); ++ } ++ return new Object[]{"misc", "redstone-implementation"}; ++ }); ++ ++ moveToMisc(builder, "light-queue-size"); ++ moveToMisc(builder, "update-pathfinding-on-block-update"); ++ moveToMisc(builder, "show-sign-click-command-failure-msgs-to-player"); ++ moveToMisc(builder, "max-leash-distance"); ++ ++ return builder.build(); ++ } ++ ++ private static void moveToMisc(final ConfigurationTransformation.Builder builder, String... key) { ++ moveFromRootAndRename(builder, path((Object[]) key), key[key.length - 1], "misc"); ++ } ++ ++ private static void moveFromGameMechanics(final ConfigurationTransformation.Builder builder, final String key, final String... parents) { ++ moveFromGameMechanicsAndRename(builder, key, key, parents); ++ } ++ ++ private static void moveFromGameMechanicsAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) { ++ moveFromRootAndRename(builder, path("game-mechanics", oldKey), newKey, parents); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V29_ZeroWorldHeight.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V29_ZeroWorldHeight.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6e481d509d091e65a4909d79014ac94ea63c8455 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V29_ZeroWorldHeight.java +@@ -0,0 +1,49 @@ ++package io.papermc.paper.configuration.transformation.world.versioned; ++ ++import io.papermc.paper.configuration.type.number.IntOr; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++/** ++ * Several configurations that set a y-level used '0' as the "disabled" value. ++ * Since 0 is now a valid value, they need to be updated. ++ */ ++public final class V29_ZeroWorldHeight implements TransformAction { ++ ++ private static final int VERSION = 29; ++ ++ private static final String FIXES_KEY = "fixes"; ++ private static final String FALLING_BLOCK_HEIGHT_NERF_KEY = "falling-block-height-nerf"; ++ private static final String TNT_ENTITY_HEIGHT_NERF_KEY = "tnt-entity-height-nerf"; ++ ++ private static final String ENVIRONMENT_KEY = "environment"; ++ private static final String NETHER_CEILING_VOID_DAMAGE_HEIGHT_KEY = "nether-ceiling-void-damage-height"; ++ ++ private static final V29_ZeroWorldHeight INSTANCE = new V29_ZeroWorldHeight(); ++ ++ private V29_ZeroWorldHeight() { ++ } ++ ++ public static void apply(ConfigurationTransformation.VersionedBuilder builder) { ++ final ConfigurationTransformation transformation = ConfigurationTransformation.builder() ++ .addAction(path(FIXES_KEY, FALLING_BLOCK_HEIGHT_NERF_KEY), INSTANCE) ++ .addAction(path(FIXES_KEY, TNT_ENTITY_HEIGHT_NERF_KEY), INSTANCE) ++ .addAction(path(ENVIRONMENT_KEY, NETHER_CEILING_VOID_DAMAGE_HEIGHT_KEY), INSTANCE) ++ .build(); ++ builder.addVersion(VERSION, transformation); ++ } ++ ++ @Override ++ public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException { ++ if (value.getInt() == 0) { ++ value.set(IntOr.Disabled.DISABLED); ++ } ++ return null; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V30_RenameFilterNbtFromSpawnEgg.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V30_RenameFilterNbtFromSpawnEgg.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d08b65234192d5b639cead675114f64bf1f409c4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V30_RenameFilterNbtFromSpawnEgg.java +@@ -0,0 +1,25 @@ ++package io.papermc.paper.configuration.transformation.world.versioned; ++ ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++ ++import static org.spongepowered.configurate.NodePath.path; ++import static org.spongepowered.configurate.transformation.TransformAction.rename; ++ ++/** ++ * The {@code filter-nbt-data-from-spawn-eggs-and-related} setting had nothing ++ * to do with spawn eggs, and was just filtering bad falling blocks. ++ */ ++public final class V30_RenameFilterNbtFromSpawnEgg { ++ ++ private static final int VERSION = 30; ++ private static final NodePath OLD_PATH = path("entities", "spawning", "filter-nbt-data-from-spawn-eggs-and-related"); ++ private static final String NEW_PATH = "filter-bad-tile-entity-nbt-from-falling-blocks"; ++ ++ private V30_RenameFilterNbtFromSpawnEgg() { ++ } ++ ++ public static void apply(ConfigurationTransformation.VersionedBuilder builder) { ++ builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(OLD_PATH, rename(NEW_PATH)).build()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V31_SpawnLoadedRangeToGameRule.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V31_SpawnLoadedRangeToGameRule.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d872b1948df52759fed9c3d892aed6abfdfc8068 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V31_SpawnLoadedRangeToGameRule.java +@@ -0,0 +1,55 @@ ++package io.papermc.paper.configuration.transformation.world.versioned; ++ ++import io.papermc.paper.configuration.Configurations; ++import net.minecraft.world.level.GameRules; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++public final class V31_SpawnLoadedRangeToGameRule implements TransformAction { ++ ++ private static final int VERSION = 31; ++ private static final String SPAWN = "spawn"; ++ private static final String KEEP_SPAWN_LOADED_RANGE = "keep-spawn-loaded-range"; ++ private static final String KEEP_SPAWN_LOADED = "keep-spawn-loaded"; ++ ++ private final GameRules gameRules; ++ private final ConfigurationNode defaultsNode; ++ ++ private V31_SpawnLoadedRangeToGameRule(final GameRules gameRules, final ConfigurationNode defaultsNode) { ++ this.gameRules = gameRules; ++ this.defaultsNode = defaultsNode; ++ } ++ ++ @Override ++ public Object @Nullable [] visitPath(final NodePath path, final ConfigurationNode value) { ++ final ConfigurationNode worldSpawnNode = value.node(SPAWN); ++ final ConfigurationNode worldLoadedNode = worldSpawnNode.node(KEEP_SPAWN_LOADED); ++ final boolean keepLoaded = worldLoadedNode.getBoolean(this.defaultsNode.node(SPAWN, KEEP_SPAWN_LOADED).getBoolean()); ++ worldLoadedNode.raw(null); ++ final ConfigurationNode worldRangeNode = worldSpawnNode.node(KEEP_SPAWN_LOADED_RANGE); ++ final int range = worldRangeNode.getInt(this.defaultsNode.node(SPAWN, KEEP_SPAWN_LOADED_RANGE).getInt()); ++ worldRangeNode.raw(null); ++ if (worldSpawnNode.empty()) { ++ worldSpawnNode.raw(null); ++ } ++ if (!keepLoaded) { ++ this.gameRules.getRule(GameRules.RULE_SPAWN_CHUNK_RADIUS).set(0, null); ++ } else { ++ this.gameRules.getRule(GameRules.RULE_SPAWN_CHUNK_RADIUS).set(range, null); ++ } ++ return null; ++ } ++ ++ public static void apply(final ConfigurationTransformation.VersionedBuilder builder, final Configurations.ContextMap contextMap, final @Nullable ConfigurationNode defaultsNode) { ++ if (defaultsNode != null) { ++ builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(path(), new V31_SpawnLoadedRangeToGameRule(contextMap.require(Configurations.GAME_RULES), defaultsNode)).build()); ++ } else { ++ builder.addVersion(VERSION, ConfigurationTransformation.empty()); // increment version of default world config ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a3eaa47cfcfc4fd2a607f9b375230fada35620d3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java +@@ -0,0 +1,53 @@ ++package io.papermc.paper.configuration.type; ++ ++import org.apache.commons.lang3.BooleanUtils; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.Locale; ++import java.util.function.Predicate; ++ ++public record BooleanOrDefault(@Nullable Boolean value) { ++ private static final String DEFAULT_VALUE = "default"; ++ public static final BooleanOrDefault USE_DEFAULT = new BooleanOrDefault(null); ++ public static final ScalarSerializer<BooleanOrDefault> SERIALIZER = new Serializer(); ++ ++ public boolean or(boolean fallback) { ++ return this.value == null ? fallback : this.value; ++ } ++ ++ private static final class Serializer extends ScalarSerializer<BooleanOrDefault> { ++ Serializer() { ++ super(BooleanOrDefault.class); ++ } ++ ++ @Override ++ public BooleanOrDefault deserialize(Type type, Object obj) throws SerializationException { ++ if (obj instanceof String string) { ++ if (DEFAULT_VALUE.equalsIgnoreCase(string)) { ++ return USE_DEFAULT; ++ } ++ try { ++ return new BooleanOrDefault(BooleanUtils.toBoolean(string.toLowerCase(Locale.ROOT), "true", "false")); ++ } catch (IllegalArgumentException ex) { ++ throw new SerializationException(BooleanOrDefault.class, obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'", ex); ++ } ++ } else if (obj instanceof Boolean bool) { ++ return new BooleanOrDefault(bool); ++ } ++ throw new SerializationException(BooleanOrDefault.class, obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'"); ++ } ++ ++ @Override ++ protected Object serialize(BooleanOrDefault item, Predicate<Class<?>> typeSupported) { ++ final @Nullable Boolean value = item.value; ++ if (value != null) { ++ return value.toString(); ++ } else { ++ return DEFAULT_VALUE; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/Duration.java b/src/main/java/io/papermc/paper/configuration/type/Duration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..422ccb0b332b3e94be228b9b94f379467d6461a5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/Duration.java +@@ -0,0 +1,97 @@ ++package io.papermc.paper.configuration.type; ++ ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.Objects; ++import java.util.function.Predicate; ++import java.util.regex.Pattern; ++ ++public final class Duration { ++ ++ private static final Pattern SPACE = Pattern.compile(" "); ++ private static final Pattern NOT_NUMERIC = Pattern.compile("[^-\\d.]"); ++ public static final Serializer SERIALIZER = new Serializer(); ++ ++ private final long seconds; ++ private final String value; ++ ++ private Duration(String value) { ++ this.value = value; ++ this.seconds = getSeconds(value); ++ } ++ ++ public long seconds() { ++ return this.seconds; ++ } ++ ++ public long ticks() { ++ return this.seconds * 20; ++ } ++ ++ public String value() { ++ return this.value; ++ } ++ ++ @Override ++ public boolean equals(@Nullable Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ Duration duration = (Duration) o; ++ return seconds == duration.seconds && this.value.equals(duration.value); ++ } ++ ++ @Override ++ public int hashCode() { ++ return Objects.hash(this.seconds, this.value); ++ } ++ ++ @Override ++ public String toString() { ++ return "Duration{" + ++ "seconds=" + this.seconds + ++ ", value='" + this.value + '\'' + ++ '}'; ++ } ++ ++ public static Duration of(String time) { ++ return new Duration(time); ++ } ++ ++ private static int getSeconds(String str) { ++ str = SPACE.matcher(str).replaceAll(""); ++ final char unit = str.charAt(str.length() - 1); ++ str = NOT_NUMERIC.matcher(str).replaceAll(""); ++ double num; ++ try { ++ num = Double.parseDouble(str); ++ } catch (Exception e) { ++ num = 0D; ++ } ++ switch (unit) { ++ case 'd': num *= (double) 60*60*24; break; ++ case 'h': num *= (double) 60*60; break; ++ case 'm': num *= (double) 60; break; ++ default: case 's': break; ++ } ++ return (int) num; ++ } ++ ++ static final class Serializer extends ScalarSerializer<Duration> { ++ private Serializer() { ++ super(Duration.class); ++ } ++ ++ @Override ++ public Duration deserialize(Type type, Object obj) throws SerializationException { ++ return new Duration(obj.toString()); ++ } ++ ++ @Override ++ protected Object serialize(Duration item, Predicate<Class<?>> typeSupported) { ++ return item.value(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/DurationOrDisabled.java b/src/main/java/io/papermc/paper/configuration/type/DurationOrDisabled.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3f17e75e08e1cb4359b96a78c5b8d5284c484e43 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/DurationOrDisabled.java +@@ -0,0 +1,54 @@ ++package io.papermc.paper.configuration.type; ++ ++import java.lang.reflect.Type; ++import java.util.Optional; ++import java.util.function.Predicate; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++@SuppressWarnings("OptionalUsedAsFieldOrParameterType") ++public final class DurationOrDisabled { ++ private static final String DISABLE_VALUE = "disabled"; ++ public static final DurationOrDisabled USE_DISABLED = new DurationOrDisabled(Optional.empty()); ++ public static final ScalarSerializer<DurationOrDisabled> SERIALIZER = new Serializer(); ++ ++ private Optional<Duration> value; ++ ++ public DurationOrDisabled(final Optional<Duration> value) { ++ this.value = value; ++ } ++ ++ public Optional<Duration> value() { ++ return this.value; ++ } ++ ++ public void value(final Optional<Duration> value) { ++ this.value = value; ++ } ++ ++ public Duration or(final Duration fallback) { ++ return this.value.orElse(fallback); ++ } ++ ++ private static final class Serializer extends ScalarSerializer<DurationOrDisabled> { ++ Serializer() { ++ super(DurationOrDisabled.class); ++ } ++ ++ @Override ++ public DurationOrDisabled deserialize(final Type type, final Object obj) throws SerializationException { ++ if (obj instanceof final String string) { ++ if (DISABLE_VALUE.equalsIgnoreCase(string)) { ++ return USE_DISABLED; ++ } ++ return new DurationOrDisabled(Optional.of(Duration.SERIALIZER.deserialize(string))); ++ } ++ throw new SerializationException(obj + "(" + type + ") is not a duration or '" + DISABLE_VALUE + "'"); ++ } ++ ++ @Override ++ protected Object serialize(final DurationOrDisabled item, final Predicate<Class<?>> typeSupported) { ++ return item.value.map(Duration::value).orElse(DISABLE_VALUE); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/EngineMode.java b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f8b685762f59049fde88e8d1bc10e1504916010 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java +@@ -0,0 +1,37 @@ ++package io.papermc.paper.configuration.type; ++ ++import io.papermc.paper.configuration.serializer.EngineModeSerializer; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++ ++public enum EngineMode { ++ ++ HIDE(1, "hide ores"), OBFUSCATE(2, "obfuscate"), OBFUSCATE_LAYER(3, "obfuscate layer"); ++ ++ public static final ScalarSerializer<EngineMode> SERIALIZER = new EngineModeSerializer(); ++ ++ private final int id; ++ private final String description; ++ ++ EngineMode(int id, String description) { ++ this.id = id; ++ this.description = description; ++ } ++ ++ public static EngineMode valueOf(int id) { ++ for (EngineMode engineMode : values()) { ++ if (engineMode.getId() == id) { ++ return engineMode; ++ } ++ } ++ ++ throw new IllegalArgumentException("No enum constant with id " + id); ++ } ++ ++ public int getId() { ++ return id; ++ } ++ ++ public String getDescription() { ++ return description; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java +new file mode 100644 +index 0000000000000000000000000000000000000000..24763d3d270c29c95e0b3e85111145234f660a62 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java +@@ -0,0 +1,38 @@ ++package io.papermc.paper.configuration.type.fallback; ++ ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.util.Map; ++import java.util.OptionalInt; ++import java.util.Set; ++ ++public class ArrowDespawnRate extends FallbackValue.Int { ++ ++ ArrowDespawnRate(Map<ContextKey<?>, Object> context, Object value) throws SerializationException { ++ super(context, fromObject(value)); ++ } ++ ++ private ArrowDespawnRate(Map<ContextKey<?>, Object> context) { ++ super(context, OptionalInt.empty()); ++ } ++ ++ @Override ++ protected OptionalInt process(int value) { ++ return Util.negToDef(value); ++ } ++ ++ @Override ++ public Set<ContextKey<?>> required() { ++ return Set.of(FallbackValue.SPIGOT_WORLD_CONFIG); ++ } ++ ++ @Override ++ protected int fallback() { ++ return this.get(FallbackValue.SPIGOT_WORLD_CONFIG).arrowDespawnRate; ++ } ++ ++ public static ArrowDespawnRate def(SpigotWorldConfig spigotConfig) { ++ return new ArrowDespawnRate(FallbackValue.SPIGOT_WORLD_CONFIG.singleton(spigotConfig)); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java b/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0f2765b2edc63c11ba3c57ff55c536054826a995 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java +@@ -0,0 +1,39 @@ ++package io.papermc.paper.configuration.type.fallback; ++ ++import net.minecraft.server.MinecraftServer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.util.Map; ++import java.util.OptionalInt; ++import java.util.Set; ++import java.util.function.Supplier; ++ ++public class AutosavePeriod extends FallbackValue.Int { ++ ++ AutosavePeriod(Map<ContextKey<?>, Object> contextMap, Object value) throws SerializationException { ++ super(contextMap, fromObject(value)); ++ } ++ ++ private AutosavePeriod(Map<ContextKey<?>, Object> contextMap) { ++ super(contextMap, OptionalInt.empty()); ++ } ++ ++ @Override ++ protected OptionalInt process(int value) { ++ return Util.negToDef(value); ++ } ++ ++ @Override ++ protected Set<ContextKey<?>> required() { ++ return Set.of(FallbackValue.MINECRAFT_SERVER); ++ } ++ ++ @Override ++ protected int fallback() { ++ return this.get(FallbackValue.MINECRAFT_SERVER).get().autosavePeriod; ++ } ++ ++ public static AutosavePeriod def() { ++ return new AutosavePeriod(FallbackValue.MINECRAFT_SERVER.singleton(MinecraftServer::getServer)); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a3a1d398d783c37914fb6d646e11361afee687b8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java +@@ -0,0 +1,102 @@ ++package io.papermc.paper.configuration.type.fallback; ++ ++import com.google.common.base.Preconditions; ++import net.minecraft.server.MinecraftServer; ++import org.apache.commons.lang3.math.NumberUtils; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.util.Map; ++import java.util.Objects; ++import java.util.OptionalInt; ++import java.util.Set; ++import java.util.function.Supplier; ++ ++@SuppressWarnings("OptionalUsedAsFieldOrParameterType") ++public sealed abstract class FallbackValue permits FallbackValue.Int { ++ ++ private static final String DEFAULT_VALUE = "default"; ++ static final ContextKey<SpigotWorldConfig> SPIGOT_WORLD_CONFIG = new ContextKey<>("SpigotWorldConfig"); ++ static final ContextKey<Supplier<MinecraftServer>> MINECRAFT_SERVER = new ContextKey<>("MinecraftServer"); ++ ++ private final Map<ContextKey<?>, Object> contextMap; ++ ++ protected FallbackValue(Map<ContextKey<?>, Object> contextMap) { ++ for (ContextKey<?> contextKey : this.required()) { ++ Preconditions.checkArgument(contextMap.containsKey(contextKey), contextMap + " is missing " + contextKey); ++ } ++ this.contextMap = contextMap; ++ } ++ ++ protected abstract String serialize(); ++ ++ protected abstract Set<ContextKey<?>> required(); ++ ++ @SuppressWarnings("unchecked") ++ protected <T> T get(ContextKey<T> contextKey) { ++ return (T) Objects.requireNonNull(this.contextMap.get(contextKey), "Missing " + contextKey); ++ } ++ ++ public non-sealed abstract static class Int extends FallbackValue { ++ ++ private final OptionalInt value; ++ ++ Int(Map<ContextKey<?>, Object> contextMap, OptionalInt value) { ++ super(contextMap); ++ if (value.isEmpty()) { ++ this.value = value; ++ } else { ++ this.value = this.process(value.getAsInt()); ++ } ++ } ++ ++ public int value() { ++ return value.orElseGet(this::fallback); ++ } ++ ++ @Override ++ protected final String serialize() { ++ return value.isPresent() ? String.valueOf(this.value.getAsInt()) : DEFAULT_VALUE; ++ } ++ ++ protected OptionalInt process(int value) { ++ return OptionalInt.of(value); ++ } ++ ++ protected abstract int fallback(); ++ ++ protected static OptionalInt fromObject(Object obj) throws SerializationException { ++ if (obj instanceof OptionalInt optionalInt) { ++ return optionalInt; ++ } else if (obj instanceof String string) { ++ if (DEFAULT_VALUE.equalsIgnoreCase(string)) { ++ return OptionalInt.empty(); ++ } ++ if (NumberUtils.isParsable(string)) { ++ return OptionalInt.of(Integer.parseInt(string)); ++ } ++ } else if (obj instanceof Integer num) { ++ return OptionalInt.of(num); ++ } ++ throw new SerializationException(obj + " is not a integer or '" + DEFAULT_VALUE + "'"); ++ } ++ } ++ ++ static class ContextKey<T> { ++ ++ private final String name; ++ ++ ContextKey(String name) { ++ this.name = name; ++ } ++ ++ @Override ++ public String toString() { ++ return this.name; ++ } ++ ++ Map<ContextKey<?>, Object> singleton(T value) { ++ return Map.of(this, value); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8d0fcd038e12c70a3a5aaf2669452589d9055255 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java +@@ -0,0 +1,55 @@ ++package io.papermc.paper.configuration.type.fallback; ++ ++import net.minecraft.server.MinecraftServer; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Predicate; ++import java.util.function.Supplier; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++public class FallbackValueSerializer extends ScalarSerializer<FallbackValue> { ++ ++ private static final Map<Class<?>, FallbackCreator<?>> REGISTRY = new HashMap<>(); ++ ++ static { ++ REGISTRY.put(ArrowDespawnRate.class, ArrowDespawnRate::new); ++ REGISTRY.put(AutosavePeriod.class, AutosavePeriod::new); ++ } ++ ++ FallbackValueSerializer(Map<FallbackValue.ContextKey<?>, Object> contextMap) { ++ super(FallbackValue.class); ++ this.contextMap = contextMap; ++ } ++ ++ @FunctionalInterface ++ private interface FallbackCreator<T extends FallbackValue> { ++ T create(Map<FallbackValue.ContextKey<?>, Object> context, Object value) throws SerializationException; ++ } ++ ++ private final Map<FallbackValue.ContextKey<?>, Object> contextMap; ++ ++ @Override ++ public FallbackValue deserialize(Type type, Object obj) throws SerializationException { ++ final @Nullable FallbackCreator<?> creator = REGISTRY.get(erase(type)); ++ if (creator == null) { ++ throw new SerializationException(type + " does not have a FallbackCreator registered"); ++ } ++ return creator.create(this.contextMap, obj); ++ } ++ ++ @Override ++ protected Object serialize(FallbackValue item, Predicate<Class<?>> typeSupported) { ++ return item.serialize(); ++ } ++ ++ public static FallbackValueSerializer create(SpigotWorldConfig config, Supplier<MinecraftServer> server) { ++ return new FallbackValueSerializer(Map.of(FallbackValue.SPIGOT_WORLD_CONFIG, config, FallbackValue.MINECRAFT_SERVER, server)); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java b/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java +new file mode 100644 +index 0000000000000000000000000000000000000000..70cc7b45e7355f6c8476a74a070f1266e4cca189 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.configuration.type.fallback; ++ ++import java.util.OptionalInt; ++ ++final class Util { ++ ++ static OptionalInt negToDef(int value) { ++ return value < 0 ? OptionalInt.empty() : OptionalInt.of(value); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java b/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..31068170086aeac51a2adb952b19672e875ba528 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java +@@ -0,0 +1,11 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++ ++@Retention(RetentionPolicy.RUNTIME) ++@Target(ElementType.FIELD) ++public @interface BelowZeroToEmpty { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java b/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5833c06b0707906ab7d10786ecd115f20e42e925 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java +@@ -0,0 +1,59 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import com.google.common.base.Preconditions; ++import java.util.OptionalDouble; ++import java.util.function.Function; ++import java.util.function.Predicate; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++ ++public interface DoubleOr { ++ ++ default double or(final double fallback) { ++ return this.value().orElse(fallback); ++ } ++ ++ OptionalDouble value(); ++ ++ default double doubleValue() { ++ return this.value().orElseThrow(); ++ } ++ ++ record Default(OptionalDouble value) implements DoubleOr { ++ private static final String DEFAULT_VALUE = "default"; ++ public static final Default USE_DEFAULT = new Default(OptionalDouble.empty()); ++ public static final ScalarSerializer<Default> SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT); ++ } ++ ++ final class Serializer<T extends DoubleOr> extends OptionalNumSerializer<T, OptionalDouble> { ++ Serializer(final Class<T> classOfT, final Function<OptionalDouble, T> factory, String emptySerializedValue, T emptyValue) { ++ super(classOfT, emptySerializedValue, emptyValue, OptionalDouble::empty, OptionalDouble::isEmpty, factory, double.class); ++ } ++ ++ @Override ++ protected Object serialize(final T item, final Predicate<Class<?>> typeSupported) { ++ final OptionalDouble value = item.value(); ++ if (value.isPresent()) { ++ return value.getAsDouble(); ++ } else { ++ return this.emptySerializedValue; ++ } ++ } ++ ++ @Override ++ protected OptionalDouble full(final String value) { ++ return OptionalDouble.of(Double.parseDouble(value)); ++ } ++ ++ @Override ++ protected OptionalDouble full(final Number num) { ++ return OptionalDouble.of(num.doubleValue()); ++ } ++ ++ @Override ++ protected boolean belowZero(final OptionalDouble value) { ++ Preconditions.checkArgument(value.isPresent()); ++ return value.getAsDouble() < 0; ++ } ++ } ++} ++ +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java b/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java +new file mode 100644 +index 0000000000000000000000000000000000000000..09f16e5dde565801b153bd6705637c5f71427c8a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java +@@ -0,0 +1,81 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import com.google.common.base.Preconditions; ++import com.mojang.logging.LogUtils; ++import java.util.OptionalInt; ++import java.util.function.Function; ++import java.util.function.IntPredicate; ++import java.util.function.Predicate; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++ ++public interface IntOr { ++ ++ Logger LOGGER = LogUtils.getClassLogger(); ++ ++ default int or(final int fallback) { ++ return this.value().orElse(fallback); ++ } ++ ++ OptionalInt value(); ++ ++ default int intValue() { ++ return this.value().orElseThrow(); ++ } ++ ++ record Default(OptionalInt value) implements IntOr { ++ private static final String DEFAULT_VALUE = "default"; ++ public static final Default USE_DEFAULT = new Default(OptionalInt.empty()); ++ public static final ScalarSerializer<Default> SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT); ++ } ++ ++ record Disabled(OptionalInt value) implements IntOr { ++ private static final String DISABLED_VALUE = "disabled"; ++ public static final Disabled DISABLED = new Disabled(OptionalInt.empty()); ++ public static final ScalarSerializer<Disabled> SERIALIZER = new Serializer<>(Disabled.class, Disabled::new, DISABLED_VALUE, DISABLED); ++ ++ public boolean test(IntPredicate predicate) { ++ return this.value.isPresent() && predicate.test(this.value.getAsInt()); ++ } ++ ++ public boolean enabled() { ++ return this.value.isPresent(); ++ } ++ } ++ ++ final class Serializer<T extends IntOr> extends OptionalNumSerializer<T, OptionalInt> { ++ ++ private Serializer(Class<T> classOfT, Function<OptionalInt, T> factory, String emptySerializedValue, T emptyValue) { ++ super(classOfT, emptySerializedValue, emptyValue, OptionalInt::empty, OptionalInt::isEmpty, factory, int.class); ++ } ++ ++ @Override ++ protected OptionalInt full(final String value) { ++ return OptionalInt.of(Integer.parseInt(value)); ++ } ++ ++ @Override ++ protected OptionalInt full(final Number num) { ++ if (num.intValue() != num.doubleValue() || num.intValue() != num.longValue()) { ++ LOGGER.error("{} cannot be converted to an integer without losing information", num); ++ } ++ return OptionalInt.of(num.intValue()); ++ } ++ ++ @Override ++ protected boolean belowZero(final OptionalInt value) { ++ Preconditions.checkArgument(value.isPresent()); ++ return value.getAsInt() < 0; ++ } ++ ++ @Override ++ protected Object serialize(final T item, final Predicate<Class<?>> typeSupported) { ++ final OptionalInt value = item.value(); ++ if (value.isPresent()) { ++ return value.getAsInt(); ++ } else { ++ return this.emptySerializedValue; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java b/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..614aba60bb07946a144650fd3aedb31649057ae1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java +@@ -0,0 +1,58 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import java.lang.reflect.AnnotatedType; ++import java.util.function.Function; ++import java.util.function.Predicate; ++import java.util.function.Supplier; ++import org.apache.commons.lang3.math.NumberUtils; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public abstract class OptionalNumSerializer<T, O> extends ScalarSerializer.Annotated<T> { ++ ++ protected final String emptySerializedValue; ++ protected final T emptyValue; ++ private final Supplier<O> empty; ++ private final Predicate<O> isEmpty; ++ private final Function<O, T> factory; ++ private final Class<?> number; ++ ++ protected OptionalNumSerializer(final Class<T> classOfT, final String emptySerializedValue, final T emptyValue, final Supplier<O> empty, final Predicate<O> isEmpty, final Function<O, T> factory, final Class<?> number) { ++ super(classOfT); ++ this.emptySerializedValue = emptySerializedValue; ++ this.emptyValue = emptyValue; ++ this.empty = empty; ++ this.isEmpty = isEmpty; ++ this.factory = factory; ++ this.number = number; ++ } ++ ++ @Override ++ public final T deserialize(final AnnotatedType type, final Object obj) throws SerializationException { ++ final O value; ++ if (obj instanceof String string) { ++ if (this.emptySerializedValue.equalsIgnoreCase(string)) { ++ value = this.empty.get(); ++ } else if (NumberUtils.isParsable(string)) { ++ value = this.full(string); ++ } else { ++ throw new SerializationException("%s (%s) is not a(n) %s or '%s'".formatted(obj, type, this.number.getSimpleName(), this.emptySerializedValue)); ++ } ++ } else if (obj instanceof Number num) { ++ value = this.full(num); ++ } else { ++ throw new SerializationException("%s (%s) is not a(n) %s or '%s'".formatted(obj, type, this.number.getSimpleName(), this.emptySerializedValue)); ++ } ++ if (this.isEmpty.test(value) || (type.isAnnotationPresent(BelowZeroToEmpty.class) && this.belowZero(value))) { ++ return this.emptyValue; ++ } else { ++ return this.factory.apply(value); ++ } ++ } ++ ++ protected abstract O full(final String value); ++ ++ protected abstract O full(final Number num); ++ ++ protected abstract boolean belowZero(O value); ++} +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index b334265d4015fec13d7fedbffba2b6c22f4c8bc8..5b4ac7b4fd0077e900e9f788963f1613bbc9a5d0 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -129,6 +129,10 @@ public class Main { + RegionFileVersion.configure(dedicatedserversettings.getProperties().regionFileComression); + Path path2 = Paths.get("eula.txt"); + Eula eula = new Eula(path2); ++ // Paper start - load config files early for access below if needed ++ org.bukkit.configuration.file.YamlConfiguration bukkitConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("bukkit-settings")); ++ org.bukkit.configuration.file.YamlConfiguration spigotConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("spigot-settings")); ++ // Paper end - load config files early for access below if needed + + if (optionset.has("initSettings")) { // CraftBukkit + // CraftBukkit start - SPIGOT-5761: Create bukkit.yml and commands.yml if not present +@@ -163,7 +167,7 @@ public class Main { + } + + File file = (File) optionset.valueOf("universe"); // CraftBukkit +- Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file); ++ Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file, optionset); // Paper - pass OptionSet to load paper config files + // CraftBukkit start + String s = (String) Optional.ofNullable((String) optionset.valueOf("world")).orElse(dedicatedserversettings.getProperties().levelName); + LevelStorageSource convertable = LevelStorageSource.createDefault(file.toPath()); +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index db2031ffbc802bd850ab40acaec2a9624b753f4c..299afe72a2fc2affca196f2fdfbf27e5533c51a6 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -309,6 +309,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + private static final int SAMPLE_INTERVAL = 100; + public final double[] recentTps = new double[ 3 ]; + // Spigot end ++ public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files + + public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) { + AtomicReference<S> atomicreference = new AtomicReference(); +@@ -405,6 +406,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this)); + // CraftBukkit end ++ this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files + } + + private void readScoreboard(DimensionDataStorage persistentStateManager) { +diff --git a/src/main/java/net/minecraft/server/Services.java b/src/main/java/net/minecraft/server/Services.java +index dfbb04800d6f1dcbb909fcdfeb1ebf1a5efa6a48..5928e5f1934b8e247ba516595018ed5c633d3b5d 100644 +--- a/src/main/java/net/minecraft/server/Services.java ++++ b/src/main/java/net/minecraft/server/Services.java +@@ -10,16 +10,32 @@ import javax.annotation.Nullable; + import net.minecraft.server.players.GameProfileCache; + import net.minecraft.util.SignatureValidator; + ++ + public record Services( +- MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache ++ MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache, @javax.annotation.Nullable io.papermc.paper.configuration.PaperConfigurations paperConfigurations // Paper - add paper configuration files + ) { ++ // Paper start - add paper configuration files ++ public Services(MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache) { ++ this(sessionService, servicesKeySet, profileRepository, profileCache, null); ++ } ++ ++ @Override ++ public io.papermc.paper.configuration.PaperConfigurations paperConfigurations() { ++ return java.util.Objects.requireNonNull(this.paperConfigurations); ++ } ++ // Paper end - add paper configuration files + private static final String USERID_CACHE_FILE = "usercache.json"; + +- public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory) { ++ public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory, joptsimple.OptionSet optionSet) throws Exception { // Paper - add optionset to load paper config files + MinecraftSessionService minecraftSessionService = authenticationService.createMinecraftSessionService(); + GameProfileRepository gameProfileRepository = authenticationService.createProfileRepository(); + GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, new File(rootDirectory, "usercache.json")); +- return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache); ++ // Paper start - load paper config files from cli options ++ 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, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache, paperConfigurations); ++ // Paper end - load paper config files from cli options + } + + @Nullable +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index d3c7c1e016af5d04b5a18a6ced898cf4dc9b1700..cd26616aba6abd44abc5eb8b01cc96f29248aecd 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -200,6 +200,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings")); + org.spigotmc.SpigotConfig.registerCommands(); + // Spigot end ++ // Paper start - initialize global and world-defaults configuration ++ this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess()); ++ this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); ++ // Paper end - initialize global and world-defaults configuration + + this.setPvpAllowed(dedicatedserverproperties.pvp); + this.setFlightAllowed(dedicatedserverproperties.allowFlight); +diff --git a/src/main/java/net/minecraft/server/dedicated/Settings.java b/src/main/java/net/minecraft/server/dedicated/Settings.java +index 6d89a5414f46a0c30badb4fcd25bc6cb6d18db3a..0ec3b546db0cf3858dd9cd9ea067d1d6713a8491 100644 +--- a/src/main/java/net/minecraft/server/dedicated/Settings.java ++++ b/src/main/java/net/minecraft/server/dedicated/Settings.java +@@ -119,6 +119,7 @@ public abstract class Settings<T extends Settings<T>> { + try { + // CraftBukkit start - Don't attempt writing to file if it's read only + if (path.toFile().exists() && !path.toFile().canWrite()) { ++ Settings.LOGGER.warn("Can not write to file {}, skipping.", path); // Paper - log message file is read-only + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 63ad1280667a02e00f9c636193e38f933cad0185..a696a0d168987aaa4e59c471a23eeb48d683c1b2 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -242,7 +242,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Holder holder = worlddimension.type(); // CraftBukkit - decompile error + + // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error +- super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env); ++ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), 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, minecraftserver.registryAccess(), iworlddataserver.getGameRules()))); // Paper - create paper world configs + 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 e433037a03ffafabb952887ae3980e1d51411d4c..c061813d275fbc48d7629cc59d90dbb4c347516c 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -156,6 +156,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); + public boolean populating; + public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot ++ // Paper start - add paper world config ++ private final io.papermc.paper.configuration.WorldConfiguration paperConfig; ++ public io.papermc.paper.configuration.WorldConfiguration paperConfig() { ++ return this.paperConfig; ++ } ++ // Paper end - add paper world config + + public final SpigotTimings.WorldTimingsHandler timings; // Spigot + public static BlockPos lastPhysicsProblem; // Spigot +@@ -173,8 +179,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey<LevelStem> getTypeKey(); + +- protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env) { ++ protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> 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<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper - create paper world config + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot ++ this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config + this.generator = gen; + this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env); + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 6d1d3f9cf7451494af7c57d8bb1cd0ed25ea63e2..f1302dfb68bf8e4e1f4d8b084ad81422f65eecc4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -954,6 +954,7 @@ public final class CraftServer implements Server { + } + + org.spigotmc.SpigotConfig.init((File) this.console.options.valueOf("spigot-settings")); // Spigot ++ this.console.paperConfigurations.reloadConfigs(this.console); + for (ServerLevel world : this.console.getAllLevels()) { + world.serverLevelData.setDifficulty(config.difficulty); + world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals); +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 4b8602b168f9dd386aa72b4e5d189c441c93542e..1e3ca7ca98abfd5be233a7eeb6dad201776d2d6a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -142,6 +142,19 @@ public class Main { + .defaultsTo(new File("spigot.yml")) + .describedAs("Yml file"); + // Spigot End ++ ++ // Paper start ++ acceptsAll(asList("paper-dir", "paper-settings-directory"), "Directory for Paper settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File(io.papermc.paper.configuration.PaperConfigurations.CONFIG_DIR)) ++ .describedAs("Config directory"); ++ acceptsAll(asList("paper", "paper-settings"), "File for Paper settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("paper.yml")) ++ .describedAs("Yml file"); ++ // Paper end + } + }; + +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index 192b6fbd34b9b90112f869ae6e367ab9ba5a5906..08b0ca7b68bf238366f4d6904478852ecbe9394a 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -96,7 +96,7 @@ public class SpigotConfig + } + } + +- static void readConfig(Class<?> clazz, Object instance) ++ public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public + { + for ( Method method : clazz.getDeclaredMethods() ) + { +diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java +index a6d2ce801b236b046b94913bccf7eccfc561f35a..b244b65799d9c082b8b1639bad15727442e63168 100644 +--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java ++++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java +@@ -58,8 +58,14 @@ public class SpigotWorldConfig + + public int getInt(String path, int def) + { +- this.config.addDefault( "world-settings.default." + path, def ); +- return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path ) ); ++ // Paper start - get int without setting default ++ return this.getInt(path, def, true); ++ } ++ public int getInt(String path, int def, boolean setDef) ++ { ++ if (setDef) this.config.addDefault( "world-settings.default." + path, def ); ++ return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path, def ) ); ++ // Paper end + } + + public <T> List getList(String path, T def) +diff --git a/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0396589795da1f83ddf62426236dde9a3afa1376 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.configuration; ++ ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public final class GlobalConfigTestingBase { ++ ++ public static void setupGlobalConfigForTest() { ++ //noinspection ConstantConditions ++ if (GlobalConfiguration.get() == null) { ++ ConfigurationNode node = PaperConfigurations.createForTesting(); ++ try { ++ GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class); ++ GlobalConfiguration.set(globalConfiguration); ++ } catch (SerializationException e) { ++ throw new RuntimeException(e); ++ } ++ } ++ } ++} +diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java +index 544307fe34cbcfa286a7d7b30900ebea127d189e..1b1d51a68c0abe7d8f0aa1172064192c71ae645e 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 { + BIOMES = REGISTRY_CUSTOM.registryOrThrow(Registries.BIOME); + + DummyServer.setup(); ++ io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper + + CraftRegistry.setMinecraftRegistry(REGISTRY_CUSTOM); + diff --git a/patches/server/0006-MC-Dev-fixes.patch b/patches/server/0006-MC-Dev-fixes.patch new file mode 100644 index 0000000000..536a66afb3 --- /dev/null +++ b/patches/server/0006-MC-Dev-fixes.patch @@ -0,0 +1,111 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Wed, 30 Mar 2016 19:36:20 -0400 +Subject: [PATCH] MC Dev fixes + + +diff --git a/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java b/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java +index 643bb8860962ad691b11073f6dbf406bf7ec5fb1..9b8ec1fd158f6e51779be263fd56b9119d0d9bbb 100644 +--- a/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java ++++ b/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java +@@ -78,6 +78,6 @@ public class ItemInput { + } + + private String getItemName() { +- return this.item.unwrapKey().map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString(); ++ return this.item.unwrapKey().<Object>map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString(); // Paper - decompile fix + } + } +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index 0ef1a7b2a17e81144d594f29f7b5e54d5038dcf4..8994a381b05dcdd1163d2e7a0b63a8875b6063ed 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -439,12 +439,12 @@ public class BlockPos extends Vec3i { + if (this.index == l) { + return this.endOfData(); + } else { +- int i = this.index % i; +- int j = this.index / i; +- int k = j % j; +- int l = j / j; ++ int offsetX = this.index % i; // Paper - decomp fix ++ int u = this.index / i; // Paper - decomp fix ++ int offsetY = u % j; // Paper - decomp fix ++ int offsetZ = u / j; // Paper - decomp fix + this.index++; +- return this.cursor.set(startX + i, startY + k, startZ + l); ++ return this.cursor.set(startX + offsetX, startY + offsetY, startZ + offsetZ); // Paper - decomp fix + } + } + }; +diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +index a467eb6b3b74cdb5378ff5f3043efbe6f4a6f06e..34b3b3251da21bce616870d312fd42fd58ba7881 100644 +--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java ++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +@@ -317,7 +317,7 @@ public class BuiltInRegistries { + Bootstrap.checkBootstrapCalled(() -> "registry " + key); + ResourceLocation resourceLocation = key.location(); + LOADERS.put(resourceLocation, () -> initializer.run(registry)); +- WRITABLE_REGISTRY.register((ResourceKey<WritableRegistry<?>>)key, registry, RegistrationInfo.BUILT_IN); ++ WRITABLE_REGISTRY.register((ResourceKey)key, registry, RegistrationInfo.BUILT_IN); // Paper - decompile fix + return registry; + } + +diff --git a/src/main/java/net/minecraft/nbt/TagParser.java b/src/main/java/net/minecraft/nbt/TagParser.java +index a614e960fcd5958ad17b679eee8a8e6926f58e62..da101bca71f4710812621b98f0a0d8cab180346a 100644 +--- a/src/main/java/net/minecraft/nbt/TagParser.java ++++ b/src/main/java/net/minecraft/nbt/TagParser.java +@@ -253,11 +253,11 @@ public class TagParser { + } + + if (typeReader == ByteTag.TYPE) { +- list.add((T)((NumericTag)tag).getAsByte()); ++ list.add((T)(Byte)((NumericTag)tag).getAsByte()); // Paper - decompile fix + } else if (typeReader == LongTag.TYPE) { +- list.add((T)((NumericTag)tag).getAsLong()); ++ list.add((T)(Long)((NumericTag)tag).getAsLong()); // Paper - decompile fix + } else { +- list.add((T)((NumericTag)tag).getAsInt()); ++ list.add((T)(Integer)((NumericTag)tag).getAsInt()); // Paper - decompile fix + } + + if (!this.hasElementSeparator()) { +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 299afe72a2fc2affca196f2fdfbf27e5533c51a6..40adb6117b9e0d5f70103113202a07715e403e2a 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1952,7 +1952,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + PackRepository resourcepackrepository = this.packRepository; + + Objects.requireNonNull(this.packRepository); +- return stream.map(resourcepackrepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()); // CraftBukkit - decompile error ++ return stream.<Pack>map(resourcepackrepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()); // CraftBukkit - decompile error // Paper - decompile error // todo: is this needed anymore? + }, this).thenCompose((immutablelist) -> { + MultiPackResourceManager resourcemanager = new MultiPackResourceManager(PackType.SERVER_DATA, immutablelist); + +diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java +index 661a6274a800ca9b91bdb809d026972d23c3b263..ea72dcb064a35bc6245bc5c94d592efedd8faf41 100644 +--- a/src/main/java/net/minecraft/util/SortedArraySet.java ++++ b/src/main/java/net/minecraft/util/SortedArraySet.java +@@ -28,7 +28,7 @@ public class SortedArraySet<T> extends AbstractSet<T> { + } + + public static <T extends Comparable<T>> SortedArraySet<T> create(int initialCapacity) { +- return new SortedArraySet<>(initialCapacity, Comparator.naturalOrder()); ++ return new SortedArraySet<>(initialCapacity, Comparator.<T>naturalOrder()); // Paper - decompile fix + } + + public static <T> SortedArraySet<T> create(Comparator<T> comparator) { +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 8eb1aca72df0bca292473e90ecb74159db4fe034..4b4dcee6abe7a6db43638d04665125eec560496e 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Pillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +@@ -66,7 +66,7 @@ public class Pillager extends AbstractIllager implements CrossbowAttackMob, Inve + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); +- this.goalSelector.addGoal(2, new Raider.HoldGroundAttackGoal(this, this, 10.0F)); ++ this.goalSelector.addGoal(2, new Raider.HoldGroundAttackGoal(this, 10.0F)); // Paper - decomp fix + 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)); diff --git a/patches/server/0007-ConcurrentUtil.patch b/patches/server/0007-ConcurrentUtil.patch new file mode 100644 index 0000000000..b5d70f9b38 --- /dev/null +++ b/patches/server/0007-ConcurrentUtil.patch @@ -0,0 +1,7018 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Sun, 23 Jan 2022 22:58:11 -0800 +Subject: [PATCH] ConcurrentUtil + + +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f4415f782b32fed25da98e44b172f717c4d46e34 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java +@@ -0,0 +1,1402 @@ ++package ca.spottedleaf.concurrentutil.collection; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Iterator; ++import java.util.List; ++import java.util.NoSuchElementException; ++import java.util.Queue; ++import java.util.Spliterator; ++import java.util.Spliterators; ++import java.util.function.Consumer; ++import java.util.function.IntFunction; ++import java.util.function.Predicate; ++ ++/** ++ * MT-Safe linked first in first out ordered queue. ++ * ++ * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is ++ * not any slower in lower contention reads/writes. ++ * <p> ++ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}. ++ * </p> ++ * <p><b> ++ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if ++ * they are at the head of the queue. ++ * </b></p> ++ * @param <E> Type of element in this queue. ++ */ ++public class MultiThreadedQueue<E> implements Queue<E> { ++ ++ protected volatile LinkedNode<E> head; /* Always non-null, high chance of being the actual head */ ++ ++ protected volatile LinkedNode<E> tail; /* Always non-null, high chance of being the actual tail */ ++ ++ /* Note that it is possible to reach head from tail. */ ++ ++ /* IMPL NOTE: Leave hashCode and equals to their defaults */ ++ ++ protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class); ++ protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class); ++ ++ /* head */ ++ ++ protected final void setHeadPlain(final LinkedNode<E> newHead) { ++ HEAD_HANDLE.set(this, newHead); ++ } ++ ++ protected final void setHeadOpaque(final LinkedNode<E> newHead) { ++ HEAD_HANDLE.setOpaque(this, newHead); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getHeadPlain() { ++ return (LinkedNode<E>)HEAD_HANDLE.get(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getHeadOpaque() { ++ return (LinkedNode<E>)HEAD_HANDLE.getOpaque(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getHeadAcquire() { ++ return (LinkedNode<E>)HEAD_HANDLE.getAcquire(this); ++ } ++ ++ /* tail */ ++ ++ protected final void setTailPlain(final LinkedNode<E> newTail) { ++ TAIL_HANDLE.set(this, newTail); ++ } ++ ++ protected final void setTailOpaque(final LinkedNode<E> newTail) { ++ TAIL_HANDLE.setOpaque(this, newTail); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getTailPlain() { ++ return (LinkedNode<E>)TAIL_HANDLE.get(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getTailOpaque() { ++ return (LinkedNode<E>)TAIL_HANDLE.getOpaque(this); ++ } ++ ++ /** ++ * Constructs a {@code MultiThreadedQueue}, initially empty. ++ * <p> ++ * The returned object may not be published without synchronization. ++ * </p> ++ */ ++ public MultiThreadedQueue() { ++ final LinkedNode<E> value = new LinkedNode<>(null, null); ++ this.setHeadPlain(value); ++ this.setTailPlain(value); ++ } ++ ++ /** ++ * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}. ++ * <p> ++ * The returned object may not be published without synchronization. ++ * </p> ++ * @param collection The specified collection. ++ * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements. ++ */ ++ public MultiThreadedQueue(final Iterable<? extends E> collection) { ++ final Iterator<? extends E> elements = collection.iterator(); ++ ++ if (!elements.hasNext()) { ++ final LinkedNode<E> value = new LinkedNode<>(null, null); ++ this.setHeadPlain(value); ++ this.setTailPlain(value); ++ return; ++ } ++ ++ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); ++ LinkedNode<E> tail = head; ++ ++ while (elements.hasNext()) { ++ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); ++ tail.setNextPlain(next); ++ tail = next; ++ } ++ ++ this.setHeadPlain(head); ++ this.setTailPlain(tail); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public E remove() throws NoSuchElementException { ++ final E ret = this.poll(); ++ ++ if (ret == null) { ++ throw new NoSuchElementException(); ++ } ++ ++ return ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ * <p> ++ * Contrary to the specification of {@link Collection#add}, this method will fail to add the element to this queue ++ * and return {@code false} if this queue is add-blocked. ++ * </p> ++ */ ++ @Override ++ public boolean add(final E element) { ++ return this.offer(element); ++ } ++ ++ /** ++ * Adds the specified element to the tail of this queue. If this queue is currently add-locked, then the queue is ++ * released from that lock and this element is added. The unlock operation and addition of the specified ++ * element is atomic. ++ * @param element The specified element. ++ * @return {@code true} if this queue previously allowed additions ++ */ ++ public boolean forceAdd(final E element) { ++ final LinkedNode<E> node = new LinkedNode<>(element, null); ++ ++ return !this.forceAppendList(node, node); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public E element() throws NoSuchElementException { ++ final E ret = this.peek(); ++ ++ if (ret == null) { ++ throw new NoSuchElementException(); ++ } ++ ++ return ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ * <p> ++ * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked. ++ * </p> ++ */ ++ @Override ++ public boolean offer(final E element) { ++ Validate.notNull(element, "Null element"); ++ ++ final LinkedNode<E> node = new LinkedNode<>(element, null); ++ ++ return this.appendList(node, node); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public E peek() { ++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ if (this.getHeadOpaque() == head && curr != head) { ++ this.setHeadOpaque(curr); ++ } ++ return element; ++ } ++ ++ if (next == null || curr == next) { ++ return null; ++ } ++ curr = next; ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public E poll() { ++ return this.removeHead(); ++ } ++ ++ /** ++ * Retrieves and removes the head of this queue if it matches the specified predicate. If this queue is empty ++ * or the head does not match the predicate, this function returns {@code null}. ++ * <p> ++ * The predicate may be invoked multiple or no times in this call. ++ * </p> ++ * @param predicate The specified predicate. ++ * @return The head if it matches the predicate, or {@code null} if it did not or this queue is empty. ++ */ ++ public E pollIf(final Predicate<E> predicate) { ++ return this.removeHead(Validate.notNull(predicate, "Null predicate")); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void clear() { ++ //noinspection StatementWithEmptyBody ++ while (this.poll() != null); ++ } ++ ++ /** ++ * Prevents elements from being added to this queue. Once this is called, any attempt to add to this queue will fail. ++ * <p> ++ * This function is MT-Safe. ++ * </p> ++ * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions. ++ */ ++ public boolean preventAdds() { ++ final LinkedNode<E> deadEnd = new LinkedNode<>(null, null); ++ deadEnd.setNextPlain(deadEnd); ++ ++ if (!this.appendList(deadEnd, deadEnd)) { ++ return false; ++ } ++ ++ this.setTailPlain(deadEnd); /* (try to) Ensure tail is set for the following #allowAdds call */ ++ return true; ++ } ++ ++ /** ++ * Allows elements to be added to this queue once again. Note that this function has undefined behaviour if ++ * {@link #preventAdds()} is not called beforehand. The benefit of this function over {@link #tryAllowAdds()} ++ * is that this function might perform better. ++ * <p> ++ * This function is not MT-Safe. ++ * </p> ++ */ ++ public void allowAdds() { ++ LinkedNode<E> tail = this.getTailPlain(); ++ ++ /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */ ++ /* Thus it is possible for an outdated tail to be set */ ++ while (tail != (tail = tail.getNextPlain())) {} ++ ++ tail.setNextVolatile(null); ++ } ++ ++ /** ++ * Tries to allow elements to be added to this queue. Returns {@code true} if the queue was previous add-locked, ++ * {@code false} otherwise. ++ * <p> ++ * This function is MT-Safe, however it should not be used with {@link #allowAdds()}. ++ * </p> ++ * @return {@code true} if the queue was previously add-locked, {@code false} otherwise. ++ */ ++ public boolean tryAllowAdds() { ++ LinkedNode<E> tail = this.getTailPlain(); ++ ++ for (int failures = 0;;) { ++ /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */ ++ /* Thus it is possible for an outdated tail to be set */ ++ while (tail != (tail = tail.getNextAcquire())) { ++ if (tail == null) { ++ return false; ++ } ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (tail == (tail = tail.compareExchangeNextVolatile(tail, null))) { ++ return true; ++ } ++ ++ if (tail == null) { ++ return false; ++ } ++ ++failures; ++ } ++ } ++ ++ /** ++ * Atomically adds the specified element to this queue or allows additions to the queue. If additions ++ * are not allowed, the element is not added. ++ * <p> ++ * This function is MT-Safe. ++ * </p> ++ * @param element The specified element. ++ * @return {@code true} if the queue now allows additions, {@code false} if the element was added. ++ */ ++ public boolean addOrAllowAdds(final E element) { ++ Validate.notNull(element, "Null element"); ++ int failures = 0; ++ ++ final LinkedNode<E> append = new LinkedNode<>(element, null); ++ ++ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) { ++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ ++ /* It is likely due to a cache miss caused by another write to the next field */ ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (next == null) { ++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, append); ++ ++ if (compared == null) { ++ /* Added */ ++ /* Avoid CASing on tail more than we need to */ ++ /* CAS to avoid setting an out-of-date tail */ ++ if (this.getTailOpaque() == currTail) { ++ this.setTailOpaque(append); ++ } ++ return false; // we added ++ } ++ ++ ++failures; ++ curr = compared; ++ continue; ++ } else if (next == curr) { ++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(curr, null); ++ ++ if (compared == curr) { ++ return true; // we let additions through ++ } ++ ++ ++failures; ++ ++ if (compared != null) { ++ curr = compared; ++ } ++ continue; ++ } ++ ++ if (curr == currTail) { ++ /* Tail is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to tail */ ++ if (currTail == (currTail = this.getTailOpaque())) { ++ curr = next; ++ } else { ++ curr = currTail; ++ } ++ } ++ } ++ } ++ ++ /** ++ * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no ++ * head is removed. ++ * <p> ++ * This function is MT-Safe. ++ * </p> ++ * If the queue is already add-blocked and empty then no operation is performed. ++ * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns ++ * an non-null value which was the previous head of queue. ++ */ ++ public E pollOrBlockAdds() { ++ int failures = 0; ++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) { ++ final E currentVal = curr.getElementVolatile(); ++ final LinkedNode<E> next = curr.getNextOpaque(); ++ ++ if (next == curr) { ++ return null; /* Additions are already blocked */ ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (currentVal != null) { ++ if (curr.getAndSetElementVolatile(null) == null) { ++ ++failures; ++ continue; ++ } ++ ++ /* "CAS" to avoid setting an out-of-date head */ ++ if (this.getHeadOpaque() == head) { ++ this.setHeadOpaque(next != null ? next : curr); ++ } ++ ++ return currentVal; ++ } ++ ++ if (next == null) { ++ /* Try to update stale head */ ++ if (curr != head && this.getHeadOpaque() == head) { ++ this.setHeadOpaque(curr); ++ } ++ ++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, curr); ++ ++ if (compared != null) { ++ // failed to block additions ++ curr = compared; ++ ++failures; ++ continue; ++ } ++ ++ return null; /* We blocked additions */ ++ } ++ ++ if (head == curr) { ++ /* head is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to head */ ++ if (head == (head = this.getHeadOpaque())) { ++ curr = next; ++ } else { ++ curr = head; ++ } ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean remove(final Object object) { ++ Validate.notNull(object, "Null object to remove"); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) { ++ return true; ++ } ++ } ++ ++ if (next == curr || next == null) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean removeIf(final Predicate<? super E> filter) { ++ Validate.notNull(filter, "Null filter"); ++ ++ boolean ret = false; ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ ret |= filter.test(element) && curr.getAndSetElementVolatile(null) == element; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean removeAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ boolean ret = false; ++ ++ /* Volatile is required to synchronize with the write to the first element */ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ ret |= collection.contains(element) && curr.getAndSetElementVolatile(null) == element; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean retainAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ boolean ret = false; ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ ret |= !collection.contains(element) && curr.getAndSetElementVolatile(null) == element; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public Object[] toArray() { ++ final List<E> ret = new ArrayList<>(); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ ret.add(element); ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return ret.toArray(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public <T> T[] toArray(final T[] array) { ++ final List<T> ret = new ArrayList<>(); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ //noinspection unchecked ++ ret.add((T)element); ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return ret.toArray(array); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public <T> T[] toArray(final IntFunction<T[]> generator) { ++ Validate.notNull(generator, "Null generator"); ++ ++ final List<T> ret = new ArrayList<>(); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ //noinspection unchecked ++ ret.add((T)element); ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return ret.toArray(generator); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public String toString() { ++ final StringBuilder builder = new StringBuilder(); ++ ++ builder.append("MultiThreadedQueue: {elements: {"); ++ ++ int deadEntries = 0; ++ int totalEntries = 0; ++ int aliveEntries = 0; ++ ++ boolean addLocked = false; ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();; ++totalEntries) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element == null) { ++ ++deadEntries; ++ } else { ++ ++aliveEntries; ++ } ++ ++ if (totalEntries != 0) { ++ builder.append(", "); ++ } ++ ++ builder.append(totalEntries).append(": \"").append(element).append('"'); ++ ++ if (next == null) { ++ break; ++ } ++ if (curr == next) { ++ addLocked = true; ++ break; ++ } ++ curr = next; ++ } ++ ++ builder.append("}, total_entries: \"").append(totalEntries).append("\", alive_entries: \"").append(aliveEntries) ++ .append("\", dead_entries:").append(deadEntries).append("\", add_locked: \"").append(addLocked) ++ .append("\"}"); ++ ++ return builder.toString(); ++ } ++ ++ /** ++ * Adds all elements from the specified collection to this queue. The addition is atomic. ++ * @param collection The specified collection. ++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or ++ * {@code false} if the specified collection contains no elements. ++ */ ++ @Override ++ public boolean addAll(final Collection<? extends E> collection) { ++ return this.addAll((Iterable<? extends E>)collection); ++ } ++ ++ /** ++ * Adds all elements from the specified iterable object to this queue. The addition is atomic. ++ * @param iterable The specified iterable object. ++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or ++ * {@code false} if the specified iterable contains no elements. ++ */ ++ public boolean addAll(final Iterable<? extends E> iterable) { ++ Validate.notNull(iterable, "Null iterable"); ++ ++ final Iterator<? extends E> elements = iterable.iterator(); ++ if (!elements.hasNext()) { ++ return false; ++ } ++ ++ /* Build a list of nodes to append */ ++ /* This is an much faster due to the fact that zero additional synchronization is performed */ ++ ++ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); ++ LinkedNode<E> tail = head; ++ ++ while (elements.hasNext()) { ++ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); ++ tail.setNextPlain(next); ++ tail = next; ++ } ++ ++ return this.appendList(head, tail); ++ } ++ ++ /** ++ * Adds all of the elements from the specified array to this queue. ++ * @param items The specified array. ++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or ++ * {@code false} if the specified array has a length of 0. ++ */ ++ public boolean addAll(final E[] items) { ++ return this.addAll(items, 0, items.length); ++ } ++ ++ /** ++ * Adds all of the elements from the specified array to this queue. ++ * @param items The specified array. ++ * @param off The offset in the array. ++ * @param len The number of items. ++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or ++ * {@code false} if the specified array has a length of 0. ++ */ ++ public boolean addAll(final E[] items, final int off, final int len) { ++ Validate.notNull(items, "Items may not be null"); ++ Validate.arrayBounds(off, len, items.length, "Items array indices out of bounds"); ++ ++ if (len == 0) { ++ return false; ++ } ++ ++ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null); ++ LinkedNode<E> tail = head; ++ ++ for (int i = 1; i < len; ++i) { ++ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(items[off + i], "Null element"), null); ++ tail.setNextPlain(next); ++ tail = next; ++ } ++ ++ return this.appendList(head, tail); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean containsAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ for (final Object element : collection) { ++ if (!this.contains(element)) { ++ return false; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public Iterator<E> iterator() { ++ return new LinkedIterator<>(this.getHeadOpaque()); ++ } ++ ++ /** ++ * {@inheritDoc} ++ * <p> ++ * Note that this function is computed non-atomically and in O(n) time. The value returned may not be representative of ++ * the queue in its current state. ++ * </p> ++ */ ++ @Override ++ public int size() { ++ int size = 0; ++ ++ /* Volatile is required to synchronize with the write to the first element */ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ ++size; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return size; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean isEmpty() { ++ return this.peek() == null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean contains(final Object object) { ++ Validate.notNull(object, "Null object"); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null && (element == object || element.equals(object))) { ++ return true; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Finds the first element in this queue that matches the predicate. ++ * @param predicate The predicate to test elements against. ++ * @return The first element that matched the predicate, {@code null} if none matched. ++ */ ++ public E find(final Predicate<E> predicate) { ++ Validate.notNull(predicate, "Null predicate"); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null && predicate.test(element)) { ++ return element; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ ++ return null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void forEach(final Consumer<? super E> action) { ++ Validate.notNull(action, "Null action"); ++ ++ for (LinkedNode<E> curr = this.getHeadOpaque();;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E element = curr.getElementPlain(); /* Likely in sync */ ++ ++ if (element != null) { ++ action.accept(element); ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ } ++ ++ // return true if normal addition, false if the queue previously disallowed additions ++ protected final boolean forceAppendList(final LinkedNode<E> head, final LinkedNode<E> tail) { ++ int failures = 0; ++ ++ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) { ++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ ++ /* It is likely due to a cache miss caused by another write to the next field */ ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (next == null || next == curr) { ++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(next, head); ++ ++ if (compared == next) { ++ /* Added */ ++ /* Avoid CASing on tail more than we need to */ ++ /* "CAS" to avoid setting an out-of-date tail */ ++ if (this.getTailOpaque() == currTail) { ++ this.setTailOpaque(tail); ++ } ++ return next != curr; ++ } ++ ++ ++failures; ++ curr = compared; ++ continue; ++ } ++ ++ if (curr == currTail) { ++ /* Tail is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to tail */ ++ if (currTail == (currTail = this.getTailOpaque())) { ++ curr = next; ++ } else { ++ curr = currTail; ++ } ++ } ++ } ++ } ++ ++ // return true if successful, false otherwise ++ protected final boolean appendList(final LinkedNode<E> head, final LinkedNode<E> tail) { ++ int failures = 0; ++ ++ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) { ++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ ++ /* It is likely due to a cache miss caused by another write to the next field */ ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ ++ if (next == curr) { ++ /* Additions are stopped */ ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (next == null) { ++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, head); ++ ++ if (compared == null) { ++ /* Added */ ++ /* Avoid CASing on tail more than we need to */ ++ /* CAS to avoid setting an out-of-date tail */ ++ if (this.getTailOpaque() == currTail) { ++ this.setTailOpaque(tail); ++ } ++ return true; ++ } ++ ++ ++failures; ++ curr = compared; ++ continue; ++ } ++ ++ if (curr == currTail) { ++ /* Tail is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to tail */ ++ if (currTail == (currTail = this.getTailOpaque())) { ++ curr = next; ++ } else { ++ curr = currTail; ++ } ++ } ++ } ++ } ++ ++ protected final E removeHead(final Predicate<E> predicate) { ++ int failures = 0; ++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) { ++ // volatile here synchronizes-with writes to element ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E currentVal = curr.getElementPlain(); ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (currentVal != null) { ++ if (!predicate.test(currentVal)) { ++ /* Try to update stale head */ ++ if (curr != head && this.getHeadOpaque() == head) { ++ this.setHeadOpaque(curr); ++ } ++ return null; ++ } ++ if (curr.getAndSetElementVolatile(null) == null) { ++ /* Failed to get head */ ++ if (curr == (curr = next) || next == null) { ++ return null; ++ } ++ ++failures; ++ continue; ++ } ++ ++ /* "CAS" to avoid setting an out-of-date head */ ++ if (this.getHeadOpaque() == head) { ++ this.setHeadOpaque(next != null ? next : curr); ++ } ++ ++ return currentVal; ++ } ++ ++ if (curr == next || next == null) { ++ /* Try to update stale head */ ++ if (curr != head && this.getHeadOpaque() == head) { ++ this.setHeadOpaque(curr); ++ } ++ return null; /* End of queue */ ++ } ++ ++ if (head == curr) { ++ /* head is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to head */ ++ if (head == (head = this.getHeadOpaque())) { ++ curr = next; ++ } else { ++ curr = head; ++ } ++ } ++ } ++ } ++ ++ protected final E removeHead() { ++ int failures = 0; ++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ final E currentVal = curr.getElementPlain(); ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (currentVal != null) { ++ if (curr.getAndSetElementVolatile(null) == null) { ++ /* Failed to get head */ ++ if (curr == (curr = next) || next == null) { ++ return null; ++ } ++ ++failures; ++ continue; ++ } ++ ++ /* "CAS" to avoid setting an out-of-date head */ ++ if (this.getHeadOpaque() == head) { ++ this.setHeadOpaque(next != null ? next : curr); ++ } ++ ++ return currentVal; ++ } ++ ++ if (curr == next || next == null) { ++ /* Try to update stale head */ ++ if (curr != head && this.getHeadOpaque() == head) { ++ this.setHeadOpaque(curr); ++ } ++ return null; /* End of queue */ ++ } ++ ++ if (head == curr) { ++ /* head is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to head */ ++ if (head == (head = this.getHeadOpaque())) { ++ curr = next; ++ } else { ++ curr = head; ++ } ++ } ++ } ++ } ++ ++ /** ++ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should ++ * be faster than a loop on {@link #poll()}. ++ * <p> ++ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()}, ++ * {@link #clear()}, etc). ++ * Write operations are safe to be called concurrently. ++ * </p> ++ * @param consumer The consumer to accept the elements. ++ * @return The total number of elements drained. ++ */ ++ public int drain(final Consumer<E> consumer) { ++ return this.drain(consumer, false, ConcurrentUtil::rethrow); ++ } ++ ++ /** ++ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should ++ * be faster than a loop on {@link #poll()}. ++ * <p> ++ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and ++ * additions to the queue will fail. ++ * </p> ++ * <p> ++ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()}, ++ * {@link #clear()}, etc). ++ * Write operations are safe to be called concurrently. ++ * </p> ++ * @param consumer The consumer to accept the elements. ++ * @param preventAdds Whether to prevent additions to this queue after draining. ++ * @return The total number of elements drained. ++ */ ++ public int drain(final Consumer<E> consumer, final boolean preventAdds) { ++ return this.drain(consumer, preventAdds, ConcurrentUtil::rethrow); ++ } ++ ++ /** ++ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should ++ * be faster than a loop on {@link #poll()}. ++ * <p> ++ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and ++ * additions to the queue will fail. ++ * </p> ++ * <p> ++ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()}, ++ * {@link #clear()}, {@link #remove(Object)} etc). ++ * Only write operations are safe to be called concurrently. ++ * </p> ++ * @param consumer The consumer to accept the elements. ++ * @param preventAdds Whether to prevent additions to this queue after draining. ++ * @param exceptionHandler Invoked when the consumer raises an exception. ++ * @return The total number of elements drained. ++ */ ++ public int drain(final Consumer<E> consumer, final boolean preventAdds, final Consumer<Throwable> exceptionHandler) { ++ Validate.notNull(consumer, "Null consumer"); ++ Validate.notNull(exceptionHandler, "Null exception handler"); ++ ++ /* This function assumes proper synchronization is made to ensure drain and no other read function are called concurrently */ ++ /* This allows plain write usages instead of opaque or higher */ ++ int total = 0; ++ ++ final LinkedNode<E> head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */ ++ LinkedNode<E> curr = head; ++ ++ for (;;) { ++ /* Volatile acquires with the write to the element field */ ++ final E currentVal = curr.getElementPlain(); ++ LinkedNode<E> next = curr.getNextVolatile(); ++ ++ if (next == curr) { ++ /* Add-locked nodes always have a null value */ ++ break; ++ } ++ ++ if (currentVal == null) { ++ if (next == null) { ++ if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) { ++ // failed to prevent adds, continue ++ curr = next; ++ continue; ++ } else { ++ // we're done here ++ break; ++ } ++ } ++ curr = next; ++ continue; ++ } ++ ++ try { ++ consumer.accept(currentVal); ++ } catch (final Exception ex) { ++ this.setHeadOpaque(next != null ? next : curr); /* Avoid perf penalty (of reiterating) if the exception handler decides to re-throw */ ++ curr.setElementOpaque(null); /* set here, we might re-throw */ ++ ++ exceptionHandler.accept(ex); ++ } ++ ++ curr.setElementOpaque(null); ++ ++ ++total; ++ ++ if (next == null) { ++ if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) { ++ /* Retry with next value */ ++ curr = next; ++ continue; ++ } ++ break; ++ } ++ ++ curr = next; ++ } ++ if (curr != head) { ++ this.setHeadOpaque(curr); /* While this may be a plain write, eventually publish it for methods such as find. */ ++ } ++ return total; ++ } ++ ++ @Override ++ public Spliterator<E> spliterator() { // TODO implement ++ return Spliterators.spliterator(this, Spliterator.CONCURRENT | ++ Spliterator.NONNULL | Spliterator.ORDERED); ++ } ++ ++ protected static final class LinkedNode<E> { ++ ++ protected volatile Object element; ++ protected volatile LinkedNode<E> next; ++ ++ protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class); ++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class); ++ ++ protected LinkedNode(final Object element, final LinkedNode<E> next) { ++ ELEMENT_HANDLE.set(this, element); ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ /* element */ ++ ++ @SuppressWarnings("unchecked") ++ protected final E getElementPlain() { ++ return (E)ELEMENT_HANDLE.get(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final E getElementVolatile() { ++ return (E)ELEMENT_HANDLE.getVolatile(this); ++ } ++ ++ protected final void setElementPlain(final E update) { ++ ELEMENT_HANDLE.set(this, (Object)update); ++ } ++ ++ protected final void setElementOpaque(final E update) { ++ ELEMENT_HANDLE.setOpaque(this, (Object)update); ++ } ++ ++ protected final void setElementVolatile(final E update) { ++ ELEMENT_HANDLE.setVolatile(this, (Object)update); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final E getAndSetElementVolatile(final E update) { ++ return (E)ELEMENT_HANDLE.getAndSet(this, update); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final E compareExchangeElementVolatile(final E expect, final E update) { ++ return (E)ELEMENT_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ /* next */ ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getNextPlain() { ++ return (LinkedNode<E>)NEXT_HANDLE.get(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getNextOpaque() { ++ return (LinkedNode<E>)NEXT_HANDLE.getOpaque(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getNextAcquire() { ++ return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getNextVolatile() { ++ return (LinkedNode<E>)NEXT_HANDLE.getVolatile(this); ++ } ++ ++ protected final void setNextPlain(final LinkedNode<E> next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ protected final void setNextVolatile(final LinkedNode<E> next) { ++ NEXT_HANDLE.setVolatile(this, next); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> compareExchangeNextVolatile(final LinkedNode<E> expect, final LinkedNode<E> set) { ++ return (LinkedNode<E>)NEXT_HANDLE.compareAndExchange(this, expect, set); ++ } ++ } ++ ++ protected static final class LinkedIterator<E> implements Iterator<E> { ++ ++ protected LinkedNode<E> curr; /* last returned by next() */ ++ protected LinkedNode<E> next; /* next to return from next() */ ++ protected E nextElement; /* cached to avoid a race condition with removing or polling */ ++ ++ protected LinkedIterator(final LinkedNode<E> start) { ++ /* setup nextElement and next */ ++ for (LinkedNode<E> curr = start;;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ ++ final E element = curr.getElementPlain(); ++ ++ if (element != null) { ++ this.nextElement = element; ++ this.next = curr; ++ break; ++ } ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ curr = next; ++ } ++ } ++ ++ protected final void findNext() { ++ /* only called if this.nextElement != null, which means this.next != null */ ++ for (LinkedNode<E> curr = this.next;;) { ++ final LinkedNode<E> next = curr.getNextVolatile(); ++ ++ if (next == null || next == curr) { ++ break; ++ } ++ ++ final E element = next.getElementPlain(); ++ ++ if (element != null) { ++ this.nextElement = element; ++ this.curr = this.next; /* this.next will be the value returned from next(), set this.curr for remove() */ ++ this.next = next; ++ return; ++ } ++ curr = next; ++ } ++ ++ /* out of nodes to iterate */ ++ /* keep curr for remove() calls */ ++ this.next = null; ++ this.nextElement = null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean hasNext() { ++ return this.nextElement != null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public E next() { ++ final E element = this.nextElement; ++ ++ if (element == null) { ++ throw new NoSuchElementException(); ++ } ++ ++ this.findNext(); ++ ++ return element; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void remove() { ++ if (this.curr == null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.curr.setElementVolatile(null); ++ this.curr = null; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..597659f38aa816646dcda4ca39c002b6d9f9a792 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java +@@ -0,0 +1,148 @@ ++package ca.spottedleaf.concurrentutil.collection; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import java.lang.invoke.VarHandle; ++import java.util.ConcurrentModificationException; ++ ++/** ++ * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics, ++ * and the writer side of the queue is ordered by release semantics. ++ */ ++// TODO test ++public class SRSWLinkedQueue<E> { ++ ++ // always non-null ++ protected LinkedNode<E> head; ++ ++ // always non-null ++ protected LinkedNode<E> tail; ++ ++ /* IMPL NOTE: Leave hashCode and equals to their defaults */ ++ ++ public SRSWLinkedQueue() { ++ final LinkedNode<E> dummy = new LinkedNode<>(null, null); ++ this.head = this.tail = dummy; ++ } ++ ++ /** ++ * Must be the reader thread. ++ * ++ * <p> ++ * Returns, without removing, the first element of this queue. ++ * </p> ++ * @return Returns, without removing, the first element of this queue. ++ */ ++ public E peekFirst() { ++ LinkedNode<E> head = this.head; ++ E ret = head.getElementPlain(); ++ if (ret == null) { ++ head = head.getNextAcquire(); ++ if (head == null) { ++ // empty ++ return null; ++ } ++ // update head reference for next poll() call ++ this.head = head; ++ // guaranteed to be non-null ++ ret = head.getElementPlain(); ++ if (ret == null) { ++ throw new ConcurrentModificationException("Multiple reader threads"); ++ } ++ } ++ ++ return ret; ++ } ++ ++ /** ++ * Must be the reader thread. ++ * ++ * <p> ++ * Returns and removes the first element of this queue. ++ * </p> ++ * @return Returns and removes the first element of this queue. ++ */ ++ public E poll() { ++ LinkedNode<E> head = this.head; ++ E ret = head.getElementPlain(); ++ if (ret == null) { ++ head = head.getNextAcquire(); ++ if (head == null) { ++ // empty ++ return null; ++ } ++ // guaranteed to be non-null ++ ret = head.getElementPlain(); ++ if (ret == null) { ++ throw new ConcurrentModificationException("Multiple reader threads"); ++ } ++ } ++ ++ head.setElementPlain(null); ++ LinkedNode<E> next = head.getNextAcquire(); ++ this.head = next == null ? head : next; ++ ++ return ret; ++ } ++ ++ /** ++ * Must be the writer thread. ++ * ++ * <p> ++ * Adds the element to the end of the queue. ++ * </p> ++ * ++ * @throws NullPointerException If the provided element is null ++ */ ++ public void addLast(final E element) { ++ Validate.notNull(element, "Provided element cannot be null"); ++ final LinkedNode<E> append = new LinkedNode<>(element, null); ++ ++ this.tail.setNextRelease(append); ++ this.tail = append; ++ } ++ ++ protected static final class LinkedNode<E> { ++ ++ protected volatile Object element; ++ protected volatile LinkedNode<E> next; ++ ++ protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class); ++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class); ++ ++ protected LinkedNode(final Object element, final LinkedNode<E> next) { ++ ELEMENT_HANDLE.set(this, element); ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ /* element */ ++ ++ @SuppressWarnings("unchecked") ++ protected final E getElementPlain() { ++ return (E)ELEMENT_HANDLE.get(this); ++ } ++ ++ protected final void setElementPlain(final E update) { ++ ELEMENT_HANDLE.set(this, (Object)update); ++ } ++ /* next */ ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getNextPlain() { ++ return (LinkedNode<E>)NEXT_HANDLE.get(this); ++ } ++ ++ @SuppressWarnings("unchecked") ++ protected final LinkedNode<E> getNextAcquire() { ++ return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setNextPlain(final LinkedNode<E> next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ protected final void setNextRelease(final LinkedNode<E> next) { ++ NEXT_HANDLE.setRelease(this, next); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a1ad3308f9c3545a604b635896259a1cd3382b2a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java +@@ -0,0 +1,98 @@ ++package ca.spottedleaf.concurrentutil.completable; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++import java.util.function.BiConsumer; ++ ++public final class Completable<T> { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>(); ++ private T result; ++ private Throwable throwable; ++ private volatile boolean completed; ++ ++ public boolean isCompleted() { ++ return this.completed; ++ } ++ ++ /** ++ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero ++ * synchronisation ++ */ ++ public T getResult() { ++ return this.result; ++ } ++ ++ /** ++ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero ++ * synchronisation ++ */ ++ public Throwable getThrowable() { ++ return this.throwable; ++ } ++ ++ public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) { ++ if (this.waiters.add(consumer)) { ++ return new CancellableImpl(consumer); ++ } ++ return null; ++ } ++ ++ private void completeAllWaiters(final T result, final Throwable throwable) { ++ this.completed = true; ++ BiConsumer<T, Throwable> waiter; ++ while ((waiter = this.waiters.pollOrBlockAdds()) != null) { ++ this.completeWaiter(waiter, result, throwable); ++ } ++ } ++ ++ private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) { ++ try { ++ consumer.accept(result, throwable); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable throwable2) { ++ LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2); ++ } ++ } ++ ++ public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) { ++ if (this.waiters.add(consumer)) { ++ return new CancellableImpl(consumer); ++ } ++ this.completeWaiter(consumer, this.result, this.throwable); ++ return new CancellableImpl(consumer); ++ } ++ ++ public void complete(final T result) { ++ this.result = result; ++ this.completeAllWaiters(result, null); ++ } ++ ++ public void completeWithThrowable(final Throwable throwable) { ++ if (throwable == null) { ++ throw new NullPointerException("Throwable cannot be null"); ++ } ++ this.throwable = throwable; ++ this.completeAllWaiters(null, throwable); ++ } ++ ++ private final class CancellableImpl implements Cancellable { ++ ++ private final BiConsumer<T, Throwable> waiter; ++ ++ private CancellableImpl(final BiConsumer<T, Throwable> waiter) { ++ this.waiter = waiter; ++ } ++ ++ @Override ++ public boolean cancel() { ++ return Completable.this.waiters.remove(this.waiter); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8c452b0988da4725762d543f6bee09915c328ae6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java +@@ -0,0 +1,198 @@ ++package ca.spottedleaf.concurrentutil.executor; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.util.function.BooleanSupplier; ++ ++public interface BaseExecutor { ++ ++ /** ++ * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued, ++ * returns {@code true}. ++ * ++ * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise. ++ */ ++ public default boolean haveAllTasksExecuted() { ++ // order is important ++ // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher - ++ // so our check fails, and we try again ++ final long completed = this.getTotalTasksExecuted(); ++ final long scheduled = this.getTotalTasksScheduled(); ++ ++ return completed == scheduled; ++ } ++ ++ /** ++ * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled. ++ */ ++ public long getTotalTasksScheduled(); ++ ++ /** ++ * Returns the number of tasks that have fully been executed. ++ */ ++ public long getTotalTasksExecuted(); ++ ++ ++ /** ++ * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()} ++ * <p> ++ * This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can ++ * be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using ++ * waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty. ++ * </p> ++ * <p> ++ * This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more ++ * time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled. ++ * </p> ++ * <p> ++ * Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this cal. ++ * </p> ++ * ++ * @throws IllegalStateException If the current thread is not allowed to wait ++ */ ++ public default void waitUntilAllExecuted() throws IllegalStateException { ++ long failures = 1L; // start at 0.25ms ++ ++ while (!this.haveAllTasksExecuted()) { ++ Thread.yield(); ++ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms ++ } ++ } ++ ++ /** ++ * Executes the next available task. ++ * <p> ++ * If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed. ++ * </p> ++ * <p> ++ * If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed ++ * when there are no other tasks available with a higher priority. ++ * </p> ++ * <p> ++ * If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then ++ * this function will be biased to execute tasks that have higher priorities. ++ * </p> ++ * ++ * @return {@code true} if a task was executed, {@code false} otherwise ++ * @throws IllegalStateException If the current thread is not allowed to execute a task ++ */ ++ public boolean executeTask() throws IllegalStateException; ++ ++ /** ++ * Executes all queued tasks. ++ * ++ * @return {@code true} if a task was executed, {@code false} otherwise ++ * @throws IllegalStateException If the current thread is not allowed to execute a task ++ */ ++ public default boolean executeAll() { ++ if (!this.executeTask()) { ++ return false; ++ } ++ ++ while (this.executeTask()); ++ ++ return true; ++ } ++ ++ /** ++ * Waits and executes tasks until the condition returns {@code true}. ++ * <p> ++ * WARNING: This function is <i>not</i> suitable for waiting until a deadline! ++ * Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead. ++ * </p> ++ */ ++ public default void executeConditionally(final BooleanSupplier condition) { ++ long failures = 0; ++ while (!condition.getAsBoolean()) { ++ if (this.executeTask()) { ++ failures = failures >>> 2; ++ } else { ++ failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms ++ } ++ } ++ } ++ ++ /** ++ * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() >= deadline}. ++ */ ++ public default void executeConditionally(final BooleanSupplier condition, final long deadline) { ++ long failures = 0; ++ // double check deadline; we don't know how expensive the condition is ++ while ((System.nanoTime() < deadline) && !condition.getAsBoolean() && (System.nanoTime() < deadline)) { ++ if (this.executeTask()) { ++ failures = failures >>> 2; ++ } else { ++ failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms ++ } ++ } ++ } ++ ++ /** ++ * Waits and executes tasks until {@code System.nanoTime() >= deadline}. ++ */ ++ public default void executeUntil(final long deadline) { ++ long failures = 0; ++ while (System.nanoTime() < deadline) { ++ if (this.executeTask()) { ++ failures = failures >>> 2; ++ } else { ++ failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms ++ } ++ } ++ } ++ ++ /** ++ * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will ++ * result in {@link IllegalStateException} being thrown. ++ * <p> ++ * This operation is atomic with respect to other shutdown calls ++ * </p> ++ * <p> ++ * After this call has completed, regardless of return value, this queue will be shutdown. ++ * </p> ++ * ++ * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already ++ * @throws UnsupportedOperationException If this queue does not support shutdown ++ */ ++ public default boolean shutdown() throws UnsupportedOperationException { ++ throw new UnsupportedOperationException(); ++ } ++ ++ /** ++ * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method ++ * does not indicate whether all of the tasks scheduled have been executed. ++ * @return Returns whether this queue has shut down. ++ */ ++ public default boolean isShutdown() { ++ return false; ++ } ++ ++ public static interface BaseTask extends Cancellable { ++ ++ /** ++ * Causes a lazily queued task to become queued or executed ++ * ++ * @throws IllegalStateException If the backing queue has shutdown ++ * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed ++ */ ++ public boolean queue(); ++ ++ /** ++ * Forces this task to be marked as completed. ++ * ++ * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed. ++ */ ++ @Override ++ public boolean cancel(); ++ ++ /** ++ * Executes this task. This will also mark the task as completing. ++ * <p> ++ * Exceptions thrown from the runnable will be rethrown. ++ * </p> ++ * ++ * @return {@code true} if this task was executed, {@code false} if it was already marked as completed. ++ */ ++ public boolean execute(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..11449056361bb6c5a055f543cdd135c4113757c6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java +@@ -0,0 +1,14 @@ ++package ca.spottedleaf.concurrentutil.executor; ++ ++/** ++ * Interface specifying that something can be cancelled. ++ */ ++public interface Cancellable { ++ ++ /** ++ * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function ++ * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only ++ * when this function successfully stops this task from being completed will it return {@code true}. ++ */ ++ public boolean cancel(); ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3ce10053d4ec51855ad7012abb5d97df1c0e557a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java +@@ -0,0 +1,170 @@ ++package ca.spottedleaf.concurrentutil.executor.standard; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++ ++public class DelayedPrioritisedTask { ++ ++ protected volatile int priority; ++ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class); ++ ++ protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0; ++ ++ protected final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this); ++ } ++ ++ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update); ++ } ++ ++ protected final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val); ++ } ++ ++ protected final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val); ++ } ++ ++ protected volatile PrioritisedExecutor.PrioritisedTask task; ++ protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class); ++ ++ protected PrioritisedExecutor.PrioritisedTask getTaskPlain() { ++ return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this); ++ } ++ ++ protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() { ++ return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this); ++ } ++ ++ protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) { ++ return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update); ++ } ++ ++ public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) { ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ // only public for debugging ++ public int getPriorityInternal() { ++ return this.getPriorityVolatile(); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask getTask() { ++ return this.getTaskVolatile(); ++ } ++ ++ public void setTask(final PrioritisedExecutor.PrioritisedTask task) { ++ int priority = this.getPriorityVolatile(); ++ ++ if (this.compareAndExchangeTaskVolatile(null, task) != null) { ++ throw new IllegalStateException("setTask() called twice"); ++ } ++ ++ int failures = 0; ++ for (;;) { ++ task.setPriority(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) { ++ return; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ final int priority = this.getPriorityVolatile(); ++ if ((priority & PRIORITY_SET) != 0) { ++ return this.task.getPriority(); ++ } ++ ++ return PrioritisedExecutor.Priority.getPriority(priority); ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_SET) != 0) { ++ this.getTaskPlain().raisePriority(priority); ++ return; ++ } ++ ++ if (!priority.isLowerPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_SET) != 0) { ++ this.getTaskPlain().setPriority(priority); ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_SET) != 0) { ++ this.getTaskPlain().lowerPriority(priority); ++ return; ++ } ++ ++ if (!priority.isHigherPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e5d8ff730ba9d83efc2d80782de313a718bf55b3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java +@@ -0,0 +1,246 @@ ++package ca.spottedleaf.concurrentutil.executor.standard; ++ ++import ca.spottedleaf.concurrentutil.executor.BaseExecutor; ++ ++public interface PrioritisedExecutor extends BaseExecutor { ++ ++ public static enum Priority { ++ ++ /** ++ * Priority value indicating the task has completed or is being completed. ++ * This priority cannot be used to schedule tasks. ++ */ ++ COMPLETING(-1), ++ ++ /** ++ * Absolute highest priority, should only be used for when a task is blocking a time-critical thread. ++ */ ++ BLOCKING(), ++ ++ /** ++ * Should only be used for urgent but not time-critical tasks. ++ */ ++ HIGHEST(), ++ ++ /** ++ * Two priorities above normal. ++ */ ++ HIGHER(), ++ ++ /** ++ * One priority above normal. ++ */ ++ HIGH(), ++ ++ /** ++ * Default priority. ++ */ ++ NORMAL(), ++ ++ /** ++ * One priority below normal. ++ */ ++ LOW(), ++ ++ /** ++ * Two priorities below normal. ++ */ ++ LOWER(), ++ ++ /** ++ * Use for tasks that should eventually execute, but are not needed to. ++ */ ++ LOWEST(), ++ ++ /** ++ * Use for tasks that can be delayed indefinitely. ++ */ ++ IDLE(); ++ ++ // returns whether the priority can be scheduled ++ public static boolean isValidPriority(final Priority priority) { ++ return priority != null && priority != Priority.COMPLETING; ++ } ++ ++ // returns the higher priority of the two ++ public static PrioritisedExecutor.Priority max(final Priority p1, final Priority p2) { ++ return p1.isHigherOrEqualPriority(p2) ? p1 : p2; ++ } ++ ++ // returns the lower priroity of the two ++ public static PrioritisedExecutor.Priority min(final Priority p1, final Priority p2) { ++ return p1.isLowerOrEqualPriority(p2) ? p1 : p2; ++ } ++ ++ public boolean isHigherOrEqualPriority(final Priority than) { ++ return this.priority <= than.priority; ++ } ++ ++ public boolean isHigherPriority(final Priority than) { ++ return this.priority < than.priority; ++ } ++ ++ public boolean isLowerOrEqualPriority(final Priority than) { ++ return this.priority >= than.priority; ++ } ++ ++ public boolean isLowerPriority(final Priority than) { ++ return this.priority > than.priority; ++ } ++ ++ public boolean isHigherOrEqualPriority(final int than) { ++ return this.priority <= than; ++ } ++ ++ public boolean isHigherPriority(final int than) { ++ return this.priority < than; ++ } ++ ++ public boolean isLowerOrEqualPriority(final int than) { ++ return this.priority >= than; ++ } ++ ++ public boolean isLowerPriority(final int than) { ++ return this.priority > than; ++ } ++ ++ public static boolean isHigherOrEqualPriority(final int priority, final int than) { ++ return priority <= than; ++ } ++ ++ public static boolean isHigherPriority(final int priority, final int than) { ++ return priority < than; ++ } ++ ++ public static boolean isLowerOrEqualPriority(final int priority, final int than) { ++ return priority >= than; ++ } ++ ++ public static boolean isLowerPriority(final int priority, final int than) { ++ return priority > than; ++ } ++ ++ static final PrioritisedExecutor.Priority[] PRIORITIES = PrioritisedExecutor.Priority.values(); ++ ++ /** includes special priorities */ ++ public static final int TOTAL_PRIORITIES = PRIORITIES.length; ++ ++ public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1; ++ ++ public static PrioritisedExecutor.Priority getPriority(final int priority) { ++ return PRIORITIES[priority + 1]; ++ } ++ ++ private static int priorityCounter; ++ ++ private static int nextCounter() { ++ return priorityCounter++; ++ } ++ ++ public final int priority; ++ ++ Priority() { ++ this(nextCounter()); ++ } ++ ++ Priority(final int priority) { ++ this.priority = priority; ++ } ++ } ++ ++ /** ++ * Queues or executes a task at {@link Priority#NORMAL} priority. ++ * @param task The task to run. ++ * ++ * @throws IllegalStateException If this queue has shutdown. ++ * @throws NullPointerException If the task is null ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public default PrioritisedTask queueRunnable(final Runnable task) { ++ return this.queueRunnable(task, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Queues or executes a task. ++ * ++ * @param task The task to run. ++ * @param priority The priority for the task. ++ * ++ * @throws IllegalStateException If this queue has shutdown. ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority); ++ ++ /** ++ * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}. ++ * ++ * @param task The task to run. ++ * ++ * @throws IllegalStateException If this queue has shutdown. ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks ++ * @return The prioritised task associated with the parameters ++ */ ++ public default PrioritisedExecutor.PrioritisedTask createTask(final Runnable task) { ++ return this.createTask(task, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}. ++ * ++ * @param task The task to run. ++ * @param priority The priority for the task. ++ * ++ * @throws IllegalStateException If this queue has shutdown. ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks ++ * @return The prioritised task associated with the parameters ++ */ ++ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final PrioritisedExecutor.Priority priority); ++ ++ public static interface PrioritisedTask extends BaseTask { ++ ++ /** ++ * Returns the current priority. Note that {@link PrioritisedExecutor.Priority#COMPLETING} will be returned ++ * if this task is completing or has completed. ++ */ ++ public PrioritisedExecutor.Priority getPriority(); ++ ++ /** ++ * Attempts to set this task's priority level to the level specified. ++ * ++ * @param priority Specified priority level. ++ * ++ * @throws IllegalArgumentException If the priority is invalid ++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue ++ * this task was scheduled on was shutdown, or if the priority was already at the specified level. ++ */ ++ public boolean setPriority(final PrioritisedExecutor.Priority priority); ++ ++ /** ++ * Attempts to raise the priority to the priority level specified. ++ * ++ * @param priority Priority specified ++ * ++ * @throws IllegalArgumentException If the priority is invalid ++ * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher. ++ */ ++ public boolean raisePriority(final PrioritisedExecutor.Priority priority); ++ ++ /** ++ * Attempts to lower the priority to the priority level specified. ++ * ++ * @param priority Priority specified ++ * ++ * @throws IllegalArgumentException If the priority is invalid ++ * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower. ++ */ ++ public boolean lowerPriority(final PrioritisedExecutor.Priority priority); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..91fe0f7049122f62f05ba09c24cba5d758340cff +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java +@@ -0,0 +1,297 @@ ++package ca.spottedleaf.concurrentutil.executor.standard; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.locks.LockSupport; ++ ++/** ++ * Thread which will continuously drain from a specified queue. ++ * <p> ++ * Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread ++ * to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour ++ * of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor} ++ * methods. ++ * </p> ++ */ ++public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ protected final PrioritisedExecutor queue; ++ ++ protected volatile boolean threadShutdown; ++ ++ protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class); ++ protected volatile boolean threadParked; ++ ++ protected volatile boolean halted; ++ ++ protected final long spinWaitTime; ++ ++ static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms ++ ++ public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) { ++ this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms ++ } ++ ++ public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns ++ this.queue = queue; ++ this.spinWaitTime = spinWaitTime; ++ } ++ ++ @Override ++ public void run() { ++ final long spinWaitTime = this.spinWaitTime; ++ ++ main_loop: ++ for (;;) { ++ this.pollTasks(); ++ ++ // spinwait ++ ++ final long start = System.nanoTime(); ++ ++ for (;;) { ++ // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event. ++ Thread.interrupted(); ++ Thread.yield(); ++ LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us ++ ++ if (this.pollTasks()) { ++ // restart loop, found tasks ++ continue main_loop; ++ } ++ ++ if (this.handleClose()) { ++ return; // we're done ++ } ++ ++ if ((System.nanoTime() - start) >= spinWaitTime) { ++ break; ++ } ++ } ++ ++ if (this.handleClose()) { ++ return; ++ } ++ ++ this.setThreadParkedVolatile(true); ++ ++ // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true ++ // (i.e it will not notify us) ++ if (this.pollTasks()) { ++ this.setThreadParkedVolatile(false); ++ continue; ++ } ++ ++ if (this.handleClose()) { ++ return; ++ } ++ ++ // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop ++ // LockSupport.park() can fail for any reason ++ while (this.getThreadParkedVolatile()) { ++ Thread.interrupted(); ++ LockSupport.park("Waiting on tasks"); ++ } ++ } ++ } ++ ++ protected boolean pollTasks() { ++ boolean ret = false; ++ ++ for (;;) { ++ if (this.halted) { ++ break; ++ } ++ try { ++ if (!this.queue.executeTask()) { ++ break; ++ } ++ ret = true; ++ } catch (final ThreadDeath death) { ++ throw death; // goodbye world... ++ } catch (final Throwable throwable) { ++ LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable); ++ } ++ } ++ ++ return ret; ++ } ++ ++ protected boolean handleClose() { ++ if (this.threadShutdown) { ++ this.pollTasks(); // this ensures we've emptied the queue ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Notify this thread that a task has been added to its queue ++ * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks ++ */ ++ public boolean notifyTasks() { ++ if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) { ++ LockSupport.unpark(this); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask queueTask = this.queue.createTask(task, priority); ++ ++ // need to override queue() to notify us of tasks ++ return new PrioritisedTask() { ++ @Override ++ public Priority getPriority() { ++ return queueTask.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ return queueTask.setPriority(priority); ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ return queueTask.raisePriority(priority); ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ return queueTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public boolean queue() { ++ final boolean ret = queueTask.queue(); ++ if (ret) { ++ PrioritisedQueueExecutorThread.this.notifyTasks(); ++ } ++ return ret; ++ } ++ ++ @Override ++ public boolean cancel() { ++ return queueTask.cancel(); ++ } ++ ++ @Override ++ public boolean execute() { ++ return queueTask.execute(); ++ } ++ }; ++ } ++ ++ @Override ++ public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.queue.queueRunnable(task, priority); ++ ++ this.notifyTasks(); ++ ++ return ret; ++ } ++ ++ @Override ++ public boolean haveAllTasksExecuted() { ++ return this.queue.haveAllTasksExecuted(); ++ } ++ ++ @Override ++ public long getTotalTasksExecuted() { ++ return this.queue.getTotalTasksExecuted(); ++ } ++ ++ @Override ++ public long getTotalTasksScheduled() { ++ return this.queue.getTotalTasksScheduled(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception. ++ */ ++ @Override ++ public void waitUntilAllExecuted() throws IllegalStateException { ++ if (Thread.currentThread() == this) { ++ throw new IllegalStateException("Cannot block on our own queue"); ++ } ++ this.queue.waitUntilAllExecuted(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ * @throws IllegalStateException Always ++ */ ++ @Override ++ public boolean executeTask() throws IllegalStateException { ++ throw new IllegalStateException(); ++ } ++ ++ /** ++ * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true. ++ * <p> ++ * This function is MT-Safe. ++ * </p> ++ * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue. ++ * @param killQueue Whether to shutdown this thread's queue ++ * @return whether this thread shut down the queue ++ * @see #halt(boolean) ++ */ ++ public boolean close(final boolean wait, final boolean killQueue) { ++ final boolean ret = killQueue && this.queue.shutdown(); ++ this.threadShutdown = true; ++ ++ // force thread to respond to the shutdown ++ this.setThreadParkedVolatile(false); ++ LockSupport.unpark(this); ++ ++ if (wait) { ++ this.waitUntilAllExecuted(); ++ } ++ ++ return ret; ++ } ++ ++ ++ /** ++ * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}. ++ * <p> ++ * This is not safe to call with {@link #close(boolean, boolean)} if <code>wait = true</code>, in which case ++ * the waiting thread may block indefinitely. ++ * </p> ++ * <p> ++ * This function is MT-Safe. ++ * </p> ++ * @param killQueue Whether to shutdown this thread's queue ++ * @see #close(boolean, boolean) ++ */ ++ public void halt(final boolean killQueue) { ++ if (killQueue) { ++ this.queue.shutdown(); ++ } ++ this.threadShutdown = true; ++ this.halted = true; ++ ++ // force thread to respond to the shutdown ++ this.setThreadParkedVolatile(false); ++ LockSupport.unpark(this); ++ } ++ ++ protected final boolean getThreadParkedVolatile() { ++ return (boolean)THREAD_PARKED_HANDLE.getVolatile(this); ++ } ++ ++ protected final boolean exchangeThreadParkedVolatile(final boolean value) { ++ return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value); ++ } ++ ++ protected final void setThreadParkedVolatile(final boolean value) { ++ THREAD_PARKED_HANDLE.setVolatile(this, value); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..26fa2caa18a9194e57574a4a7fa9f7a4265740e0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java +@@ -0,0 +1,579 @@ ++package ca.spottedleaf.concurrentutil.executor.standard; ++ ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Comparator; ++import java.util.TreeSet; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.BiConsumer; ++ ++public final class PrioritisedThreadPool { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ protected final PrioritisedThread[] threads; ++ protected final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator()); ++ protected final String name; ++ protected final long queueMaxHoldTime; ++ ++ protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>(); ++ protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>(); ++ ++ protected boolean shutdown; ++ ++ protected long schedulingIdGenerator; ++ ++ protected static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6); ++ ++ public PrioritisedThreadPool(final String name, final int threads) { ++ this(name, threads, null); ++ } ++ ++ public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) { ++ this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms ++ } ++ ++ public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier, ++ final long queueHoldTime) { // in ns ++ if (threads <= 0) { ++ throw new IllegalArgumentException("Thread count must be > 0, not " + threads); ++ } ++ if (name == null) { ++ throw new IllegalArgumentException("Name cannot be null"); ++ } ++ this.name = name; ++ this.queueMaxHoldTime = queueHoldTime; ++ ++ this.threads = new PrioritisedThread[threads]; ++ for (int i = 0; i < threads; ++i) { ++ this.threads[i] = new PrioritisedThread(this); ++ ++ // set default attributes ++ this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i); ++ this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> { ++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); ++ }); ++ ++ // let thread modifier override defaults ++ if (threadModifier != null) { ++ threadModifier.accept(this.threads[i], Integer.valueOf(i)); ++ } ++ ++ // now the thread can start ++ this.threads[i].start(); ++ } ++ } ++ ++ public Thread[] getThreads() { ++ return Arrays.copyOf(this.threads, this.threads.length, Thread[].class); ++ } ++ ++ public PrioritisedPoolExecutor createExecutor(final String name, final int parallelism) { ++ synchronized (this.nonShutdownQueues) { ++ if (this.shutdown) { ++ throw new IllegalStateException("Queue is shutdown: " + this.toString()); ++ } ++ final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl(this, name, Math.min(Math.max(1, parallelism), this.threads.length)); ++ ++ this.nonShutdownQueues.add(ret); ++ ++ synchronized (this.activeQueues) { ++ this.activeQueues.add(ret); ++ } ++ ++ return ret; ++ } ++ } ++ ++ /** ++ * Prevents creation of new queues, shutdowns all non-shutdown queues if specified ++ */ ++ public void halt(final boolean shutdownQueues) { ++ synchronized (this.nonShutdownQueues) { ++ this.shutdown = true; ++ } ++ if (shutdownQueues) { ++ final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown; ++ synchronized (this.nonShutdownQueues) { ++ this.shutdown = true; ++ queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); ++ } ++ ++ for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { ++ queue.shutdown(); ++ } ++ } ++ ++ ++ for (final PrioritisedThread thread : this.threads) { ++ // can't kill queue, queue is null ++ thread.halt(false); ++ } ++ } ++ ++ /** ++ * Waits until all threads in this pool have shutdown, or until the specified time has passed. ++ * @param msToWait Maximum time to wait. ++ * @return {@code false} if the maximum time passed, {@code true} otherwise. ++ */ ++ public boolean join(final long msToWait) { ++ try { ++ return this.join(msToWait, false); ++ } catch (final InterruptedException ex) { ++ throw new IllegalStateException(ex); ++ } ++ } ++ ++ /** ++ * Waits until all threads in this pool have shutdown, or until the specified time has passed. ++ * @param msToWait Maximum time to wait. ++ * @return {@code false} if the maximum time passed, {@code true} otherwise. ++ * @throws InterruptedException If this thread is interrupted. ++ */ ++ public boolean joinInterruptable(final long msToWait) throws InterruptedException { ++ return this.join(msToWait, true); ++ } ++ ++ protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException { ++ final long nsToWait = msToWait * (1000 * 1000); ++ final long start = System.nanoTime(); ++ final long deadline = start + nsToWait; ++ boolean interrupted = false; ++ try { ++ for (final PrioritisedThread thread : this.threads) { ++ for (;;) { ++ if (!thread.isAlive()) { ++ break; ++ } ++ final long current = System.nanoTime(); ++ if (current >= deadline) { ++ return false; ++ } ++ ++ try { ++ thread.join(Math.max(1L, (deadline - current) / (1000 * 1000))); ++ } catch (final InterruptedException ex) { ++ if (interruptable) { ++ throw ex; ++ } ++ interrupted = true; ++ } ++ } ++ } ++ ++ return true; ++ } finally { ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ } ++ } ++ ++ public void shutdown(final boolean wait) { ++ final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown; ++ synchronized (this.nonShutdownQueues) { ++ this.shutdown = true; ++ queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); ++ } ++ ++ for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { ++ queue.shutdown(); ++ } ++ ++ for (final PrioritisedThread thread : this.threads) { ++ // none of these can be true or else NPE ++ thread.close(false, false); ++ } ++ ++ if (wait) { ++ final ArrayList<PrioritisedPoolExecutorImpl> queues; ++ synchronized (this.activeQueues) { ++ queues = new ArrayList<>(this.activeQueues); ++ } ++ for (final PrioritisedPoolExecutorImpl queue : queues) { ++ queue.waitUntilAllExecuted(); ++ } ++ } ++ } ++ ++ protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread { ++ ++ protected final PrioritisedThreadPool pool; ++ protected final AtomicBoolean alertedHighPriority = new AtomicBoolean(); ++ ++ public PrioritisedThread(final PrioritisedThreadPool pool) { ++ super(null); ++ this.pool = pool; ++ } ++ ++ public boolean alertHighPriorityExecutor() { ++ if (!this.notifyTasks()) { ++ if (!this.alertedHighPriority.get()) { ++ this.alertedHighPriority.set(true); ++ } ++ return false; ++ } ++ ++ return true; ++ } ++ ++ private boolean isAlertedHighPriority() { ++ return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false); ++ } ++ ++ @Override ++ protected boolean pollTasks() { ++ final PrioritisedThreadPool pool = this.pool; ++ final TreeSet<PrioritisedPoolExecutorImpl> queues = this.pool.queues; ++ ++ boolean ret = false; ++ for (;;) { ++ if (this.halted) { ++ break; ++ } ++ // try to find a queue ++ // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute. ++ // so we can only break when it's empty ++ final PrioritisedPoolExecutorImpl queue; ++ // select queue ++ synchronized (queues) { ++ queue = queues.pollFirst(); ++ if (queue == null) { ++ // no tasks to execute ++ break; ++ } ++ ++ queue.schedulingId = ++pool.schedulingIdGenerator; ++ // we own this queue now, so increment the executor count ++ // do we also need to push this queue up for grabs for another executor? ++ if (++queue.concurrentExecutors < queue.maximumExecutors) { ++ // re-add to queues ++ // it's very important this is done in the same synchronised block for polling, as this prevents ++ // us from possibly later adding a queue that should not exist in the set ++ queues.add(queue); ++ queue.isQueued = true; ++ } else { ++ queue.isQueued = false; ++ } ++ // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock ++ // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we ++ // try to poll now we don't hold the per queue lock but we do hold the global lock... ++ } ++ ++ // parse tasks as long as we are allowed ++ final long start = System.nanoTime(); ++ final long deadline = start + pool.queueMaxHoldTime; ++ do { ++ try { ++ if (this.halted) { ++ break; ++ } ++ if (!queue.executeTask()) { ++ // no more tasks, try next queue ++ break; ++ } ++ ret = true; ++ } catch (final ThreadDeath death) { ++ throw death; // goodbye world... ++ } catch (final Throwable throwable) { ++ LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable); ++ } ++ } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline); ++ ++ synchronized (queues) { ++ // decrement executors, we are no longer executing ++ if (queue.isQueued) { ++ queues.remove(queue); ++ queue.isQueued = false; ++ } ++ if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) { ++ // reset scheduling id once the queue is empty again ++ // this will ensure empty queues are not prioritised suddenly over active queues once tasks are ++ // queued ++ queue.schedulingId = 0L; ++ } ++ ++ // ensure the executor is queued for execution again ++ if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks ++ queues.add(queue); ++ queue.isQueued = true; ++ } ++ } ++ } ++ ++ return ret; ++ } ++ } ++ ++ public interface PrioritisedPoolExecutor extends PrioritisedExecutor { ++ ++ /** ++ * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed ++ */ ++ public void halt(); ++ ++ /** ++ * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether ++ * this queue is not halted and not shutdown. ++ */ ++ public boolean isActive(); ++ } ++ ++ protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor { ++ ++ protected final PrioritisedThreadPool pool; ++ protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ protected long schedulingId; ++ protected int concurrentExecutors; ++ protected Priority scheduledPriority; ++ ++ protected final String name; ++ protected final int maximumExecutors; ++ protected boolean isQueued; ++ ++ public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors) { ++ this.pool = pool; ++ this.name = name; ++ this.maximumExecutors = maximumExecutors; ++ } ++ ++ public static Comparator<PrioritisedPoolExecutorImpl> comparator() { ++ return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> { ++ if (p1 == p2) { ++ return 0; ++ } ++ ++ // prefer higher priority ++ final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal(); ++ if (priorityCompare != 0) { ++ return priorityCompare; ++ } ++ ++ // try to spread out the executors so that each can have threads executing ++ final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors; ++ if (executorCompare != 0) { ++ return executorCompare; ++ } ++ ++ // if all else fails here we just choose whichever executor was queued first ++ return Long.compare(p1.schedulingId, p2.schedulingId); ++ }; ++ } ++ ++ private boolean isHalted; ++ ++ @Override ++ public void halt() { ++ final PrioritisedThreadPool pool = this.pool; ++ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; ++ synchronized (queues) { ++ if (this.isHalted) { ++ return; ++ } ++ this.isHalted = true; ++ if (this.isQueued) { ++ queues.remove(this); ++ this.isQueued = false; ++ } ++ } ++ synchronized (pool.nonShutdownQueues) { ++ pool.nonShutdownQueues.remove(this); ++ } ++ synchronized (pool.activeQueues) { ++ pool.activeQueues.remove(this); ++ } ++ } ++ ++ @Override ++ public boolean isActive() { ++ final PrioritisedThreadPool pool = this.pool; ++ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; ++ ++ synchronized (queues) { ++ if (this.concurrentExecutors != 0) { ++ return true; ++ } ++ synchronized (pool.activeQueues) { ++ if (pool.activeQueues.contains(this)) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ private long totalQueuedTasks = 0L; ++ ++ @Override ++ protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) { ++ // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation ++ // for accessing this queue's state. ++ final long[] priorityCounts = this.priorityCounts; ++ final boolean shutdown = this.isShutdown(); ++ ++ if (from == null && to == Priority.COMPLETING) { ++ throw new IllegalStateException("Cannot complete task without queueing it first"); ++ } ++ ++ // we should only notify for queueing of tasks, not changing priorities ++ final boolean shouldNotifyTasks = from == null; ++ ++ final Priority scheduledPriority = this.scheduledPriority; ++ if (from != null) { ++ --priorityCounts[from.priority]; ++ } ++ if (to != Priority.COMPLETING) { ++ ++priorityCounts[to.priority]; ++ } ++ final long totalQueuedTasks; ++ if (to == Priority.COMPLETING) { ++ totalQueuedTasks = --this.totalQueuedTasks; ++ } else if (from == null) { ++ totalQueuedTasks = ++this.totalQueuedTasks; ++ } else { ++ totalQueuedTasks = this.totalQueuedTasks; ++ } ++ ++ // find new highest priority ++ int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority); ++ int lowestPriority = priorityCounts.length; // exclusive ++ for (;highest < lowestPriority; ++highest) { ++ final long count = priorityCounts[highest]; ++ if (count < 0) { ++ throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks"); ++ } ++ ++ if (count != 0) { ++ break; ++ } ++ } ++ ++ final Priority newPriority; ++ if (highest == lowestPriority) { ++ // no tasks left ++ newPriority = null; ++ } else if (shutdown) { ++ // whichever is lower, the actual greatest priority or simply HIGHEST ++ // this is so shutdown automatically gets priority ++ newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority)); ++ } else { ++ newPriority = Priority.getPriority(highest); ++ } ++ ++ final int executorsWanted; ++ boolean shouldNotifyHighPriority = false; ++ ++ final PrioritisedThreadPool pool = this.pool; ++ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; ++ ++ synchronized (queues) { ++ if (!this.isQueued) { ++ // see if we need to be queued ++ if (newPriority != null) { ++ if (this.schedulingId == 0L) { ++ this.schedulingId = ++pool.schedulingIdGenerator; ++ } ++ this.scheduledPriority = newPriority; // must be updated before queue add ++ if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) { ++ shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH); ++ queues.add(this); ++ this.isQueued = true; ++ } ++ } else { ++ // do not queue ++ this.scheduledPriority = null; ++ } ++ } else { ++ // see if we need to NOT be queued ++ if (newPriority == null) { ++ queues.remove(this); ++ this.scheduledPriority = null; ++ this.isQueued = false; ++ } else if (scheduledPriority != newPriority) { ++ // if our priority changed, we need to update it - which means removing and re-adding into the queue ++ queues.remove(this); ++ // only now can we update scheduledPriority, since we are no longer in queue ++ this.scheduledPriority = newPriority; ++ queues.add(this); ++ shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH); ++ } ++ } ++ ++ if (this.isQueued) { ++ executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks); ++ } else { ++ executorsWanted = 0; ++ } ++ } ++ ++ if (newPriority == null && shutdown) { ++ synchronized (pool.activeQueues) { ++ pool.activeQueues.remove(this); ++ } ++ } ++ ++ // Wake up the number of executors we want ++ if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) { ++ int notified = 0; ++ for (final PrioritisedThread thread : pool.threads) { ++ if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks()) ++ && (++notified >= executorsWanted)) { ++ break; ++ } ++ } ++ } ++ } ++ ++ @Override ++ public boolean shutdown() { ++ final boolean ret = super.shutdown(); ++ if (!ret) { ++ return ret; ++ } ++ ++ final PrioritisedThreadPool pool = this.pool; ++ ++ // remove from active queues ++ synchronized (pool.nonShutdownQueues) { ++ pool.nonShutdownQueues.remove(this); ++ } ++ ++ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; ++ ++ // try and shift around our priority ++ synchronized (queues) { ++ if (this.scheduledPriority == null) { ++ // no tasks are queued, ensure we aren't in activeQueues ++ synchronized (pool.activeQueues) { ++ pool.activeQueues.remove(this); ++ } ++ ++ return ret; ++ } ++ ++ // try to set scheduled priority to HIGHEST so it drains faster ++ ++ if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) { ++ // already at target priority (highest or above) ++ return ret; ++ } ++ ++ // shift priority to HIGHEST ++ ++ if (this.isQueued) { ++ queues.remove(this); ++ this.scheduledPriority = Priority.HIGHEST; ++ queues.add(this); ++ } else { ++ this.scheduledPriority = Priority.HIGHEST; ++ } ++ } ++ ++ return ret; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b71404be2c82f7db35272b367af861e94d6c73d3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java +@@ -0,0 +1,378 @@ ++package ca.spottedleaf.concurrentutil.executor.standard; ++ ++import java.util.ArrayDeque; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { ++ ++ protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { ++ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { ++ this.queues[i] = new ArrayDeque<>(); ++ } ++ } ++ ++ // Use AtomicLong to separate from the queue field, we don't want false sharing here. ++ protected final AtomicLong totalScheduledTasks = new AtomicLong(); ++ protected final AtomicLong totalCompletedTasks = new AtomicLong(); ++ ++ // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check) ++ protected volatile boolean hasShutdown; ++ ++ protected long taskIdGenerator = 0; ++ ++ @Override ++ public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Priority " + priority + " is invalid"); ++ } ++ if (task == null) { ++ throw new NullPointerException("Task cannot be null"); ++ } ++ ++ if (this.hasShutdown) { ++ // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something ++ throw new IllegalStateException("Queue has shutdown"); ++ } ++ ++ final PrioritisedTask ret; ++ ++ synchronized (this.queues) { ++ if (this.hasShutdown) { ++ throw new IllegalStateException("Queue has shutdown"); ++ } ++ this.getAndAddTotalScheduledTasksVolatile(1L); ++ ++ ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this); ++ ++ this.queues[ret.priority.priority].add(ret); ++ ++ // call priority change callback (note: only after we successfully queue!) ++ this.priorityChange(ret, null, priority); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Priority " + priority + " is invalid"); ++ } ++ if (task == null) { ++ throw new NullPointerException("Task cannot be null"); ++ } ++ ++ return new PrioritisedTask(task, priority, this); ++ } ++ ++ @Override ++ public long getTotalTasksScheduled() { ++ return this.totalScheduledTasks.get(); ++ } ++ ++ @Override ++ public long getTotalTasksExecuted() { ++ return this.totalCompletedTasks.get(); ++ } ++ ++ // callback method for subclasses to override ++ // from is null when a task is immediately created ++ protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {} ++ ++ /** ++ * Polls the highest priority task currently available. {@code null} if none. This will mark the ++ * returned task as completed. ++ */ ++ protected PrioritisedTask poll() { ++ return this.poll(Priority.IDLE); ++ } ++ ++ protected PrioritisedTask poll(final PrioritisedExecutor.Priority minPriority) { ++ final ArrayDeque<PrioritisedTask>[] queues = this.queues; ++ synchronized (queues) { ++ final int max = minPriority.priority; ++ for (int i = 0; i <= max; ++i) { ++ final ArrayDeque<PrioritisedTask> queue = queues[i]; ++ PrioritisedTask task; ++ while ((task = queue.pollFirst()) != null) { ++ if (task.trySetCompleting(i)) { ++ return task; ++ } ++ } ++ } ++ } ++ ++ return null; ++ } ++ ++ /** ++ * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will ++ * be rethrown. ++ * @return {@code true} if a task was executed, {@code false} otherwise. ++ */ ++ @Override ++ public boolean executeTask() { ++ final PrioritisedTask task = this.poll(); ++ ++ if (task != null) { ++ task.executeInternal(); ++ return true; ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean shutdown() { ++ synchronized (this.queues) { ++ if (this.hasShutdown) { ++ return false; ++ } ++ this.hasShutdown = true; ++ } ++ return true; ++ } ++ ++ @Override ++ public boolean isShutdown() { ++ return this.hasShutdown; ++ } ++ ++ /* totalScheduledTasks */ ++ ++ protected final long getTotalScheduledTasksVolatile() { ++ return this.totalScheduledTasks.get(); ++ } ++ ++ protected final long getAndAddTotalScheduledTasksVolatile(final long value) { ++ return this.totalScheduledTasks.getAndAdd(value); ++ } ++ ++ /* totalCompletedTasks */ ++ ++ protected final long getTotalCompletedTasksVolatile() { ++ return this.totalCompletedTasks.get(); ++ } ++ ++ protected final long getAndAddTotalCompletedTasksVolatile(final long value) { ++ return this.totalCompletedTasks.getAndAdd(value); ++ } ++ ++ protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask { ++ protected final PrioritisedThreadedTaskQueue queue; ++ protected long id; ++ protected static final long NOT_SCHEDULED_ID = -1L; ++ ++ protected Runnable runnable; ++ protected volatile PrioritisedExecutor.Priority priority; ++ ++ protected PrioritisedTask(final long id, final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ this.priority = priority; ++ this.runnable = runnable; ++ this.queue = queue; ++ this.id = id; ++ } ++ ++ protected PrioritisedTask(final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ this.priority = priority; ++ this.runnable = runnable; ++ this.queue = queue; ++ this.id = NOT_SCHEDULED_ID; ++ } ++ ++ @Override ++ public boolean queue() { ++ if (this.queue.hasShutdown) { ++ throw new IllegalStateException("Queue has shutdown"); ++ } ++ ++ synchronized (this.queue.queues) { ++ if (this.queue.hasShutdown) { ++ throw new IllegalStateException("Queue has shutdown"); ++ } ++ ++ final PrioritisedExecutor.Priority priority = this.priority; ++ if (priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.id != NOT_SCHEDULED_ID) { ++ return false; ++ } ++ ++ this.queue.getAndAddTotalScheduledTasksVolatile(1L); ++ this.id = this.queue.taskIdGenerator++; ++ this.queue.queues[priority.priority].add(this); ++ ++ this.queue.priorityChange(this, null, priority); ++ ++ return true; ++ } ++ } ++ ++ protected boolean trySetCompleting(final int minPriority) { ++ final PrioritisedExecutor.Priority oldPriority = this.priority; ++ if (oldPriority != PrioritisedExecutor.Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) { ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ if (this.id != NOT_SCHEDULED_ID) { ++ this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); ++ } ++ return true; ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.priority; ++ } ++ ++ @Override ++ public boolean setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ synchronized (this.queue.queues) { ++ final PrioritisedExecutor.Priority curr = this.priority; ++ ++ if (curr == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr == priority) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.id != NOT_SCHEDULED_ID) { ++ this.queue.queues[priority.priority].add(this); ++ ++ // call priority change callback ++ this.queue.priorityChange(this, curr, priority); ++ } ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public boolean raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ synchronized (this.queue.queues) { ++ final PrioritisedExecutor.Priority curr = this.priority; ++ ++ if (curr == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isHigherOrEqualPriority(priority)) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.id != NOT_SCHEDULED_ID) { ++ this.queue.queues[priority.priority].add(this); ++ ++ // call priority change callback ++ this.queue.priorityChange(this, curr, priority); ++ } ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ synchronized (this.queue.queues) { ++ final PrioritisedExecutor.Priority curr = this.priority; ++ ++ if (curr == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isLowerOrEqualPriority(priority)) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.id != NOT_SCHEDULED_ID) { ++ this.queue.queues[priority.priority].add(this); ++ ++ // call priority change callback ++ this.queue.priorityChange(this, curr, priority); ++ } ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final long id; ++ synchronized (this.queue.queues) { ++ final Priority oldPriority = this.priority; ++ if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ // call priority change callback ++ if ((id = this.id) != NOT_SCHEDULED_ID) { ++ this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); ++ } ++ } ++ this.runnable = null; ++ if (id != NOT_SCHEDULED_ID) { ++ this.queue.getAndAddTotalCompletedTasksVolatile(1L); ++ } ++ return true; ++ } ++ ++ protected void executeInternal() { ++ try { ++ final Runnable execute = this.runnable; ++ this.runnable = null; ++ execute.run(); ++ } finally { ++ if (this.id != NOT_SCHEDULED_ID) { ++ this.queue.getAndAddTotalCompletedTasksVolatile(1L); ++ } ++ } ++ } ++ ++ @Override ++ public boolean execute() { ++ synchronized (this.queue.queues) { ++ final Priority oldPriority = this.priority; ++ if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ // call priority change callback ++ if (this.id != NOT_SCHEDULED_ID) { ++ this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); ++ } ++ } ++ ++ this.executeInternal(); ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4289b984badd6f9167c86193454a630b9a40f9f5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java +@@ -0,0 +1,1673 @@ ++package ca.spottedleaf.concurrentutil.map; ++ ++import ca.spottedleaf.concurrentutil.util.ArrayUtil; ++import ca.spottedleaf.concurrentutil.util.CollectionUtil; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import io.papermc.paper.util.IntegerUtil; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.NoSuchElementException; ++import java.util.Set; ++import java.util.Spliterator; ++import java.util.Spliterators; ++import java.util.function.BiConsumer; ++import java.util.function.BiFunction; ++import java.util.function.BiPredicate; ++import java.util.function.Consumer; ++import java.util.function.Function; ++import java.util.function.IntFunction; ++import java.util.function.Predicate; ++ ++/** ++ * <p> ++ * Note: Not really tested, use at your own risk. ++ * </p> ++ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread. ++ * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics, ++ * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually. ++ * Reads are ordered by acquire semantics. ++ * <p> ++ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no ++ * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined ++ * behaviour. ++ * </p> ++ * <p> ++ * Subclasses should override {@link #clone()} to return correct instances of this class. ++ * </p> ++ * @param <K> {@inheritDoc} ++ * @param <V> {@inheritDoc} ++ */ ++public class SWMRHashTable<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>> { ++ ++ protected int size; ++ ++ protected TableEntry<K, V>[] table; ++ ++ protected final float loadFactor; ++ ++ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class); ++ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class); ++ ++ /* size */ ++ ++ protected final int getSizePlain() { ++ return (int)SIZE_HANDLE.get(this); ++ } ++ ++ protected final int getSizeOpaque() { ++ return (int)SIZE_HANDLE.getOpaque(this); ++ } ++ ++ protected final int getSizeAcquire() { ++ return (int)SIZE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setSizePlain(final int value) { ++ SIZE_HANDLE.set(this, value); ++ } ++ ++ protected final void setSizeOpaque(final int value) { ++ SIZE_HANDLE.setOpaque(this, value); ++ } ++ ++ protected final void setSizeRelease(final int value) { ++ SIZE_HANDLE.setRelease(this, value); ++ } ++ ++ /* table */ ++ ++ protected final TableEntry<K, V>[] getTablePlain() { ++ //noinspection unchecked ++ return (TableEntry<K, V>[])TABLE_HANDLE.get(this); ++ } ++ ++ protected final TableEntry<K, V>[] getTableAcquire() { ++ //noinspection unchecked ++ return (TableEntry<K, V>[])TABLE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setTablePlain(final TableEntry<K, V>[] table) { ++ TABLE_HANDLE.set(this, table); ++ } ++ ++ protected final void setTableRelease(final TableEntry<K, V>[] table) { ++ TABLE_HANDLE.setRelease(this, table); ++ } ++ ++ protected static final int DEFAULT_CAPACITY = 16; ++ protected static final float DEFAULT_LOAD_FACTOR = 0.75f; ++ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; ++ ++ /** ++ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. ++ */ ++ public SWMRHashTable() { ++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); ++ } ++ ++ /** ++ * Constructs this map with the specified capacity and load factor of {@code 0.75f}. ++ * @param capacity specified initial capacity, > 0 ++ */ ++ public SWMRHashTable(final int capacity) { ++ this(capacity, DEFAULT_LOAD_FACTOR); ++ } ++ ++ /** ++ * Constructs this map with the specified capacity and load factor. ++ * @param capacity specified capacity, > 0 ++ * @param loadFactor specified load factor, > 0 and finite ++ */ ++ public SWMRHashTable(final int capacity, final float loadFactor) { ++ final int tableSize = getCapacityFor(capacity); ++ ++ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { ++ throw new IllegalArgumentException("Invalid load factor: " + loadFactor); ++ } ++ ++ //noinspection unchecked ++ final TableEntry<K, V>[] table = new TableEntry[tableSize]; ++ this.setTablePlain(table); ++ ++ if (tableSize == MAXIMUM_CAPACITY) { ++ this.threshold = -1; ++ } else { ++ this.threshold = getTargetCapacity(tableSize, loadFactor); ++ } ++ ++ this.loadFactor = loadFactor; ++ } ++ ++ /** ++ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and ++ * with a load factor of {@code 0.75f}. ++ * All of the specified map's entries are copied into this map. ++ * @param other The specified map. ++ */ ++ public SWMRHashTable(final Map<K, V> other) { ++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); ++ } ++ ++ /** ++ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and ++ * with a load factor of {@code 0.75f}. ++ * All of the specified map's entries are copied into this map. ++ * @param capacity specified capacity, > 0 ++ * @param other The specified map. ++ */ ++ public SWMRHashTable(final int capacity, final Map<K, V> other) { ++ this(capacity, DEFAULT_LOAD_FACTOR, other); ++ } ++ ++ /** ++ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and ++ * with the specified load factor. ++ * All of the specified map's entries are copied into this map. ++ * @param capacity specified capacity, > 0 ++ * @param loadFactor specified load factor, > 0 and finite ++ * @param other The specified map. ++ */ ++ public SWMRHashTable(final int capacity, final float loadFactor, final Map<K, V> other) { ++ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); ++ this.putAll(other); ++ } ++ ++ public final float getLoadFactor() { ++ return this.loadFactor; ++ } ++ ++ protected static int getCapacityFor(final int capacity) { ++ if (capacity <= 0) { ++ throw new IllegalArgumentException("Invalid capacity: " + capacity); ++ } ++ if (capacity >= MAXIMUM_CAPACITY) { ++ return MAXIMUM_CAPACITY; ++ } ++ return IntegerUtil.roundCeilLog2(capacity); ++ } ++ ++ /** Callers must still use acquire when reading the value of the entry. */ ++ protected final TableEntry<K, V> getEntryForOpaque(final K key) { ++ final int hash = SWMRHashTable.getHash(key); ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { ++ if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { ++ return curr; ++ } ++ } ++ ++ return null; ++ } ++ ++ protected final TableEntry<K, V> getEntryForPlain(final K key) { ++ final int hash = SWMRHashTable.getHash(key); ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ ++ for (TableEntry<K, V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { ++ if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { ++ return curr; ++ } ++ } ++ ++ return null; ++ } ++ ++ /* MT-Safe */ ++ ++ /** must be deterministic given a key */ ++ private static int getHash(final Object key) { ++ int hash = key == null ? 0 : key.hashCode(); ++ // inlined IntegerUtil#hash0 ++ hash *= 0x36935555; ++ hash ^= hash >>> 16; ++ return hash; ++ } ++ ++ static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash ++ static final int spread(int h) { ++ return (h ^ (h >>> 16)) & HASH_BITS; ++ } ++ ++ // rets -1 if capacity*loadFactor is too large ++ protected static int getTargetCapacity(final int capacity, final float loadFactor) { ++ final double ret = (double)capacity * (double)loadFactor; ++ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { ++ return -1; ++ } ++ ++ return (int)ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ /* Make no attempt to deal with concurrent modifications */ ++ if (!(obj instanceof Map)) { ++ return false; ++ } ++ final Map<?, ?> other = (Map<?, ?>)obj; ++ ++ if (this.size() != other.size()) { ++ return false; ++ } ++ ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V value = curr.getValueAcquire(); ++ ++ final Object otherValue = other.get(curr.key); ++ if (otherValue == null || (value != otherValue && value.equals(otherValue))) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int hashCode() { ++ /* Make no attempt to deal with concurrent modifications */ ++ int hash = 0; ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ hash += curr.hashCode(); ++ } ++ } ++ ++ return hash; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public String toString() { ++ final StringBuilder builder = new StringBuilder(64); ++ builder.append("SingleWriterMultiReaderHashMap:{"); ++ ++ this.forEach((final K key, final V value) -> { ++ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); ++ }); ++ ++ return builder.append('}').toString(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public SWMRHashTable<K, V> clone() { ++ return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public Iterator<Entry<K, V>> iterator() { ++ return new EntryIterator<>(this.getTableAcquire(), this); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void forEach(final Consumer<? super Entry<K, V>> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ action.accept(curr); ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void forEach(final BiConsumer<? super K, ? super V> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V value = curr.getValueAcquire(); ++ ++ action.accept(curr.key, value); ++ } ++ } ++ } ++ ++ /** ++ * Provides the specified consumer with all keys contained within this map. ++ * @param action The specified consumer. ++ */ ++ public void forEachKey(final Consumer<? super K> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ action.accept(curr.key); ++ } ++ } ++ } ++ ++ /** ++ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. ++ * @param action The specified consumer. ++ */ ++ public void forEachValue(final Consumer<? super V> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V value = curr.getValueAcquire(); ++ ++ action.accept(value); ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V get(final Object key) { ++ Validate.notNull(key, "Null key"); ++ ++ //noinspection unchecked ++ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key); ++ return entry == null ? null : entry.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean containsKey(final Object key) { ++ Validate.notNull(key, "Null key"); ++ ++ // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics ++ return this.get(key) != null; ++ } ++ ++ /** ++ * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call. ++ * @param key The specified key. ++ * @param value The specified value. ++ * @return {@code true} if this map contains an entry with the specified key and value. ++ */ ++ public boolean contains(final Object key, final Object value) { ++ Validate.notNull(key, "Null key"); ++ ++ //noinspection unchecked ++ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key); ++ ++ if (entry == null) { ++ return false; ++ } ++ ++ final V entryVal = entry.getValueAcquire(); ++ return entryVal == value || entryVal.equals(value); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean containsValue(final Object value) { ++ Validate.notNull(value, "Null value"); ++ ++ final TableEntry<K, V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V currVal = curr.getValueAcquire(); ++ if (currVal == value || currVal.equals(value)) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V getOrDefault(final Object key, final V defaultValue) { ++ Validate.notNull(key, "Null key"); ++ ++ //noinspection unchecked ++ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key); ++ ++ return entry == null ? defaultValue : entry.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int size() { ++ return this.getSizeAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean isEmpty() { ++ return this.getSizeAcquire() == 0; ++ } ++ ++ protected Set<K> keyset; ++ protected Collection<V> values; ++ protected Set<Map.Entry<K, V>> entrySet; ++ ++ @Override ++ public Set<K> keySet() { ++ return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset; ++ } ++ ++ @Override ++ public Collection<V> values() { ++ return this.values == null ? this.values = new ValueCollection<>(this) : this.values; ++ } ++ ++ @Override ++ public Set<Map.Entry<K, V>> entrySet() { ++ return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet; ++ } ++ ++ /* Non-MT-Safe */ ++ ++ protected int threshold; ++ ++ protected final void checkResize(final int minCapacity) { ++ if (minCapacity <= this.threshold || this.threshold < 0) { ++ return; ++ } ++ ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); ++ if (newCapacity < 0) { ++ newCapacity = MAXIMUM_CAPACITY; ++ } ++ if (newCapacity <= table.length) { ++ if (newCapacity == MAXIMUM_CAPACITY) { ++ return; ++ } ++ newCapacity = table.length << 1; ++ } ++ ++ //noinspection unchecked ++ final TableEntry<K, V>[] newTable = new TableEntry[newCapacity]; ++ final int indexMask = newCapacity - 1; ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> entry = table[i]; entry != null; entry = entry.getNextPlain()) { ++ final int hash = entry.hash; ++ final int index = hash & indexMask; ++ ++ /* we need to create a new entry since there could be reading threads */ ++ final TableEntry<K, V> insert = new TableEntry<>(hash, entry.key, entry.getValuePlain()); ++ ++ final TableEntry<K, V> prev = newTable[index]; ++ ++ newTable[index] = insert; ++ insert.setNextPlain(prev); ++ } ++ } ++ ++ if (newCapacity == MAXIMUM_CAPACITY) { ++ this.threshold = -1; /* No more resizing */ ++ } else { ++ this.threshold = getTargetCapacity(newCapacity, this.loadFactor); ++ } ++ this.setTableRelease(newTable); /* use release to publish entries in table */ ++ } ++ ++ protected final int addToSize(final int num) { ++ final int newSize = this.getSizePlain() + num; ++ ++ this.setSizeOpaque(newSize); ++ this.checkResize(newSize); ++ ++ return newSize; ++ } ++ ++ protected final int removeFromSize(final int num) { ++ final int newSize = this.getSizePlain() - num; ++ ++ this.setSizeOpaque(newSize); ++ ++ return newSize; ++ } ++ ++ /* Cannot be used to perform downsizing */ ++ protected final int removeFromSizePlain(final int num) { ++ final int newSize = this.getSizePlain() - num; ++ ++ this.setSizePlain(newSize); ++ ++ return newSize; ++ } ++ ++ protected final V put(final K key, final V value, final boolean onlyIfAbsent) { ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int hash = SWMRHashTable.getHash(key); ++ final int index = hash & (table.length - 1); ++ ++ final TableEntry<K, V> head = table[index]; ++ if (head == null) { ++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value); ++ ArrayUtil.setRelease(table, index, insert); ++ this.addToSize(1); ++ return null; ++ } ++ ++ for (TableEntry<K, V> curr = head;;) { ++ if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) { ++ if (onlyIfAbsent) { ++ return curr.getValuePlain(); ++ } ++ ++ final V currVal = curr.getValuePlain(); ++ curr.setValueRelease(value); ++ return currVal; ++ } ++ ++ final TableEntry<K, V> next = curr.getNextPlain(); ++ if (next != null) { ++ curr = next; ++ continue; ++ } ++ ++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value); ++ ++ curr.setNextRelease(insert); ++ this.addToSize(1); ++ return null; ++ } ++ } ++ ++ /** ++ * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is ++ * tested with every entry in this map. Returns the number of key-value pairs removed. ++ * @param predicate The predicate to test key-value pairs against. ++ * @return The total number of key-value pairs removed from this map. ++ */ ++ public int removeIf(final BiPredicate<K, V> predicate) { ++ Validate.notNull(predicate, "Null predicate"); ++ ++ int removed = 0; ++ ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ ++ bin_iteration_loop: ++ for (int i = 0, len = table.length; i < len; ++i) { ++ TableEntry<K, V> curr = table[i]; ++ if (curr == null) { ++ continue; ++ } ++ ++ /* Handle bin nodes first */ ++ while (predicate.test(curr.key, curr.getValuePlain())) { ++ ++removed; ++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */ ++ ++ ArrayUtil.setRelease(table, i, curr = curr.getNextPlain()); ++ ++ if (curr == null) { ++ continue bin_iteration_loop; ++ } ++ } ++ ++ TableEntry<K, V> prev; ++ ++ /* curr at this point is the bin node */ ++ ++ for (prev = curr, curr = curr.getNextPlain(); curr != null;) { ++ /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */ ++ if (predicate.test(curr.key, curr.getValuePlain())) { ++ ++removed; ++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */ ++ ++ prev.setNextRelease(curr = curr.getNextPlain()); ++ } else { ++ prev = curr; ++ curr = curr.getNextPlain(); ++ } ++ } ++ } ++ ++ return removed; ++ } ++ ++ /** ++ * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is ++ * tested with every entry in this map. Returns the number of key-value pairs removed. ++ * @param predicate The predicate to test key-value pairs against. ++ * @return The total number of key-value pairs removed from this map. ++ */ ++ public int removeEntryIf(final Predicate<? super Entry<K, V>> predicate) { ++ Validate.notNull(predicate, "Null predicate"); ++ ++ int removed = 0; ++ ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ ++ bin_iteration_loop: ++ for (int i = 0, len = table.length; i < len; ++i) { ++ TableEntry<K, V> curr = table[i]; ++ if (curr == null) { ++ continue; ++ } ++ ++ /* Handle bin nodes first */ ++ while (predicate.test(curr)) { ++ ++removed; ++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */ ++ ++ ArrayUtil.setRelease(table, i, curr = curr.getNextPlain()); ++ ++ if (curr == null) { ++ continue bin_iteration_loop; ++ } ++ } ++ ++ TableEntry<K, V> prev; ++ ++ /* curr at this point is the bin node */ ++ ++ for (prev = curr, curr = curr.getNextPlain(); curr != null;) { ++ /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */ ++ if (predicate.test(curr)) { ++ ++removed; ++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */ ++ ++ prev.setNextRelease(curr = curr.getNextPlain()); ++ } else { ++ prev = curr; ++ curr = curr.getNextPlain(); ++ } ++ } ++ } ++ ++ return removed; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V put(final K key, final V value) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(value, "Null value"); ++ ++ return this.put(key, value, false); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V putIfAbsent(final K key, final V value) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(value, "Null value"); ++ ++ return this.put(key, value, true); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean remove(final Object key, final Object value) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(value, "Null value"); ++ ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int hash = SWMRHashTable.getHash(key); ++ final int index = hash & (table.length - 1); ++ ++ final TableEntry<K, V> head = table[index]; ++ if (head == null) { ++ return false; ++ } ++ ++ if (head.hash == hash && (head.key == key || head.key.equals(key))) { ++ final V currVal = head.getValuePlain(); ++ ++ if (currVal != value && !currVal.equals(value)) { ++ return false; ++ } ++ ++ ArrayUtil.setRelease(table, index, head.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return true; ++ } ++ ++ for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { ++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { ++ final V currVal = curr.getValuePlain(); ++ ++ if (currVal != value && !currVal.equals(value)) { ++ return false; ++ } ++ ++ prev.setNextRelease(curr.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ ++ protected final V remove(final Object key, final int hash) { ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int index = (table.length - 1) & hash; ++ ++ final TableEntry<K, V> head = table[index]; ++ if (head == null) { ++ return null; ++ } ++ ++ if (hash == head.hash && (head.key == key || head.key.equals(key))) { ++ ArrayUtil.setRelease(table, index, head.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return head.getValuePlain(); ++ } ++ ++ for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { ++ if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) { ++ prev.setNextRelease(curr.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return curr.getValuePlain(); ++ } ++ } ++ ++ return null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V remove(final Object key) { ++ Validate.notNull(key, "Null key"); ++ ++ return this.remove(key, SWMRHashTable.getHash(key)); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean replace(final K key, final V oldValue, final V newValue) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(oldValue, "Null oldValue"); ++ Validate.notNull(newValue, "Null newValue"); ++ ++ final TableEntry<K, V> entry = this.getEntryForPlain(key); ++ if (entry == null) { ++ return false; ++ } ++ ++ final V currValue = entry.getValuePlain(); ++ if (currValue == oldValue || currValue.equals(oldValue)) { ++ entry.setValueRelease(newValue); ++ return true; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V replace(final K key, final V value) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(value, "Null value"); ++ ++ final TableEntry<K, V> entry = this.getEntryForPlain(key); ++ if (entry == null) { ++ return null; ++ } ++ ++ final V prev = entry.getValuePlain(); ++ entry.setValueRelease(value); ++ return prev; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) { ++ Validate.notNull(function, "Null function"); ++ ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<K, V> curr = table[i]; curr != null; curr = curr.getNextPlain()) { ++ final V value = curr.getValuePlain(); ++ ++ final V newValue = function.apply(curr.key, value); ++ if (newValue == null) { ++ throw new NullPointerException(); ++ } ++ ++ curr.setValueRelease(newValue); ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public void putAll(final Map<? extends K, ? extends V> map) { ++ Validate.notNull(map, "Null map"); ++ ++ final int size = map.size(); ++ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ ++ map.forEach(this::put); ++ } ++ ++ /** ++ * {@inheritDoc} ++ * <p> ++ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself ++ * is release ordered, that is, after the clear operation is performed a release fence is performed. ++ * </p> ++ */ ++ @Override ++ public void clear() { ++ Arrays.fill(this.getTablePlain(), null); ++ this.setSizeRelease(0); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(remappingFunction, "Null remappingFunction"); ++ ++ final int hash = SWMRHashTable.getHash(key); ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int index = hash & (table.length - 1); ++ ++ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { ++ if (curr == null) { ++ final V newVal = remappingFunction.apply(key ,null); ++ ++ if (newVal == null) { ++ return null; ++ } ++ ++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal); ++ if (prev == null) { ++ ArrayUtil.setRelease(table, index, insert); ++ } else { ++ prev.setNextRelease(insert); ++ } ++ ++ this.addToSize(1); ++ ++ return newVal; ++ } ++ ++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { ++ final V newVal = remappingFunction.apply(key, curr.getValuePlain()); ++ ++ if (newVal != null) { ++ curr.setValueRelease(newVal); ++ return newVal; ++ } ++ ++ if (prev == null) { ++ ArrayUtil.setRelease(table, index, curr.getNextPlain()); ++ } else { ++ prev.setNextRelease(curr.getNextPlain()); ++ } ++ ++ this.removeFromSize(1); ++ ++ return null; ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(remappingFunction, "Null remappingFunction"); ++ ++ final int hash = SWMRHashTable.getHash(key); ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int index = hash & (table.length - 1); ++ ++ for (TableEntry<K, V> curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) { ++ if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) { ++ continue; ++ } ++ ++ final V newVal = remappingFunction.apply(key, curr.getValuePlain()); ++ if (newVal != null) { ++ curr.setValueRelease(newVal); ++ return newVal; ++ } ++ ++ if (prev == null) { ++ ArrayUtil.setRelease(table, index, curr.getNextPlain()); ++ } else { ++ prev.setNextRelease(curr.getNextPlain()); ++ } ++ ++ this.removeFromSize(1); ++ ++ return null; ++ } ++ ++ return null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(mappingFunction, "Null mappingFunction"); ++ ++ final int hash = SWMRHashTable.getHash(key); ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int index = hash & (table.length - 1); ++ ++ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { ++ if (curr != null) { ++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { ++ return curr.getValuePlain(); ++ } ++ continue; ++ } ++ ++ final V newVal = mappingFunction.apply(key); ++ ++ if (newVal == null) { ++ return null; ++ } ++ ++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal); ++ if (prev == null) { ++ ArrayUtil.setRelease(table, index, insert); ++ } else { ++ prev.setNextRelease(insert); ++ } ++ ++ this.addToSize(1); ++ ++ return newVal; ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) { ++ Validate.notNull(key, "Null key"); ++ Validate.notNull(value, "Null value"); ++ Validate.notNull(remappingFunction, "Null remappingFunction"); ++ ++ final int hash = SWMRHashTable.getHash(key); ++ final TableEntry<K, V>[] table = this.getTablePlain(); ++ final int index = hash & (table.length - 1); ++ ++ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { ++ if (curr == null) { ++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value); ++ if (prev == null) { ++ ArrayUtil.setRelease(table, index, insert); ++ } else { ++ prev.setNextRelease(insert); ++ } ++ ++ this.addToSize(1); ++ ++ return value; ++ } ++ ++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { ++ final V newVal = remappingFunction.apply(curr.getValuePlain(), value); ++ ++ if (newVal != null) { ++ curr.setValueRelease(newVal); ++ return newVal; ++ } ++ ++ if (prev == null) { ++ ArrayUtil.setRelease(table, index, curr.getNextPlain()); ++ } else { ++ prev.setNextRelease(curr.getNextPlain()); ++ } ++ ++ this.removeFromSize(1); ++ ++ return null; ++ } ++ } ++ } ++ ++ protected static final class TableEntry<K, V> implements Map.Entry<K, V> { ++ ++ protected final int hash; ++ protected final K key; ++ protected V value; ++ ++ protected TableEntry<K, V> next; ++ ++ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); ++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); ++ ++ /* value */ ++ ++ protected final V getValuePlain() { ++ //noinspection unchecked ++ return (V)VALUE_HANDLE.get(this); ++ } ++ ++ protected final V getValueAcquire() { ++ //noinspection unchecked ++ return (V)VALUE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setValueRelease(final V to) { ++ VALUE_HANDLE.setRelease(this, to); ++ } ++ ++ /* next */ ++ ++ protected final TableEntry<K, V> getNextPlain() { ++ //noinspection unchecked ++ return (TableEntry<K, V>)NEXT_HANDLE.get(this); ++ } ++ ++ protected final TableEntry<K, V> getNextOpaque() { ++ //noinspection unchecked ++ return (TableEntry<K, V>)NEXT_HANDLE.getOpaque(this); ++ } ++ ++ protected final void setNextPlain(final TableEntry<K, V> next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ protected final void setNextRelease(final TableEntry<K, V> next) { ++ NEXT_HANDLE.setRelease(this, next); ++ } ++ ++ protected TableEntry(final int hash, final K key, final V value) { ++ this.hash = hash; ++ this.key = key; ++ this.value = value; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public K getKey() { ++ return this.key; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V getValue() { ++ return this.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public V setValue(final V value) { ++ if (value == null) { ++ throw new NullPointerException(); ++ } ++ ++ final V curr = this.getValuePlain(); ++ ++ this.setValueRelease(value); ++ return curr; ++ } ++ ++ protected static int hash(final Object key, final Object value) { ++ return key.hashCode() ^ (value == null ? 0 : value.hashCode()); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int hashCode() { ++ return hash(this.key, this.getValueAcquire()); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof Map.Entry)) { ++ return false; ++ } ++ final Map.Entry<?, ?> other = (Map.Entry<?, ?>)obj; ++ final Object otherKey = other.getKey(); ++ final Object otherValue = other.getValue(); ++ ++ final K thisKey = this.getKey(); ++ final V thisVal = this.getValueAcquire(); ++ return (thisKey == otherKey || thisKey.equals(otherKey)) && ++ (thisVal == otherValue || thisVal.equals(otherValue)); ++ } ++ } ++ ++ ++ protected static abstract class TableEntryIterator<K, V, T> implements Iterator<T> { ++ ++ protected final TableEntry<K, V>[] table; ++ protected final SWMRHashTable<K, V> map; ++ ++ /* bin which our current element resides on */ ++ protected int tableIndex; ++ ++ protected TableEntry<K, V> currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */ ++ protected TableEntry<K, V> nextEntry; /* may not be on the same bin as currEntry */ ++ ++ protected TableEntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) { ++ this.table = table; ++ this.map = map; ++ int tableIndex = 0; ++ for (int len = table.length; tableIndex < len; ++tableIndex) { ++ final TableEntry<K, V> entry = ArrayUtil.getOpaque(table, tableIndex); ++ if (entry != null) { ++ this.nextEntry = entry; ++ this.tableIndex = tableIndex + 1; ++ return; ++ } ++ } ++ this.tableIndex = tableIndex; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ return this.nextEntry != null; ++ } ++ ++ protected final TableEntry<K, V> advanceEntry() { ++ final TableEntry<K, V>[] table = this.table; ++ final int tableLength = table.length; ++ int tableIndex = this.tableIndex; ++ final TableEntry<K, V> curr = this.nextEntry; ++ if (curr == null) { ++ return null; ++ } ++ ++ this.currEntry = curr; ++ ++ // set up nextEntry ++ ++ // find next in chain ++ TableEntry<K, V> next = curr.getNextOpaque(); ++ ++ if (next != null) { ++ this.nextEntry = next; ++ return curr; ++ } ++ ++ // nothing in chain, so find next available bin ++ for (;tableIndex < tableLength; ++tableIndex) { ++ next = ArrayUtil.getOpaque(table, tableIndex); ++ if (next != null) { ++ this.nextEntry = next; ++ this.tableIndex = tableIndex + 1; ++ return curr; ++ } ++ } ++ ++ this.nextEntry = null; ++ this.tableIndex = tableIndex; ++ return curr; ++ } ++ ++ @Override ++ public void remove() { ++ final TableEntry<K, V> curr = this.currEntry; ++ if (curr == null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.map.remove(curr.key, curr.hash); ++ ++ this.currEntry = null; ++ } ++ } ++ ++ protected static final class ValueIterator<K, V> extends TableEntryIterator<K, V, V> { ++ ++ protected ValueIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) { ++ super(table, map); ++ } ++ ++ @Override ++ public V next() { ++ final TableEntry<K, V> entry = this.advanceEntry(); ++ ++ if (entry == null) { ++ throw new NoSuchElementException(); ++ } ++ ++ return entry.getValueAcquire(); ++ } ++ } ++ ++ protected static final class KeyIterator<K, V> extends TableEntryIterator<K, V, K> { ++ ++ protected KeyIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) { ++ super(table, map); ++ } ++ ++ @Override ++ public K next() { ++ final TableEntry<K, V> curr = this.advanceEntry(); ++ ++ if (curr == null) { ++ throw new NoSuchElementException(); ++ } ++ ++ return curr.key; ++ } ++ } ++ ++ protected static final class EntryIterator<K, V> extends TableEntryIterator<K, V, Map.Entry<K, V>> { ++ ++ protected EntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) { ++ super(table, map); ++ } ++ ++ @Override ++ public Map.Entry<K, V> next() { ++ final TableEntry<K, V> curr = this.advanceEntry(); ++ ++ if (curr == null) { ++ throw new NoSuchElementException(); ++ } ++ ++ return curr; ++ } ++ } ++ ++ protected static abstract class ViewCollection<K, V, T> implements Collection<T> { ++ ++ protected final SWMRHashTable<K, V> map; ++ ++ protected ViewCollection(final SWMRHashTable<K, V> map) { ++ this.map = map; ++ } ++ ++ @Override ++ public boolean add(final T element) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean addAll(final Collection<? extends T> collections) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean removeAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ boolean modified = false; ++ for (final Object element : collection) { ++ modified |= this.remove(element); ++ } ++ return modified; ++ } ++ ++ @Override ++ public int size() { ++ return this.map.size(); ++ } ++ ++ @Override ++ public boolean isEmpty() { ++ return this.size() == 0; ++ } ++ ++ @Override ++ public void clear() { ++ this.map.clear(); ++ } ++ ++ @Override ++ public boolean containsAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ for (final Object element : collection) { ++ if (!this.contains(element)) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public Object[] toArray() { ++ final List<T> list = new ArrayList<>(this.size()); ++ ++ this.forEach(list::add); ++ ++ return list.toArray(); ++ } ++ ++ @Override ++ public <E> E[] toArray(final E[] array) { ++ final List<T> list = new ArrayList<>(this.size()); ++ ++ this.forEach(list::add); ++ ++ return list.toArray(array); ++ } ++ ++ @Override ++ public <E> E[] toArray(final IntFunction<E[]> generator) { ++ final List<T> list = new ArrayList<>(this.size()); ++ ++ this.forEach(list::add); ++ ++ return list.toArray(generator); ++ } ++ ++ @Override ++ public int hashCode() { ++ int hash = 0; ++ for (final T element : this) { ++ hash += element == null ? 0 : element.hashCode(); ++ } ++ return hash; ++ } ++ ++ @Override ++ public Spliterator<T> spliterator() { // TODO implement ++ return Spliterators.spliterator(this, Spliterator.NONNULL); ++ } ++ } ++ ++ protected static abstract class ViewSet<K, V, T> extends ViewCollection<K, V, T> implements Set<T> { ++ ++ protected ViewSet(final SWMRHashTable<K, V> map) { ++ super(map); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof Set)) { ++ return false; ++ } ++ ++ final Set<?> other = (Set<?>)obj; ++ if (other.size() != this.size()) { ++ return false; ++ } ++ ++ return this.containsAll(other); ++ } ++ } ++ ++ protected static final class EntrySet<K, V> extends ViewSet<K, V, Map.Entry<K, V>> implements Set<Map.Entry<K, V>> { ++ ++ protected EntrySet(final SWMRHashTable<K, V> map) { ++ super(map); ++ } ++ ++ @Override ++ public boolean remove(final Object object) { ++ if (!(object instanceof Map.Entry<?, ?>)) { ++ return false; ++ } ++ final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object; ++ ++ final Object key; ++ final Object value; ++ ++ try { ++ key = entry.getKey(); ++ value = entry.getValue(); ++ } catch (final IllegalStateException ex) { ++ return false; ++ } ++ ++ return this.map.remove(key, value); ++ } ++ ++ @Override ++ public boolean removeIf(final Predicate<? super Map.Entry<K, V>> filter) { ++ Validate.notNull(filter, "Null filter"); ++ ++ return this.map.removeEntryIf(filter) != 0; ++ } ++ ++ @Override ++ public boolean retainAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ return this.map.removeEntryIf((final Map.Entry<K, V> entry) -> { ++ return !collection.contains(entry); ++ }) != 0; ++ } ++ ++ @Override ++ public Iterator<Entry<K, V>> iterator() { ++ return new EntryIterator<>(this.map.getTableAcquire(), this.map); ++ } ++ ++ @Override ++ public void forEach(final Consumer<? super Entry<K, V>> action) { ++ this.map.forEach(action); ++ } ++ ++ @Override ++ public boolean contains(final Object object) { ++ if (!(object instanceof Map.Entry)) { ++ return false; ++ } ++ final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object; ++ ++ final Object key; ++ final Object value; ++ ++ try { ++ key = entry.getKey(); ++ value = entry.getValue(); ++ } catch (final IllegalStateException ex) { ++ return false; ++ } ++ ++ return this.map.contains(key, value); ++ } ++ ++ @Override ++ public String toString() { ++ return CollectionUtil.toString(this, "SWMRHashTableEntrySet"); ++ } ++ } ++ ++ protected static final class KeySet<K, V> extends ViewSet<K, V, K> { ++ ++ protected KeySet(final SWMRHashTable<K, V> map) { ++ super(map); ++ } ++ ++ @Override ++ public Iterator<K> iterator() { ++ return new KeyIterator<>(this.map.getTableAcquire(), this.map); ++ } ++ ++ @Override ++ public void forEach(final Consumer<? super K> action) { ++ Validate.notNull(action, "Null action"); ++ ++ this.map.forEachKey(action); ++ } ++ ++ @Override ++ public boolean contains(final Object key) { ++ Validate.notNull(key, "Null key"); ++ ++ return this.map.containsKey(key); ++ } ++ ++ @Override ++ public boolean remove(final Object key) { ++ Validate.notNull(key, "Null key"); ++ ++ return this.map.remove(key) != null; ++ } ++ ++ @Override ++ public boolean retainAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ return this.map.removeIf((final K key, final V value) -> { ++ return !collection.contains(key); ++ }) != 0; ++ } ++ ++ @Override ++ public boolean removeIf(final Predicate<? super K> filter) { ++ Validate.notNull(filter, "Null filter"); ++ ++ return this.map.removeIf((final K key, final V value) -> { ++ return filter.test(key); ++ }) != 0; ++ } ++ ++ @Override ++ public String toString() { ++ return CollectionUtil.toString(this, "SWMRHashTableKeySet"); ++ } ++ } ++ ++ protected static final class ValueCollection<K, V> extends ViewSet<K, V, V> implements Collection<V> { ++ ++ protected ValueCollection(final SWMRHashTable<K, V> map) { ++ super(map); ++ } ++ ++ @Override ++ public Iterator<V> iterator() { ++ return new ValueIterator<>(this.map.getTableAcquire(), this.map); ++ } ++ ++ @Override ++ public void forEach(final Consumer<? super V> action) { ++ Validate.notNull(action, "Null action"); ++ ++ this.map.forEachValue(action); ++ } ++ ++ @Override ++ public boolean contains(final Object object) { ++ Validate.notNull(object, "Null object"); ++ ++ return this.map.containsValue(object); ++ } ++ ++ @Override ++ public boolean remove(final Object object) { ++ Validate.notNull(object, "Null object"); ++ ++ final Iterator<V> itr = this.iterator(); ++ while (itr.hasNext()) { ++ final V val = itr.next(); ++ if (val == object || val.equals(object)) { ++ itr.remove(); ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean removeIf(final Predicate<? super V> filter) { ++ Validate.notNull(filter, "Null filter"); ++ ++ return this.map.removeIf((final K key, final V value) -> { ++ return filter.test(value); ++ }) != 0; ++ } ++ ++ @Override ++ public boolean retainAll(final Collection<?> collection) { ++ Validate.notNull(collection, "Null collection"); ++ ++ return this.map.removeIf((final K key, final V value) -> { ++ return !collection.contains(value); ++ }) != 0; ++ } ++ ++ @Override ++ public String toString() { ++ return CollectionUtil.toString(this, "SWMRHashTableValues"); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..94fca3c9b31ca4e40688209e419e93320b0f7c34 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java +@@ -0,0 +1,672 @@ ++package ca.spottedleaf.concurrentutil.map; ++ ++import ca.spottedleaf.concurrentutil.util.ArrayUtil; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import io.papermc.paper.util.IntegerUtil; ++import java.lang.invoke.VarHandle; ++import java.util.Arrays; ++import java.util.function.Consumer; ++import java.util.function.LongConsumer; ++ ++// trimmed down version of SWMRHashTable ++public class SWMRLong2ObjectHashTable<V> { ++ ++ protected int size; ++ ++ protected TableEntry<V>[] table; ++ ++ protected final float loadFactor; ++ ++ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class); ++ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class); ++ ++ /* size */ ++ ++ protected final int getSizePlain() { ++ return (int)SIZE_HANDLE.get(this); ++ } ++ ++ protected final int getSizeOpaque() { ++ return (int)SIZE_HANDLE.getOpaque(this); ++ } ++ ++ protected final int getSizeAcquire() { ++ return (int)SIZE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setSizePlain(final int value) { ++ SIZE_HANDLE.set(this, value); ++ } ++ ++ protected final void setSizeOpaque(final int value) { ++ SIZE_HANDLE.setOpaque(this, value); ++ } ++ ++ protected final void setSizeRelease(final int value) { ++ SIZE_HANDLE.setRelease(this, value); ++ } ++ ++ /* table */ ++ ++ protected final TableEntry<V>[] getTablePlain() { ++ //noinspection unchecked ++ return (TableEntry<V>[])TABLE_HANDLE.get(this); ++ } ++ ++ protected final TableEntry<V>[] getTableAcquire() { ++ //noinspection unchecked ++ return (TableEntry<V>[])TABLE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setTablePlain(final TableEntry<V>[] table) { ++ TABLE_HANDLE.set(this, table); ++ } ++ ++ protected final void setTableRelease(final TableEntry<V>[] table) { ++ TABLE_HANDLE.setRelease(this, table); ++ } ++ ++ protected static final int DEFAULT_CAPACITY = 16; ++ protected static final float DEFAULT_LOAD_FACTOR = 0.75f; ++ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; ++ ++ /** ++ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. ++ */ ++ public SWMRLong2ObjectHashTable() { ++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); ++ } ++ ++ /** ++ * Constructs this map with the specified capacity and load factor of {@code 0.75f}. ++ * @param capacity specified initial capacity, > 0 ++ */ ++ public SWMRLong2ObjectHashTable(final int capacity) { ++ this(capacity, DEFAULT_LOAD_FACTOR); ++ } ++ ++ /** ++ * Constructs this map with the specified capacity and load factor. ++ * @param capacity specified capacity, > 0 ++ * @param loadFactor specified load factor, > 0 and finite ++ */ ++ public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) { ++ final int tableSize = getCapacityFor(capacity); ++ ++ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { ++ throw new IllegalArgumentException("Invalid load factor: " + loadFactor); ++ } ++ ++ //noinspection unchecked ++ final TableEntry<V>[] table = new TableEntry[tableSize]; ++ this.setTablePlain(table); ++ ++ if (tableSize == MAXIMUM_CAPACITY) { ++ this.threshold = -1; ++ } else { ++ this.threshold = getTargetCapacity(tableSize, loadFactor); ++ } ++ ++ this.loadFactor = loadFactor; ++ } ++ ++ /** ++ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and ++ * with a load factor of {@code 0.75f}. ++ * All of the specified map's entries are copied into this map. ++ * @param other The specified map. ++ */ ++ public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable<V> other) { ++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); ++ } ++ ++ /** ++ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and ++ * with a load factor of {@code 0.75f}. ++ * All of the specified map's entries are copied into this map. ++ * @param capacity specified capacity, > 0 ++ * @param other The specified map. ++ */ ++ public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable<V> other) { ++ this(capacity, DEFAULT_LOAD_FACTOR, other); ++ } ++ ++ /** ++ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and ++ * with the specified load factor. ++ * All of the specified map's entries are copied into this map. ++ * @param capacity specified capacity, > 0 ++ * @param loadFactor specified load factor, > 0 and finite ++ * @param other The specified map. ++ */ ++ public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable<V> other) { ++ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); ++ this.putAll(other); ++ } ++ ++ public final float getLoadFactor() { ++ return this.loadFactor; ++ } ++ ++ protected static int getCapacityFor(final int capacity) { ++ if (capacity <= 0) { ++ throw new IllegalArgumentException("Invalid capacity: " + capacity); ++ } ++ if (capacity >= MAXIMUM_CAPACITY) { ++ return MAXIMUM_CAPACITY; ++ } ++ return IntegerUtil.roundCeilLog2(capacity); ++ } ++ ++ /** Callers must still use acquire when reading the value of the entry. */ ++ protected final TableEntry<V> getEntryForOpaque(final long key) { ++ final int hash = SWMRLong2ObjectHashTable.getHash(key); ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { ++ if (key == curr.key) { ++ return curr; ++ } ++ } ++ ++ return null; ++ } ++ ++ protected final TableEntry<V> getEntryForPlain(final long key) { ++ final int hash = SWMRLong2ObjectHashTable.getHash(key); ++ final TableEntry<V>[] table = this.getTablePlain(); ++ ++ for (TableEntry<V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { ++ if (key == curr.key) { ++ return curr; ++ } ++ } ++ ++ return null; ++ } ++ ++ /* MT-Safe */ ++ ++ /** must be deterministic given a key */ ++ protected static int getHash(final long key) { ++ return (int)it.unimi.dsi.fastutil.HashCommon.mix(key); ++ } ++ ++ // rets -1 if capacity*loadFactor is too large ++ protected static int getTargetCapacity(final int capacity, final float loadFactor) { ++ final double ret = (double)capacity * (double)loadFactor; ++ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { ++ return -1; ++ } ++ ++ return (int)ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ /* Make no attempt to deal with concurrent modifications */ ++ if (!(obj instanceof SWMRLong2ObjectHashTable)) { ++ return false; ++ } ++ final SWMRLong2ObjectHashTable<?> other = (SWMRLong2ObjectHashTable<?>)obj; ++ ++ if (this.size() != other.size()) { ++ return false; ++ } ++ ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V value = curr.getValueAcquire(); ++ ++ final Object otherValue = other.get(curr.key); ++ if (otherValue == null || (value != otherValue && value.equals(otherValue))) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int hashCode() { ++ /* Make no attempt to deal with concurrent modifications */ ++ int hash = 0; ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ hash += curr.hashCode(); ++ } ++ } ++ ++ return hash; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public String toString() { ++ final StringBuilder builder = new StringBuilder(64); ++ builder.append("SingleWriterMultiReaderHashMap:{"); ++ ++ this.forEach((final long key, final V value) -> { ++ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); ++ }); ++ ++ return builder.append('}').toString(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public SWMRLong2ObjectHashTable<V> clone() { ++ return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public void forEach(final Consumer<? super SWMRLong2ObjectHashTable.TableEntry<V>> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ action.accept(curr); ++ } ++ } ++ } ++ ++ @FunctionalInterface ++ public static interface BiLongObjectConsumer<V> { ++ public void accept(final long key, final V value); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public void forEach(final BiLongObjectConsumer<? super V> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V value = curr.getValueAcquire(); ++ ++ action.accept(curr.key, value); ++ } ++ } ++ } ++ ++ /** ++ * Provides the specified consumer with all keys contained within this map. ++ * @param action The specified consumer. ++ */ ++ public void forEachKey(final LongConsumer action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ action.accept(curr.key); ++ } ++ } ++ } ++ ++ /** ++ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. ++ * @param action The specified consumer. ++ */ ++ public void forEachValue(final Consumer<? super V> action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry<V>[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final V value = curr.getValueAcquire(); ++ ++ action.accept(value); ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public V get(final long key) { ++ final TableEntry<V> entry = this.getEntryForOpaque(key); ++ return entry == null ? null : entry.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public boolean containsKey(final long key) { ++ // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics ++ return this.get(key) != null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public V getOrDefault(final long key, final V defaultValue) { ++ final TableEntry<V> entry = this.getEntryForOpaque(key); ++ ++ return entry == null ? defaultValue : entry.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int size() { ++ return this.getSizeAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public boolean isEmpty() { ++ return this.getSizeAcquire() == 0; ++ } ++ ++ /* Non-MT-Safe */ ++ ++ protected int threshold; ++ ++ protected final void checkResize(final int minCapacity) { ++ if (minCapacity <= this.threshold || this.threshold < 0) { ++ return; ++ } ++ ++ final TableEntry<V>[] table = this.getTablePlain(); ++ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); ++ if (newCapacity < 0) { ++ newCapacity = MAXIMUM_CAPACITY; ++ } ++ if (newCapacity <= table.length) { ++ if (newCapacity == MAXIMUM_CAPACITY) { ++ return; ++ } ++ newCapacity = table.length << 1; ++ } ++ ++ //noinspection unchecked ++ final TableEntry<V>[] newTable = new TableEntry[newCapacity]; ++ final int indexMask = newCapacity - 1; ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry<V> entry = table[i]; entry != null; entry = entry.getNextPlain()) { ++ final long key = entry.key; ++ final int hash = SWMRLong2ObjectHashTable.getHash(key); ++ final int index = hash & indexMask; ++ ++ /* we need to create a new entry since there could be reading threads */ ++ final TableEntry<V> insert = new TableEntry<>(key, entry.getValuePlain()); ++ ++ final TableEntry<V> prev = newTable[index]; ++ ++ newTable[index] = insert; ++ insert.setNextPlain(prev); ++ } ++ } ++ ++ if (newCapacity == MAXIMUM_CAPACITY) { ++ this.threshold = -1; /* No more resizing */ ++ } else { ++ this.threshold = getTargetCapacity(newCapacity, this.loadFactor); ++ } ++ this.setTableRelease(newTable); /* use release to publish entries in table */ ++ } ++ ++ protected final int addToSize(final int num) { ++ final int newSize = this.getSizePlain() + num; ++ ++ this.setSizeOpaque(newSize); ++ this.checkResize(newSize); ++ ++ return newSize; ++ } ++ ++ protected final int removeFromSize(final int num) { ++ final int newSize = this.getSizePlain() - num; ++ ++ this.setSizeOpaque(newSize); ++ ++ return newSize; ++ } ++ ++ protected final V put(final long key, final V value, final boolean onlyIfAbsent) { ++ final TableEntry<V>[] table = this.getTablePlain(); ++ final int hash = SWMRLong2ObjectHashTable.getHash(key); ++ final int index = hash & (table.length - 1); ++ ++ final TableEntry<V> head = table[index]; ++ if (head == null) { ++ final TableEntry<V> insert = new TableEntry<>(key, value); ++ ArrayUtil.setRelease(table, index, insert); ++ this.addToSize(1); ++ return null; ++ } ++ ++ for (TableEntry<V> curr = head;;) { ++ if (key == curr.key) { ++ if (onlyIfAbsent) { ++ return curr.getValuePlain(); ++ } ++ ++ final V currVal = curr.getValuePlain(); ++ curr.setValueRelease(value); ++ return currVal; ++ } ++ ++ final TableEntry<V> next = curr.getNextPlain(); ++ if (next != null) { ++ curr = next; ++ continue; ++ } ++ ++ final TableEntry<V> insert = new TableEntry<>(key, value); ++ ++ curr.setNextRelease(insert); ++ this.addToSize(1); ++ return null; ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public V put(final long key, final V value) { ++ Validate.notNull(value, "Null value"); ++ ++ return this.put(key, value, false); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public V putIfAbsent(final long key, final V value) { ++ Validate.notNull(value, "Null value"); ++ ++ return this.put(key, value, true); ++ } ++ ++ protected final V remove(final long key, final int hash) { ++ final TableEntry<V>[] table = this.getTablePlain(); ++ final int index = (table.length - 1) & hash; ++ ++ final TableEntry<V> head = table[index]; ++ if (head == null) { ++ return null; ++ } ++ ++ if (head.key == key) { ++ ArrayUtil.setRelease(table, index, head.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return head.getValuePlain(); ++ } ++ ++ for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { ++ if (key == curr.key) { ++ prev.setNextRelease(curr.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return curr.getValuePlain(); ++ } ++ } ++ ++ return null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public V remove(final long key) { ++ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key)); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public void putAll(final SWMRLong2ObjectHashTable<? extends V> map) { ++ Validate.notNull(map, "Null map"); ++ ++ final int size = map.size(); ++ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ ++ map.forEach(this::put); ++ } ++ ++ /** ++ * {@inheritDoc} ++ * <p> ++ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself ++ * is release ordered, that is, after the clear operation is performed a release fence is performed. ++ * </p> ++ */ ++ public void clear() { ++ Arrays.fill(this.getTablePlain(), null); ++ this.setSizeRelease(0); ++ } ++ ++ public static final class TableEntry<V> { ++ ++ protected final long key; ++ protected V value; ++ ++ protected TableEntry<V> next; ++ ++ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); ++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); ++ ++ /* value */ ++ ++ protected final V getValuePlain() { ++ //noinspection unchecked ++ return (V)VALUE_HANDLE.get(this); ++ } ++ ++ protected final V getValueAcquire() { ++ //noinspection unchecked ++ return (V)VALUE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setValueRelease(final V to) { ++ VALUE_HANDLE.setRelease(this, to); ++ } ++ ++ /* next */ ++ ++ protected final TableEntry<V> getNextPlain() { ++ //noinspection unchecked ++ return (TableEntry<V>)NEXT_HANDLE.get(this); ++ } ++ ++ protected final TableEntry<V> getNextOpaque() { ++ //noinspection unchecked ++ return (TableEntry<V>)NEXT_HANDLE.getOpaque(this); ++ } ++ ++ protected final void setNextPlain(final TableEntry<V> next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ protected final void setNextRelease(final TableEntry<V> next) { ++ NEXT_HANDLE.setRelease(this, next); ++ } ++ ++ protected TableEntry(final long key, final V value) { ++ this.key = key; ++ this.value = value; ++ } ++ ++ public long getKey() { ++ return this.key; ++ } ++ ++ public V getValue() { ++ return this.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public V setValue(final V value) { ++ if (value == null) { ++ throw new NullPointerException(); ++ } ++ ++ final V curr = this.getValuePlain(); ++ ++ this.setValueRelease(value); ++ return curr; ++ } ++ ++ protected static int hash(final long key, final Object value) { ++ return SWMRLong2ObjectHashTable.getHash(key) ^ (value == null ? 0 : value.hashCode()); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int hashCode() { ++ return hash(this.key, this.getValueAcquire()); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof TableEntry<?>)) { ++ return false; ++ } ++ final TableEntry<?> other = (TableEntry<?>)obj; ++ final long otherKey = other.getKey(); ++ final long thisKey = this.getKey(); ++ final Object otherValue = other.getValueAcquire(); ++ final V thisVal = this.getValueAcquire(); ++ return (thisKey == otherKey) && (thisVal == otherValue || thisVal.equals(otherValue)); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ebb1ab06165addb173fea4d295001fe37f4e79d3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java +@@ -0,0 +1,816 @@ ++package ca.spottedleaf.concurrentutil.util; ++ ++import java.lang.invoke.VarHandle; ++ ++public final class ArrayUtil { ++ ++ public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class); ++ ++ public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class); ++ ++ public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class); ++ ++ public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class); ++ ++ public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class); ++ ++ public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class); ++ ++ private ArrayUtil() { ++ throw new RuntimeException(); ++ } ++ ++ /* byte array */ ++ ++ public static byte getPlain(final byte[] array, final int index) { ++ return (byte)BYTE_ARRAY_HANDLE.get(array, index); ++ } ++ ++ public static byte getOpaque(final byte[] array, final int index) { ++ return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index); ++ } ++ ++ public static byte getAcquire(final byte[] array, final int index) { ++ return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index); ++ } ++ ++ public static byte getVolatile(final byte[] array, final int index) { ++ return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index); ++ } ++ ++ public static void setPlain(final byte[] array, final int index, final byte value) { ++ BYTE_ARRAY_HANDLE.set(array, index, value); ++ } ++ ++ public static void setOpaque(final byte[] array, final int index, final byte value) { ++ BYTE_ARRAY_HANDLE.setOpaque(array, index, value); ++ } ++ ++ public static void setRelease(final byte[] array, final int index, final byte value) { ++ BYTE_ARRAY_HANDLE.setRelease(array, index, value); ++ } ++ ++ public static void setVolatile(final byte[] array, final int index, final byte value) { ++ BYTE_ARRAY_HANDLE.setVolatile(array, index, value); ++ } ++ ++ public static void setVolatileContended(final byte[] array, final int index, final byte param) { ++ int failures = 0; ++ ++ for (byte curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return; ++ } ++ } ++ } ++ ++ public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) { ++ return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) { ++ return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param); ++ } ++ ++ public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) { ++ return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); ++ } ++ ++ public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) { ++ return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); ++ } ++ ++ public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) { ++ return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); ++ } ++ ++ public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) { ++ return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param); ++ } ++ ++ public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) { ++ return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) { ++ int failures = 0; ++ ++ for (byte curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) { ++ int failures = 0; ++ ++ for (byte curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) { ++ int failures = 0; ++ ++ for (byte curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) { ++ int failures = 0; ++ ++ for (byte curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) { ++ int failures = 0; ++ ++ for (byte curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return curr; ++ } ++ } ++ } ++ ++ /* short array */ ++ ++ public static short getPlain(final short[] array, final int index) { ++ return (short)SHORT_ARRAY_HANDLE.get(array, index); ++ } ++ ++ public static short getOpaque(final short[] array, final int index) { ++ return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index); ++ } ++ ++ public static short getAcquire(final short[] array, final int index) { ++ return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index); ++ } ++ ++ public static short getVolatile(final short[] array, final int index) { ++ return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index); ++ } ++ ++ public static void setPlain(final short[] array, final int index, final short value) { ++ SHORT_ARRAY_HANDLE.set(array, index, value); ++ } ++ ++ public static void setOpaque(final short[] array, final int index, final short value) { ++ SHORT_ARRAY_HANDLE.setOpaque(array, index, value); ++ } ++ ++ public static void setRelease(final short[] array, final int index, final short value) { ++ SHORT_ARRAY_HANDLE.setRelease(array, index, value); ++ } ++ ++ public static void setVolatile(final short[] array, final int index, final short value) { ++ SHORT_ARRAY_HANDLE.setVolatile(array, index, value); ++ } ++ ++ public static void setVolatileContended(final short[] array, final int index, final short param) { ++ int failures = 0; ++ ++ for (short curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return; ++ } ++ } ++ } ++ ++ public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) { ++ return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static short getAndAddVolatile(final short[] array, final int index, final short param) { ++ return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param); ++ } ++ ++ public static short getAndAndVolatile(final short[] array, final int index, final short param) { ++ return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); ++ } ++ ++ public static short getAndOrVolatile(final short[] array, final int index, final short param) { ++ return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); ++ } ++ ++ public static short getAndXorVolatile(final short[] array, final int index, final short param) { ++ return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); ++ } ++ ++ public static short getAndSetVolatile(final short[] array, final int index, final short param) { ++ return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param); ++ } ++ ++ public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) { ++ return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static short getAndAddVolatileContended(final short[] array, final int index, final short param) { ++ int failures = 0; ++ ++ for (short curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static short getAndAndVolatileContended(final short[] array, final int index, final short param) { ++ int failures = 0; ++ ++ for (short curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static short getAndOrVolatileContended(final short[] array, final int index, final short param) { ++ int failures = 0; ++ ++ for (short curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static short getAndXorVolatileContended(final short[] array, final int index, final short param) { ++ int failures = 0; ++ ++ for (short curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static short getAndSetVolatileContended(final short[] array, final int index, final short param) { ++ int failures = 0; ++ ++ for (short curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return curr; ++ } ++ } ++ } ++ ++ /* int array */ ++ ++ public static int getPlain(final int[] array, final int index) { ++ return (int)INT_ARRAY_HANDLE.get(array, index); ++ } ++ ++ public static int getOpaque(final int[] array, final int index) { ++ return (int)INT_ARRAY_HANDLE.getOpaque(array, index); ++ } ++ ++ public static int getAcquire(final int[] array, final int index) { ++ return (int)INT_ARRAY_HANDLE.getAcquire(array, index); ++ } ++ ++ public static int getVolatile(final int[] array, final int index) { ++ return (int)INT_ARRAY_HANDLE.getVolatile(array, index); ++ } ++ ++ public static void setPlain(final int[] array, final int index, final int value) { ++ INT_ARRAY_HANDLE.set(array, index, value); ++ } ++ ++ public static void setOpaque(final int[] array, final int index, final int value) { ++ INT_ARRAY_HANDLE.setOpaque(array, index, value); ++ } ++ ++ public static void setRelease(final int[] array, final int index, final int value) { ++ INT_ARRAY_HANDLE.setRelease(array, index, value); ++ } ++ ++ public static void setVolatile(final int[] array, final int index, final int value) { ++ INT_ARRAY_HANDLE.setVolatile(array, index, value); ++ } ++ ++ public static void setVolatileContended(final int[] array, final int index, final int param) { ++ int failures = 0; ++ ++ for (int curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return; ++ } ++ } ++ } ++ ++ public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) { ++ return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static int getAndAddVolatile(final int[] array, final int index, final int param) { ++ return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param); ++ } ++ ++ public static int getAndAndVolatile(final int[] array, final int index, final int param) { ++ return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); ++ } ++ ++ public static int getAndOrVolatile(final int[] array, final int index, final int param) { ++ return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); ++ } ++ ++ public static int getAndXorVolatile(final int[] array, final int index, final int param) { ++ return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); ++ } ++ ++ public static int getAndSetVolatile(final int[] array, final int index, final int param) { ++ return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param); ++ } ++ ++ public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) { ++ return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static int getAndAddVolatileContended(final int[] array, final int index, final int param) { ++ int failures = 0; ++ ++ for (int curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static int getAndAndVolatileContended(final int[] array, final int index, final int param) { ++ int failures = 0; ++ ++ for (int curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static int getAndOrVolatileContended(final int[] array, final int index, final int param) { ++ int failures = 0; ++ ++ for (int curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static int getAndXorVolatileContended(final int[] array, final int index, final int param) { ++ int failures = 0; ++ ++ for (int curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static int getAndSetVolatileContended(final int[] array, final int index, final int param) { ++ int failures = 0; ++ ++ for (int curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return curr; ++ } ++ } ++ } ++ ++ /* long array */ ++ ++ public static long getPlain(final long[] array, final int index) { ++ return (long)LONG_ARRAY_HANDLE.get(array, index); ++ } ++ ++ public static long getOpaque(final long[] array, final int index) { ++ return (long)LONG_ARRAY_HANDLE.getOpaque(array, index); ++ } ++ ++ public static long getAcquire(final long[] array, final int index) { ++ return (long)LONG_ARRAY_HANDLE.getAcquire(array, index); ++ } ++ ++ public static long getVolatile(final long[] array, final int index) { ++ return (long)LONG_ARRAY_HANDLE.getVolatile(array, index); ++ } ++ ++ public static void setPlain(final long[] array, final int index, final long value) { ++ LONG_ARRAY_HANDLE.set(array, index, value); ++ } ++ ++ public static void setOpaque(final long[] array, final int index, final long value) { ++ LONG_ARRAY_HANDLE.setOpaque(array, index, value); ++ } ++ ++ public static void setRelease(final long[] array, final int index, final long value) { ++ LONG_ARRAY_HANDLE.setRelease(array, index, value); ++ } ++ ++ public static void setVolatile(final long[] array, final int index, final long value) { ++ LONG_ARRAY_HANDLE.setVolatile(array, index, value); ++ } ++ ++ public static void setVolatileContended(final long[] array, final int index, final long param) { ++ int failures = 0; ++ ++ for (long curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return; ++ } ++ } ++ } ++ ++ public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) { ++ return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static long getAndAddVolatile(final long[] array, final int index, final long param) { ++ return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param); ++ } ++ ++ public static long getAndAndVolatile(final long[] array, final int index, final long param) { ++ return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); ++ } ++ ++ public static long getAndOrVolatile(final long[] array, final int index, final long param) { ++ return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); ++ } ++ ++ public static long getAndXorVolatile(final long[] array, final int index, final long param) { ++ return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); ++ } ++ ++ public static long getAndSetVolatile(final long[] array, final int index, final long param) { ++ return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param); ++ } ++ ++ public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) { ++ return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static long getAndAddVolatileContended(final long[] array, final int index, final long param) { ++ int failures = 0; ++ ++ for (long curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static long getAndAndVolatileContended(final long[] array, final int index, final long param) { ++ int failures = 0; ++ ++ for (long curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static long getAndOrVolatileContended(final long[] array, final int index, final long param) { ++ int failures = 0; ++ ++ for (long curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static long getAndXorVolatileContended(final long[] array, final int index, final long param) { ++ int failures = 0; ++ ++ for (long curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static long getAndSetVolatileContended(final long[] array, final int index, final long param) { ++ int failures = 0; ++ ++ for (long curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return curr; ++ } ++ } ++ } ++ ++ /* boolean array */ ++ ++ public static boolean getPlain(final boolean[] array, final int index) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index); ++ } ++ ++ public static boolean getOpaque(final boolean[] array, final int index) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index); ++ } ++ ++ public static boolean getAcquire(final boolean[] array, final int index) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index); ++ } ++ ++ public static boolean getVolatile(final boolean[] array, final int index) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index); ++ } ++ ++ public static void setPlain(final boolean[] array, final int index, final boolean value) { ++ BOOLEAN_ARRAY_HANDLE.set(array, index, value); ++ } ++ ++ public static void setOpaque(final boolean[] array, final int index, final boolean value) { ++ BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value); ++ } ++ ++ public static void setRelease(final boolean[] array, final int index, final boolean value) { ++ BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value); ++ } ++ ++ public static void setVolatile(final boolean[] array, final int index, final boolean value) { ++ BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value); ++ } ++ ++ public static void setVolatileContended(final boolean[] array, final int index, final boolean param) { ++ int failures = 0; ++ ++ for (boolean curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return; ++ } ++ } ++ } ++ ++ public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); ++ } ++ ++ public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); ++ } ++ ++ public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param); ++ } ++ ++ public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) { ++ return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); ++ } ++ ++ public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) { ++ int failures = 0; ++ ++ for (boolean curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) { ++ int failures = 0; ++ ++ for (boolean curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) { ++ int failures = 0; ++ ++ for (boolean curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) { ++ return curr; ++ } ++ } ++ } ++ ++ public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) { ++ int failures = 0; ++ ++ for (boolean curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return curr; ++ } ++ } ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T getPlain(final T[] array, final int index) { ++ final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index); ++ return (T)ret; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T getOpaque(final T[] array, final int index) { ++ final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index); ++ return (T)ret; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T getAcquire(final T[] array, final int index) { ++ final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index); ++ return (T)ret; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T getVolatile(final T[] array, final int index) { ++ final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index); ++ return (T)ret; ++ } ++ ++ public static <T> void setPlain(final T[] array, final int index, final T value) { ++ OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value); ++ } ++ ++ public static <T> void setOpaque(final T[] array, final int index, final T value) { ++ OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value); ++ } ++ ++ public static <T> void setRelease(final T[] array, final int index, final T value) { ++ OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value); ++ } ++ ++ public static <T> void setVolatile(final T[] array, final int index, final T value) { ++ OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value); ++ } ++ ++ public static <T> void setVolatileContended(final T[] array, final int index, final T param) { ++ int failures = 0; ++ ++ for (T curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return; ++ } ++ } ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) { ++ final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); ++ return (T)ret; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T getAndSetVolatile(final T[] array, final int index, final T param) { ++ final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param); ++ return (T)ret; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static <T> T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) { ++ final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); ++ return (T)ret; ++ } ++ ++ public static <T> T getAndSetVolatileContended(final T[] array, final int index, final T param) { ++ int failures = 0; ++ ++ for (T curr = getVolatile(array, index);;++failures) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { ++ return curr; ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9420b9822de99d3a31224642452835b0c986f7b4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java +@@ -0,0 +1,31 @@ ++package ca.spottedleaf.concurrentutil.util; ++ ++import java.util.Collection; ++ ++public final class CollectionUtil { ++ ++ public static String toString(final Collection<?> collection, final String name) { ++ return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString(); ++ } ++ ++ public static StringBuilder toString(final Collection<?> collection, final String name, final StringBuilder builder) { ++ builder.append(name).append("{elements={"); ++ ++ boolean first = true; ++ ++ for (final Object element : collection) { ++ if (!first) { ++ builder.append(", "); ++ } ++ first = false; ++ ++ builder.append('"').append(element).append('"'); ++ } ++ ++ return builder.append("}}"); ++ } ++ ++ private CollectionUtil() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..23ae82e55696a7e2ff0e0f9609c0df6a48bb8d1d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java +@@ -0,0 +1,166 @@ ++package ca.spottedleaf.concurrentutil.util; ++ ++import java.lang.invoke.MethodHandles; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.locks.LockSupport; ++ ++public final class ConcurrentUtil { ++ ++ public static String genericToString(final Object object) { ++ return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString(); ++ } ++ ++ public static void rethrow(Throwable exception) { ++ rethrow0(exception); ++ } ++ ++ private static <T extends Throwable> void rethrow0(Throwable thr) throws T { ++ throw (T)thr; ++ } ++ ++ public static VarHandle getVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) { ++ try { ++ return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType); ++ } catch (final Exception ex) { ++ throw new RuntimeException(ex); // unreachable ++ } ++ } ++ ++ public static VarHandle getStaticVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) { ++ try { ++ return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType); ++ } catch (final Exception ex) { ++ throw new RuntimeException(ex); // unreachable ++ } ++ } ++ ++ /** ++ * Non-exponential backoff algorithm to use in lightly contended areas. ++ * @see ConcurrentUtil#exponentiallyBackoffSimple(long) ++ * @see ConcurrentUtil#exponentiallyBackoffComplex(long) ++ */ ++ public static void backoff() { ++ Thread.onSpinWait(); ++ } ++ ++ /** ++ * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be ++ * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}. ++ * @param counter The current counter. ++ * @return The counter plus 1. ++ * @see ConcurrentUtil#backoff() ++ * @see ConcurrentUtil#exponentiallyBackoffComplex(long) ++ */ ++ public static long exponentiallyBackoffSimple(final long counter) { ++ for (long i = 0; i < counter; ++i) { ++ backoff(); ++ } ++ return counter + 1L; ++ } ++ ++ /** ++ * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread ++ * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange. ++ * @param counter The current counter. ++ * @return The next (if any) step in the backoff logic. ++ * @see ConcurrentUtil#backoff() ++ * @see ConcurrentUtil#exponentiallyBackoffSimple(long) ++ */ ++ public static long exponentiallyBackoffComplex(final long counter) { ++ // TODO experimentally determine counters ++ if (counter < 100L) { ++ return exponentiallyBackoffSimple(counter); ++ } ++ if (counter < 1_200L) { ++ Thread.yield(); ++ LockSupport.parkNanos(1_000L); ++ return counter + 1L; ++ } ++ // scale 0.1ms (100us) per failure ++ Thread.yield(); ++ LockSupport.parkNanos(100_000L * counter); ++ return counter + 1; ++ } ++ ++ /** ++ * Simple exponential backoff that will linearly increase the time per failure, according to the scale. ++ * @param counter The current failure counter. ++ * @param scale Time per failure, in ns. ++ * @param max The maximum time to wait for, in ns. ++ * @return The next counter. ++ */ ++ public static long linearLongBackoff(long counter, final long scale, long max) { ++ counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow ++ max = Math.max(0, max); ++ ++ if (scale <= 0L) { ++ return counter; ++ } ++ ++ long time = scale * counter; ++ ++ if (time > max || time / scale != counter) { ++ time = max; ++ } ++ ++ boolean interrupted = Thread.interrupted(); ++ if (time > 1_000_000L) { // 1ms ++ Thread.yield(); ++ } ++ LockSupport.parkNanos(time); ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ return counter; ++ } ++ ++ /** ++ * Simple exponential backoff that will linearly increase the time per failure, according to the scale. ++ * @param counter The current failure counter. ++ * @param scale Time per failure, in ns. ++ * @param max The maximum time to wait for, in ns. ++ * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}. ++ * @return The next counter. ++ */ ++ public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) { ++ counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow ++ max = Math.max(0, max); ++ ++ if (scale <= 0L) { ++ return counter; ++ } ++ ++ long time = scale * counter; ++ ++ // check overflow ++ if (time / scale != counter) { ++ // overflew ++ --counter; ++ time = max; ++ } else if (time > max) { ++ time = max; ++ } ++ ++ final long currTime = System.nanoTime(); ++ final long diff = deadline - currTime; ++ if (diff <= 0) { ++ return counter; ++ } ++ if (diff <= 1_500_000L) { // 1.5ms ++ time = 100_000L; // 100us ++ } else if (time > 1_000_000L) { // 1ms ++ Thread.yield(); ++ } ++ ++ boolean interrupted = Thread.interrupted(); ++ LockSupport.parkNanos(time); ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ return counter; ++ } ++ ++ public static VarHandle getArrayHandle(final Class<?> type) { ++ return MethodHandles.arrayElementVarHandle(type); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java +new file mode 100644 +index 0000000000000000000000000000000000000000..382177d0d162fa3139c9078a873ce2504a2b17b2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.concurrentutil.util; ++ ++public final class Validate { ++ ++ public static <T> T notNull(final T obj) { ++ if (obj == null) { ++ throw new NullPointerException(); ++ } ++ return obj; ++ } ++ ++ public static <T> T notNull(final T obj, final String msgIfNull) { ++ if (obj == null) { ++ throw new NullPointerException(msgIfNull); ++ } ++ return obj; ++ } ++ ++ public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) { ++ if (off < 0 || len < 0 || (arrayLength - off) < len) { ++ throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength); ++ } ++ } ++ ++ private Validate() { ++ throw new RuntimeException(); ++ } ++} diff --git a/patches/server/0008-CB-fixes.patch b/patches/server/0008-CB-fixes.patch new file mode 100644 index 0000000000..285cc0ff21 --- /dev/null +++ b/patches/server/0008-CB-fixes.patch @@ -0,0 +1,146 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic <[email protected]> +Date: Fri, 25 Feb 2022 07:14:48 -0800 +Subject: [PATCH] CB fixes + +* Missing Level -> LevelStem generic in StructureCheck + Need to use the right for injectDatafixingContext (Spottedleaf) + +* Removed incorrect parent perm for `minecraft.debugstick.always` (Machine_Maker) + +* Fixed method signature of Marker#addPassenger (Machine_Maker) + +* Removed unneeded UOE in CustomWorldChunkManager (extends BiomeSource) (Machine_Maker) + +* Honor Server#getLootTable method contract (Machine_Maker) + +Co-authored-by: Spottedleaf <[email protected]> + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index a696a0d168987aaa4e59c471a23eeb48d683c1b2..9d11fcb3df12182ae00ce73f7e30091fd199a341 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -303,7 +303,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + long l = minecraftserver.getWorldData().worldGenOptions().seed(); + +- this.structureCheck = new StructureCheck(this.chunkSource.chunkScanner(), this.registryAccess(), minecraftserver.getStructureManager(), resourcekey, chunkgenerator, this.chunkSource.randomState(), this, chunkgenerator.getBiomeSource(), l, datafixer); ++ this.structureCheck = new StructureCheck(this.chunkSource.chunkScanner(), this.registryAccess(), minecraftserver.getStructureManager(), this.getTypeKey(), chunkgenerator, this.chunkSource.randomState(), this, chunkgenerator.getBiomeSource(), l, datafixer); // Paper - Fix missing CB diff + this.structureManager = new StructureManager(this, this.serverLevelData.worldGenOptions(), this.structureCheck); // CraftBukkit + if ((this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END)) || env == org.bukkit.World.Environment.THE_END) { // CraftBukkit - Allow to create EnderDragonBattle in default and custom END + this.dragonFight = new EndDragonFight(this, this.serverLevelData.worldGenOptions().seed(), this.serverLevelData.endDragonFightData()); // CraftBukkit +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +index 0dc7f88877020bddd5a84db51d349f52b673048e..aae73586265593ee7830fb8dd5c2e3d7560057f0 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +@@ -40,7 +40,7 @@ public class StructureCheck { + private final ChunkScanAccess storageAccess; + private final RegistryAccess registryAccess; + private final StructureTemplateManager structureTemplateManager; +- private final ResourceKey<Level> dimension; ++ private final ResourceKey<net.minecraft.world.level.dimension.LevelStem> dimension; // Paper - fix missing CB diff + private final ChunkGenerator chunkGenerator; + private final RandomState randomState; + private final LevelHeightAccessor heightAccessor; +@@ -54,7 +54,7 @@ public class StructureCheck { + ChunkScanAccess chunkIoWorker, + RegistryAccess registryManager, + StructureTemplateManager structureTemplateManager, +- ResourceKey<Level> worldKey, ++ ResourceKey<net.minecraft.world.level.dimension.LevelStem> worldKey, // Paper - fix missing CB diff + ChunkGenerator chunkGenerator, + RandomState noiseConfig, + LevelHeightAccessor world, +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java b/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java +index 85c7f3027978b1d7d6c31b7ad21b3377cdda5925..e34deaf398dc6722c3128bdd6b9bc16da2d33bf7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java +@@ -187,4 +187,11 @@ public class CraftLootTable implements org.bukkit.loot.LootTable { + org.bukkit.loot.LootTable table = (org.bukkit.loot.LootTable) obj; + return table.getKey().equals(this.getKey()); + } ++ ++ // Paper start - satisfy equals/hashCode contract ++ @Override ++ public int hashCode() { ++ return java.util.Objects.hash(key); ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index f1302dfb68bf8e4e1f4d8b084ad81422f65eecc4..fe0f57dbeecc4b5a0c81863f33e41d11eb60943a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2514,7 +2514,13 @@ public final class CraftServer implements Server { + Preconditions.checkArgument(key != null, "NamespacedKey key cannot be null"); + + ReloadableServerRegistries.Holder registry = this.getServer().reloadableRegistries(); +- return new CraftLootTable(key, registry.getLootTable(CraftLootTable.bukkitKeyToMinecraft(key))); ++ // Paper start - honor method contract ++ final ResourceKey<net.minecraft.world.level.storage.loot.LootTable> lootTableKey = CraftLootTable.bukkitKeyToMinecraft(key); ++ final Optional<net.minecraft.world.level.storage.loot.LootTable> table = registry.get().lookup(Registries.LOOT_TABLE) ++ .flatMap(registryEntryLookup -> registryEntryLookup.get(lootTableKey)) ++ .map(net.minecraft.core.Holder::value); ++ return table.map(lootTable -> new CraftLootTable(key, lootTable)).orElse(null); ++ // Paper end + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 1e3ca7ca98abfd5be233a7eeb6dad201776d2d6a..9ec50bbb262b25fea157ae48e8395f5cd38f8906 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -123,6 +123,7 @@ public class Main { + this.acceptsAll(Main.asList("forceUpgrade"), "Whether to force a world upgrade"); + this.acceptsAll(Main.asList("eraseCache"), "Whether to force cache erase during world upgrade"); + this.acceptsAll(Main.asList("recreateRegionFiles"), "Whether to recreate region files during world upgrade"); ++ this.accepts("safeMode", "Loads level with vanilla datapack only"); // Paper + this.acceptsAll(Main.asList("nogui"), "Disables the graphical console"); + + this.acceptsAll(Main.asList("nojline"), "Disables jline and emulates the vanilla console"); +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index 905adf97c0d1f0d1c774a6835a5dffcfea884e58..c017ce2ca1bc535795c958a2e509af2adf88efa9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -26,6 +26,7 @@ import org.bukkit.scheduler.BukkitWorker; + + /** + * The fundamental concepts for this implementation: ++ * <ul> + * <li>Main thread owns {@link #head} and {@link #currentTick}, but it may be read from any thread</li> + * <li>Main thread exclusively controls {@link #temp} and {@link #pending}. + * They are never to be accessed outside of the main thread; alternatives exist to prevent locking.</li> +@@ -41,6 +42,7 @@ import org.bukkit.scheduler.BukkitWorker; + * <li>Sync tasks are only to be removed from runners on the main thread when coupled with a removal from pending and temp.</li> + * <li>Most of the design in this scheduler relies on queuing special tasks to perform any data changes on the main thread. + * When executed from inside a synchronous method, the scheduler will be updated before next execution by virtue of the frequent {@link #parsePending()} calls.</li> ++ * </ul> + */ + public class CraftScheduler implements BukkitScheduler { + +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 4c3c9c0e01ba8eeb9bd3f31cd795445cf03f8278..e08d4a45e313ef1b9005ef00ee0185a188171207 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -244,7 +244,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + try { + nmsStack.applyComponents(new ItemParser(Commands.createValidationContext(MinecraftServer.getDefaultRegistryAccess())).parse(new StringReader(arguments)).components()); + } catch (CommandSyntaxException ex) { +- Logger.getLogger(CraftMagicNumbers.class.getName()).log(Level.SEVERE, null, ex); ++ com.mojang.logging.LogUtils.getClassLogger().error("Exception modifying ItemStack", new Throwable(ex)); // Paper - show stack trace + } + + stack.setItemMeta(CraftItemStack.getItemMeta(nmsStack)); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java +index 5ac25dab93fd4c9e9533c80d1ca3d93446d7a365..245ad120a36b6defca7e6889faae1ca5fc33d0c7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java +@@ -15,7 +15,7 @@ public final class CraftDefaultPermissions { + DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".nbt.place", "Gives the user the ability to place restricted blocks with NBT in creative", org.bukkit.permissions.PermissionDefault.OP, parent); + DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".nbt.copy", "Gives the user the ability to copy NBT in creative", org.bukkit.permissions.PermissionDefault.TRUE, parent); + DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".debugstick", "Gives the user the ability to use the debug stick in creative", org.bukkit.permissions.PermissionDefault.OP, parent); +- DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".debugstick.always", "Gives the user the ability to use the debug stick in all game modes", org.bukkit.permissions.PermissionDefault.FALSE, parent); ++ DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".debugstick.always", "Gives the user the ability to use the debug stick in all game modes", org.bukkit.permissions.PermissionDefault.FALSE/* , parent */); // Paper - should not have this parent, as it's not a "vanilla" utility + // Spigot end + parent.recalculatePermissibles(); + } diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch new file mode 100644 index 0000000000..d437efd43b --- /dev/null +++ b/patches/server/0009-MC-Utils.patch @@ -0,0 +1,8279 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Mon, 28 Mar 2016 20:55:47 -0400 +Subject: [PATCH] MC Utils + +== AT == +public net.minecraft.server.level.ServerChunkCache mainThread +public net.minecraft.server.level.ServerLevel chunkSource +public org.bukkit.craftbukkit.inventory.CraftItemStack handle +public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder; +public net.minecraft.server.level.ServerChunkCache mainThreadProcessor +public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor +public net.minecraft.world.level.chunk.LevelChunkSection states + +diff --git a/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4029dc68cf35d63aa70c4a76c35bf65a7fc6358f +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java +@@ -0,0 +1,68 @@ ++package com.destroystokyo.paper.util.concurrent; ++ ++import java.util.concurrent.atomic.AtomicLong; ++ ++/** ++ * copied from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/lock/WeakSeqLock.java ++ * @author Spottedleaf ++ */ ++public final class WeakSeqLock { ++ // TODO when the switch to J11 is made, nuke this class from orbit ++ ++ protected final AtomicLong lock = new AtomicLong(); ++ ++ public WeakSeqLock() { ++ //VarHandle.storeStoreFence(); // warn: usages must be checked to ensure this behaviour isn't needed ++ } ++ ++ public void acquireWrite() { ++ // must be release-type write ++ this.lock.lazySet(this.lock.get() + 1); ++ } ++ ++ public boolean canRead(final long read) { ++ return (read & 1) == 0; ++ } ++ ++ public boolean tryAcquireWrite() { ++ this.acquireWrite(); ++ return true; ++ } ++ ++ public void releaseWrite() { ++ // must be acquire-type write ++ final long lock = this.lock.get(); // volatile here acts as store-store ++ this.lock.lazySet(lock + 1); ++ } ++ ++ public void abortWrite() { ++ // must be acquire-type write ++ final long lock = this.lock.get(); // volatile here acts as store-store ++ this.lock.lazySet(lock ^ 1); ++ } ++ ++ public long acquireRead() { ++ int failures = 0; ++ long curr; ++ ++ for (curr = this.lock.get(); !this.canRead(curr); curr = this.lock.get()) { ++ // without j11, our only backoff is the yield() call... ++ ++ if (++failures > 5_000) { /* TODO determine a threshold */ ++ Thread.yield(); ++ } ++ /* Better waiting is beyond the scope of this lock; if it is needed the lock is being misused */ ++ } ++ ++ //VarHandle.loadLoadFence(); // volatile acts as the load-load barrier ++ return curr; ++ } ++ ++ public boolean tryReleaseRead(final long read) { ++ return this.lock.get() == read; // volatile acts as the load-load barrier ++ } ++ ++ public long getSequentialCounter() { ++ return this.lock.get(); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java +new file mode 100644 +index 0000000000000000000000000000000000000000..59868f37d14bbc0ece0836095cdad148778995e6 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java +@@ -0,0 +1,162 @@ ++package com.destroystokyo.paper.util.map; ++ ++import com.destroystokyo.paper.util.concurrent.WeakSeqLock; ++import it.unimi.dsi.fastutil.longs.Long2IntMap; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ObjectIterator; ++ ++/** ++ * @author Spottedleaf ++ */ ++public class QueuedChangesMapLong2Int { ++ ++ protected final Long2IntOpenHashMap updatingMap; ++ protected final Long2IntOpenHashMap visibleMap; ++ protected final Long2IntOpenHashMap queuedPuts; ++ protected final LongOpenHashSet queuedRemove; ++ ++ protected int queuedDefaultReturnValue; ++ ++ // we use a seqlock as writes are not common. ++ protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock(); ++ ++ public QueuedChangesMapLong2Int() { ++ this(16, 0.75f); ++ } ++ ++ public QueuedChangesMapLong2Int(final int capacity, final float loadFactor) { ++ this.updatingMap = new Long2IntOpenHashMap(capacity, loadFactor); ++ this.visibleMap = new Long2IntOpenHashMap(capacity, loadFactor); ++ this.queuedPuts = new Long2IntOpenHashMap(); ++ this.queuedRemove = new LongOpenHashSet(); ++ } ++ ++ public void queueDefaultReturnValue(final int dfl) { ++ this.queuedDefaultReturnValue = dfl; ++ this.updatingMap.defaultReturnValue(dfl); ++ } ++ ++ public int queueUpdate(final long k, final int v) { ++ this.queuedRemove.remove(k); ++ this.queuedPuts.put(k, v); ++ ++ return this.updatingMap.put(k, v); ++ } ++ ++ public int queueRemove(final long k) { ++ this.queuedPuts.remove(k); ++ this.queuedRemove.add(k); ++ ++ return this.updatingMap.remove(k); ++ } ++ ++ public int getUpdating(final long k) { ++ return this.updatingMap.get(k); ++ } ++ ++ public int getVisible(final long k) { ++ return this.visibleMap.get(k); ++ } ++ ++ public int getVisibleAsync(final long k) { ++ long readlock; ++ int ret = 0; ++ ++ do { ++ readlock = this.updatingMapSeqLock.acquireRead(); ++ try { ++ ret = this.visibleMap.get(k); ++ } catch (final Throwable thr) { ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ // ignore... ++ continue; ++ } ++ ++ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); ++ ++ return ret; ++ } ++ ++ public boolean performUpdates() { ++ this.updatingMapSeqLock.acquireWrite(); ++ this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue); ++ this.updatingMapSeqLock.releaseWrite(); ++ ++ if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) { ++ return false; ++ } ++ ++ // update puts ++ final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator(); ++ while (iterator0.hasNext()) { ++ final Long2IntMap.Entry entry = iterator0.next(); ++ final long key = entry.getLongKey(); ++ final int val = entry.getIntValue(); ++ ++ this.updatingMapSeqLock.acquireWrite(); ++ try { ++ this.visibleMap.put(key, val); ++ } finally { ++ this.updatingMapSeqLock.releaseWrite(); ++ } ++ } ++ ++ this.queuedPuts.clear(); ++ ++ final LongIterator iterator1 = this.queuedRemove.iterator(); ++ while (iterator1.hasNext()) { ++ final long key = iterator1.nextLong(); ++ ++ this.updatingMapSeqLock.acquireWrite(); ++ try { ++ this.visibleMap.remove(key); ++ } finally { ++ this.updatingMapSeqLock.releaseWrite(); ++ } ++ } ++ ++ this.queuedRemove.clear(); ++ ++ return true; ++ } ++ ++ public boolean performUpdatesLockMap() { ++ this.updatingMapSeqLock.acquireWrite(); ++ try { ++ this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue); ++ ++ if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) { ++ return false; ++ } ++ ++ // update puts ++ final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator(); ++ while (iterator0.hasNext()) { ++ final Long2IntMap.Entry entry = iterator0.next(); ++ final long key = entry.getLongKey(); ++ final int val = entry.getIntValue(); ++ ++ this.visibleMap.put(key, val); ++ } ++ ++ this.queuedPuts.clear(); ++ ++ final LongIterator iterator1 = this.queuedRemove.iterator(); ++ while (iterator1.hasNext()) { ++ final long key = iterator1.nextLong(); ++ ++ this.visibleMap.remove(key); ++ } ++ ++ this.queuedRemove.clear(); ++ ++ return true; ++ } finally { ++ this.updatingMapSeqLock.releaseWrite(); ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7bab31a312463cc963d9621cdc543a281459bd32 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java +@@ -0,0 +1,202 @@ ++package com.destroystokyo.paper.util.map; ++ ++import com.destroystokyo.paper.util.concurrent.WeakSeqLock; ++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.List; ++ ++/** ++ * @author Spottedleaf ++ */ ++public class QueuedChangesMapLong2Object<V> { ++ ++ protected static final Object REMOVED = new Object(); ++ ++ protected final Long2ObjectLinkedOpenHashMap<V> updatingMap; ++ protected final Long2ObjectLinkedOpenHashMap<V> visibleMap; ++ protected final Long2ObjectLinkedOpenHashMap<Object> queuedChanges; ++ ++ // we use a seqlock as writes are not common. ++ protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock(); ++ ++ public QueuedChangesMapLong2Object() { ++ this(16, 0.75f); // dfl for fastutil ++ } ++ ++ public QueuedChangesMapLong2Object(final int capacity, final float loadFactor) { ++ this.updatingMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor); ++ this.visibleMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor); ++ this.queuedChanges = new Long2ObjectLinkedOpenHashMap<>(); ++ } ++ ++ public V queueUpdate(final long k, final V value) { ++ this.queuedChanges.put(k, value); ++ return this.updatingMap.put(k, value); ++ } ++ ++ public V queueRemove(final long k) { ++ this.queuedChanges.put(k, REMOVED); ++ return this.updatingMap.remove(k); ++ } ++ ++ public V getUpdating(final long k) { ++ return this.updatingMap.get(k); ++ } ++ ++ public boolean updatingContainsKey(final long k) { ++ return this.updatingMap.containsKey(k); ++ } ++ ++ public V getVisible(final long k) { ++ return this.visibleMap.get(k); ++ } ++ ++ public boolean visibleContainsKey(final long k) { ++ return this.visibleMap.containsKey(k); ++ } ++ ++ public V getVisibleAsync(final long k) { ++ long readlock; ++ V ret = null; ++ ++ do { ++ readlock = this.updatingMapSeqLock.acquireRead(); ++ ++ try { ++ ret = this.visibleMap.get(k); ++ } catch (final Throwable thr) { ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ // ignore... ++ continue; ++ } ++ ++ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); ++ ++ return ret; ++ } ++ ++ public boolean visibleContainsKeyAsync(final long k) { ++ long readlock; ++ boolean ret = false; ++ ++ do { ++ readlock = this.updatingMapSeqLock.acquireRead(); ++ ++ try { ++ ret = this.visibleMap.containsKey(k); ++ } catch (final Throwable thr) { ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ // ignore... ++ continue; ++ } ++ ++ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); ++ ++ return ret; ++ } ++ ++ public Long2ObjectLinkedOpenHashMap<V> getVisibleMap() { ++ return this.visibleMap; ++ } ++ ++ public Long2ObjectLinkedOpenHashMap<V> getUpdatingMap() { ++ return this.updatingMap; ++ } ++ ++ public int getVisibleSize() { ++ return this.visibleMap.size(); ++ } ++ ++ public int getVisibleSizeAsync() { ++ long readlock; ++ int ret; ++ ++ do { ++ readlock = this.updatingMapSeqLock.acquireRead(); ++ ret = this.visibleMap.size(); ++ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); ++ ++ return ret; ++ } ++ ++ // unlike mojang's impl this cannot be used async since it's not a view of an immutable map ++ public Collection<V> getUpdatingValues() { ++ return this.updatingMap.values(); ++ } ++ ++ public List<V> getUpdatingValuesCopy() { ++ return new ArrayList<>(this.updatingMap.values()); ++ } ++ ++ // unlike mojang's impl this cannot be used async since it's not a view of an immutable map ++ public Collection<V> getVisibleValues() { ++ return this.visibleMap.values(); ++ } ++ ++ public List<V> getVisibleValuesCopy() { ++ return new ArrayList<>(this.visibleMap.values()); ++ } ++ ++ public boolean performUpdates() { ++ if (this.queuedChanges.isEmpty()) { ++ return false; ++ } ++ ++ final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator(); ++ while (iterator.hasNext()) { ++ final Long2ObjectMap.Entry<Object> entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final Object val = entry.getValue(); ++ ++ this.updatingMapSeqLock.acquireWrite(); ++ try { ++ if (val == REMOVED) { ++ this.visibleMap.remove(key); ++ } else { ++ this.visibleMap.put(key, (V)val); ++ } ++ } finally { ++ this.updatingMapSeqLock.releaseWrite(); ++ } ++ } ++ ++ this.queuedChanges.clear(); ++ return true; ++ } ++ ++ public boolean performUpdatesLockMap() { ++ if (this.queuedChanges.isEmpty()) { ++ return false; ++ } ++ ++ final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator(); ++ ++ try { ++ this.updatingMapSeqLock.acquireWrite(); ++ ++ while (iterator.hasNext()) { ++ final Long2ObjectMap.Entry<Object> entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final Object val = entry.getValue(); ++ ++ if (val == REMOVED) { ++ this.visibleMap.remove(key); ++ } else { ++ this.visibleMap.put(key, (V)val); ++ } ++ } ++ } finally { ++ this.updatingMapSeqLock.releaseWrite(); ++ } ++ ++ this.queuedChanges.clear(); ++ return true; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..554f4d4e63c1431721989e6f502a32ccc53a8807 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java +@@ -0,0 +1,128 @@ ++package com.destroystokyo.paper.util.maplist; ++ ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++import net.minecraft.world.level.chunk.LevelChunk; ++ ++// list with O(1) remove & contains ++/** ++ * @author Spottedleaf ++ */ ++public final class ChunkList implements Iterable<LevelChunk> { ++ ++ protected final Long2IntOpenHashMap chunkToIndex = new Long2IntOpenHashMap(2, 0.8f); ++ { ++ this.chunkToIndex.defaultReturnValue(Integer.MIN_VALUE); ++ } ++ ++ protected static final LevelChunk[] EMPTY_LIST = new LevelChunk[0]; ++ ++ protected LevelChunk[] chunks = EMPTY_LIST; ++ protected int count; ++ ++ public int size() { ++ return this.count; ++ } ++ ++ public boolean contains(final LevelChunk chunk) { ++ return this.chunkToIndex.containsKey(chunk.coordinateKey); ++ } ++ ++ public boolean remove(final LevelChunk chunk) { ++ final int index = this.chunkToIndex.remove(chunk.coordinateKey); ++ if (index == Integer.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the entity at the end to this index ++ final int endIndex = --this.count; ++ final LevelChunk end = this.chunks[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.chunkToIndex.put(end.coordinateKey, index); // update index ++ } ++ this.chunks[index] = end; ++ this.chunks[endIndex] = null; ++ ++ return true; ++ } ++ ++ public boolean add(final LevelChunk chunk) { ++ final int count = this.count; ++ final int currIndex = this.chunkToIndex.putIfAbsent(chunk.coordinateKey, count); ++ ++ if (currIndex != Integer.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ LevelChunk[] list = this.chunks; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.chunks = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = chunk; ++ this.count = count + 1; ++ ++ return true; ++ } ++ ++ public LevelChunk getChecked(final int index) { ++ if (index < 0 || index >= this.count) { ++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); ++ } ++ return this.chunks[index]; ++ } ++ ++ public LevelChunk getUnchecked(final int index) { ++ return this.chunks[index]; ++ } ++ ++ public LevelChunk[] getRawData() { ++ return this.chunks; ++ } ++ ++ public void clear() { ++ this.chunkToIndex.clear(); ++ Arrays.fill(this.chunks, 0, this.count, null); ++ this.count = 0; ++ } ++ ++ @Override ++ public Iterator<LevelChunk> iterator() { ++ return new Iterator<LevelChunk>() { ++ ++ LevelChunk lastRet; ++ int current; ++ ++ @Override ++ public boolean hasNext() { ++ return this.current < ChunkList.this.count; ++ } ++ ++ @Override ++ public LevelChunk next() { ++ if (this.current >= ChunkList.this.count) { ++ throw new NoSuchElementException(); ++ } ++ return this.lastRet = ChunkList.this.chunks[this.current++]; ++ } ++ ++ @Override ++ public void remove() { ++ final LevelChunk lastRet = this.lastRet; ++ ++ if (lastRet == null) { ++ throw new IllegalStateException(); ++ } ++ this.lastRet = null; ++ ++ ChunkList.this.remove(lastRet); ++ --this.current; ++ } ++ }; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0133ea6feb1ab88f021f66855669f58367e7420b +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java +@@ -0,0 +1,128 @@ ++package com.destroystokyo.paper.util.maplist; ++ ++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; ++import net.minecraft.world.entity.Entity; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++ ++// list with O(1) remove & contains ++/** ++ * @author Spottedleaf ++ */ ++public final class EntityList implements Iterable<Entity> { ++ ++ protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f); ++ { ++ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE); ++ } ++ ++ protected static final Entity[] EMPTY_LIST = new Entity[0]; ++ ++ protected Entity[] entities = EMPTY_LIST; ++ protected int count; ++ ++ public int size() { ++ return this.count; ++ } ++ ++ public boolean contains(final Entity entity) { ++ return this.entityToIndex.containsKey(entity.getId()); ++ } ++ ++ public boolean remove(final Entity entity) { ++ final int index = this.entityToIndex.remove(entity.getId()); ++ if (index == Integer.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the entity at the end to this index ++ final int endIndex = --this.count; ++ final Entity end = this.entities[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.entityToIndex.put(end.getId(), index); // update index ++ } ++ this.entities[index] = end; ++ this.entities[endIndex] = null; ++ ++ return true; ++ } ++ ++ public boolean add(final Entity entity) { ++ final int count = this.count; ++ final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count); ++ ++ if (currIndex != Integer.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ Entity[] list = this.entities; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = entity; ++ this.count = count + 1; ++ ++ return true; ++ } ++ ++ public Entity getChecked(final int index) { ++ if (index < 0 || index >= this.count) { ++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); ++ } ++ return this.entities[index]; ++ } ++ ++ public Entity getUnchecked(final int index) { ++ return this.entities[index]; ++ } ++ ++ public Entity[] getRawData() { ++ return this.entities; ++ } ++ ++ public void clear() { ++ this.entityToIndex.clear(); ++ Arrays.fill(this.entities, 0, this.count, null); ++ this.count = 0; ++ } ++ ++ @Override ++ public Iterator<Entity> iterator() { ++ return new Iterator<Entity>() { ++ ++ Entity lastRet; ++ int current; ++ ++ @Override ++ public boolean hasNext() { ++ return this.current < EntityList.this.count; ++ } ++ ++ @Override ++ public Entity next() { ++ if (this.current >= EntityList.this.count) { ++ throw new NoSuchElementException(); ++ } ++ return this.lastRet = EntityList.this.entities[this.current++]; ++ } ++ ++ @Override ++ public void remove() { ++ final Entity lastRet = this.lastRet; ++ ++ if (lastRet == null) { ++ throw new IllegalStateException(); ++ } ++ this.lastRet = null; ++ ++ EntityList.this.remove(lastRet); ++ --this.current; ++ } ++ }; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..277cfd9d1e8fff5d9b5e534b75c3c5162d58b0b7 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java +@@ -0,0 +1,128 @@ ++package com.destroystokyo.paper.util.maplist; ++ ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap; ++import java.util.Arrays; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.GlobalPalette; ++ ++/** ++ * @author Spottedleaf ++ */ ++public final class IBlockDataList { ++ ++ static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY); ++ ++ // map of location -> (index | (location << 16) | (palette id << 32)) ++ private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f); ++ { ++ this.map.defaultReturnValue(Long.MAX_VALUE); ++ } ++ ++ private static final long[] EMPTY_LIST = new long[0]; ++ ++ private long[] byIndex = EMPTY_LIST; ++ private int size; ++ ++ public static int getLocationKey(final int x, final int y, final int z) { ++ return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4)); ++ } ++ ++ public static BlockState getBlockDataFromRaw(final long raw) { ++ return GLOBAL_PALETTE.valueFor((int)(raw >>> 32)); ++ } ++ ++ public static int getIndexFromRaw(final long raw) { ++ return (int)(raw & 0xFFFF); ++ } ++ ++ public static int getLocationFromRaw(final long raw) { ++ return (int)((raw >>> 16) & 0xFFFF); ++ } ++ ++ public static long getRawFromValues(final int index, final int location, final BlockState data) { ++ return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32); ++ } ++ ++ public static long setIndexRawValues(final long value, final int index) { ++ return value & ~(0xFFFF) | (index); ++ } ++ ++ public long add(final int x, final int y, final int z, final BlockState data) { ++ return this.add(getLocationKey(x, y, z), data); ++ } ++ ++ public long add(final int location, final BlockState data) { ++ final long curr = this.map.get((short)location); ++ ++ if (curr == Long.MAX_VALUE) { ++ final int index = this.size++; ++ final long raw = getRawFromValues(index, location, data); ++ this.map.put((short)location, raw); ++ ++ if (index >= this.byIndex.length) { ++ this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L)); ++ } ++ ++ this.byIndex[index] = raw; ++ return raw; ++ } else { ++ final int index = getIndexFromRaw(curr); ++ final long raw = this.byIndex[index] = getRawFromValues(index, location, data); ++ ++ this.map.put((short)location, raw); ++ ++ return raw; ++ } ++ } ++ ++ public long remove(final int x, final int y, final int z) { ++ return this.remove(getLocationKey(x, y, z)); ++ } ++ ++ public long remove(final int location) { ++ final long ret = this.map.remove((short)location); ++ final int index = getIndexFromRaw(ret); ++ if (ret == Long.MAX_VALUE) { ++ return ret; ++ } ++ ++ // move the entry at the end to this index ++ final int endIndex = --this.size; ++ final long end = this.byIndex[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index)); ++ } ++ this.byIndex[index] = end; ++ this.byIndex[endIndex] = 0L; ++ ++ return ret; ++ } ++ ++ public int size() { ++ return this.size; ++ } ++ ++ public long getRaw(final int index) { ++ return this.byIndex[index]; ++ } ++ ++ public int getLocation(final int index) { ++ return getLocationFromRaw(this.getRaw(index)); ++ } ++ ++ public BlockState getData(final int index) { ++ return getBlockDataFromRaw(this.getRaw(index)); ++ } ++ ++ public void clear() { ++ this.size = 0; ++ this.map.clear(); ++ } ++ ++ public LongIterator getRawIterator() { ++ return this.map.values().iterator(); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb3498ddffe1 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java +@@ -0,0 +1,125 @@ ++package com.destroystokyo.paper.util.maplist; ++ ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++ ++/** ++ * @author Spottedleaf ++ */ ++public final class ReferenceList<E> implements Iterable<E> { ++ ++ protected final Reference2IntOpenHashMap<E> referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f); ++ { ++ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE); ++ } ++ ++ protected static final Object[] EMPTY_LIST = new Object[0]; ++ ++ protected Object[] references = EMPTY_LIST; ++ protected int count; ++ ++ public int size() { ++ return this.count; ++ } ++ ++ public boolean contains(final E obj) { ++ return this.referenceToIndex.containsKey(obj); ++ } ++ ++ public boolean remove(final E obj) { ++ final int index = this.referenceToIndex.removeInt(obj); ++ if (index == Integer.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the object at the end to this index ++ final int endIndex = --this.count; ++ final E end = (E)this.references[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.referenceToIndex.put(end, index); // update index ++ } ++ this.references[index] = end; ++ this.references[endIndex] = null; ++ ++ return true; ++ } ++ ++ public boolean add(final E obj) { ++ final int count = this.count; ++ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count); ++ ++ if (currIndex != Integer.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ Object[] list = this.references; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = obj; ++ this.count = count + 1; ++ ++ return true; ++ } ++ ++ public E getChecked(final int index) { ++ if (index < 0 || index >= this.count) { ++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); ++ } ++ return (E)this.references[index]; ++ } ++ ++ public E getUnchecked(final int index) { ++ return (E)this.references[index]; ++ } ++ ++ public Object[] getRawData() { ++ return this.references; ++ } ++ ++ public void clear() { ++ this.referenceToIndex.clear(); ++ Arrays.fill(this.references, 0, this.count, null); ++ this.count = 0; ++ } ++ ++ @Override ++ public Iterator<E> iterator() { ++ return new Iterator<>() { ++ private E lastRet; ++ private int current; ++ ++ @Override ++ public boolean hasNext() { ++ return this.current < ReferenceList.this.count; ++ } ++ ++ @Override ++ public E next() { ++ if (this.current >= ReferenceList.this.count) { ++ throw new NoSuchElementException(); ++ } ++ return this.lastRet = (E)ReferenceList.this.references[this.current++]; ++ } ++ ++ @Override ++ public void remove() { ++ final E lastRet = this.lastRet; ++ ++ if (lastRet == null) { ++ throw new IllegalStateException(); ++ } ++ this.lastRet = null; ++ ++ ReferenceList.this.remove(lastRet); ++ --this.current; ++ } ++ }; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..41b9405d6759d865e0d14dd4f95163e9690e967d +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java +@@ -0,0 +1,453 @@ ++package com.destroystokyo.paper.util.misc; ++ ++import io.papermc.paper.util.IntegerUtil; ++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.level.ChunkPos; ++import javax.annotation.Nullable; ++import java.util.Iterator; ++ ++/** @author Spottedleaf */ ++public abstract class AreaMap<E> { ++ ++ /* Tested via https://gist.github.com/Spottedleaf/520419c6f41ef348fe9926ce674b7217 */ ++ ++ protected final Object2LongOpenHashMap<E> objectToLastCoordinate = new Object2LongOpenHashMap<>(); ++ protected final Object2IntOpenHashMap<E> objectToViewDistance = new Object2IntOpenHashMap<>(); ++ ++ { ++ this.objectToViewDistance.defaultReturnValue(-1); ++ this.objectToLastCoordinate.defaultReturnValue(Long.MIN_VALUE); ++ } ++ ++ // we use linked for better iteration. ++ // map of: coordinate to set of objects in coordinate ++ protected final Long2ObjectOpenHashMap<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); ++ protected final PooledLinkedHashSets<E> pooledHashSets; ++ ++ protected final ChangeCallback<E> addCallback; ++ protected final ChangeCallback<E> removeCallback; ++ protected final ChangeSourceCallback<E> changeSourceCallback; ++ ++ public AreaMap() { ++ this(new PooledLinkedHashSets<>()); ++ } ++ ++ // let users define a "global" or "shared" pooled sets if they wish ++ public AreaMap(final PooledLinkedHashSets<E> pooledHashSets) { ++ this(pooledHashSets, null, null); ++ } ++ ++ public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback) { ++ this(pooledHashSets, addCallback, removeCallback, null); ++ } ++ public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback, final ChangeSourceCallback<E> changeSourceCallback) { ++ this.pooledHashSets = pooledHashSets; ++ this.addCallback = addCallback; ++ this.removeCallback = removeCallback; ++ this.changeSourceCallback = changeSourceCallback; ++ } ++ ++ @Nullable ++ public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final long key) { ++ return this.areaMap.get(key); ++ } ++ ++ @Nullable ++ public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final ChunkPos chunkPos) { ++ return this.areaMap.get(MCUtil.getCoordinateKey(chunkPos)); ++ } ++ ++ @Nullable ++ public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final int chunkX, final int chunkZ) { ++ return this.areaMap.get(MCUtil.getCoordinateKey(chunkX, chunkZ)); ++ } ++ ++ // Long.MIN_VALUE indicates the object is not mapped ++ public final long getLastCoordinate(final E object) { ++ return this.objectToLastCoordinate.getOrDefault(object, Long.MIN_VALUE); ++ } ++ ++ // -1 indicates the object is not mapped ++ public final int getLastViewDistance(final E object) { ++ return this.objectToViewDistance.getOrDefault(object, -1); ++ } ++ ++ // returns the total number of mapped chunks ++ public final int size() { ++ return this.areaMap.size(); ++ } ++ ++ public final void addOrUpdate(final E object, final int chunkX, final int chunkZ, final int viewDistance) { ++ final int oldViewDistance = this.objectToViewDistance.put(object, viewDistance); ++ final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ final long oldPos = this.objectToLastCoordinate.put(object, newPos); ++ ++ if (oldViewDistance == -1) { ++ this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance); ++ this.addObjectCallback(object, chunkX, chunkZ, viewDistance); ++ } else { ++ this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance); ++ this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance); ++ } ++ //this.validate(object, viewDistance); ++ } ++ ++ public final boolean update(final E object, final int chunkX, final int chunkZ, final int viewDistance) { ++ final int oldViewDistance = this.objectToViewDistance.replace(object, viewDistance); ++ if (oldViewDistance == -1) { ++ return false; ++ } else { ++ final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ final long oldPos = this.objectToLastCoordinate.put(object, newPos); ++ this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance); ++ this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance); ++ } ++ //this.validate(object, viewDistance); ++ return true; ++ } ++ ++ // called after the distance map updates ++ protected void updateObjectCallback(final E Object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) { ++ if (newPosition != oldPosition && this.changeSourceCallback != null) { ++ this.changeSourceCallback.accept(Object, oldPosition, newPosition); ++ } ++ } ++ ++ public final boolean add(final E object, final int chunkX, final int chunkZ, final int viewDistance) { ++ final int oldViewDistance = this.objectToViewDistance.putIfAbsent(object, viewDistance); ++ if (oldViewDistance != -1) { ++ return false; ++ } ++ ++ final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ this.objectToLastCoordinate.put(object, newPos); ++ this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance); ++ this.addObjectCallback(object, chunkX, chunkZ, viewDistance); ++ ++ //this.validate(object, viewDistance); ++ ++ return true; ++ } ++ ++ // called after the distance map updates ++ protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {} ++ ++ public final boolean remove(final E object) { ++ final long position = this.objectToLastCoordinate.removeLong(object); ++ final int viewDistance = this.objectToViewDistance.removeInt(object); ++ ++ if (viewDistance == -1) { ++ return false; ++ } ++ ++ final int currentX = MCUtil.getCoordinateX(position); ++ final int currentZ = MCUtil.getCoordinateZ(position); ++ ++ this.removeObject(object, currentX, currentZ, currentX, currentZ, viewDistance); ++ this.removeObjectCallback(object, currentX, currentZ, viewDistance); ++ //this.validate(object, -1); ++ return true; ++ } ++ ++ // called after the distance map updates ++ protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {} ++ ++ protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getEmptySetFor(final E object); ++ ++ // expensive op, only for debug ++ protected void validate(final E object, final int viewDistance) { ++ int entiesGot = 0; ++ int expectedEntries = (2 * viewDistance + 1); ++ expectedEntries *= expectedEntries; ++ if (viewDistance < 0) { ++ expectedEntries = 0; ++ } ++ ++ final long currPosition = this.objectToLastCoordinate.getLong(object); ++ ++ final int centerX = MCUtil.getCoordinateX(currPosition); ++ final int centerZ = MCUtil.getCoordinateZ(currPosition); ++ ++ for (Iterator<Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>>> iterator = this.areaMap.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ ++ final Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> map = entry.getValue(); ++ ++ if (map.referenceCount == 0) { ++ throw new IllegalStateException("Invalid map"); ++ } ++ ++ if (map.contains(object)) { ++ ++entiesGot; ++ ++ final int chunkX = MCUtil.getCoordinateX(key); ++ final int chunkZ = MCUtil.getCoordinateZ(key); ++ ++ final int dist = Math.max(IntegerUtil.branchlessAbs(chunkX - centerX), IntegerUtil.branchlessAbs(chunkZ - centerZ)); ++ ++ if (dist > viewDistance) { ++ throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist); ++ } ++ } ++ } ++ ++ if (entiesGot != expectedEntries) { ++ throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot); ++ } ++ } ++ ++ private void addObjectTo(final E object, final int chunkX, final int chunkZ, final int currChunkX, ++ final int currChunkZ, final int prevChunkX, final int prevChunkZ) { ++ final long key = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ ++ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> empty = this.getEmptySetFor(object); ++ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.putIfAbsent(key, empty); ++ ++ if (current != null) { ++ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWith(current, object); ++ if (next == current) { ++ throw new IllegalStateException("Expected different map: got " + next.toString()); ++ } ++ this.areaMap.put(key, next); ++ ++ current = next; ++ // fall through to callback ++ } else { ++ current = empty; ++ } ++ ++ if (this.addCallback != null) { ++ try { ++ this.addCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, current); ++ } catch (final Throwable ex) { ++ if (ex instanceof ThreadDeath) { ++ throw (ThreadDeath)ex; ++ } ++ MinecraftServer.LOGGER.error("Add callback for map threw exception ", ex); ++ } ++ } ++ } ++ ++ private void removeObjectFrom(final E object, final int chunkX, final int chunkZ, final int currChunkX, ++ final int currChunkZ, final int prevChunkX, final int prevChunkZ) { ++ final long key = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ ++ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.get(key); ++ ++ if (current == null) { ++ throw new IllegalStateException("Current map may not be null for " + object + ", (" + chunkX + "," + chunkZ + ")"); ++ } ++ ++ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWithout(current, object); ++ ++ if (next == current) { ++ throw new IllegalStateException("Current map [" + next.toString() + "] should have contained " + object + ", (" + chunkX + "," + chunkZ + ")"); ++ } ++ ++ if (next != null) { ++ this.areaMap.put(key, next); ++ } else { ++ this.areaMap.remove(key); ++ } ++ ++ if (this.removeCallback != null) { ++ try { ++ this.removeCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, next); ++ } catch (final Throwable ex) { ++ if (ex instanceof ThreadDeath) { ++ throw (ThreadDeath)ex; ++ } ++ MinecraftServer.LOGGER.error("Remove callback for map threw exception ", ex); ++ } ++ } ++ } ++ ++ private void addObject(final E object, final int chunkX, final int chunkZ, final int prevChunkX, final int prevChunkZ, final int viewDistance) { ++ final int maxX = chunkX + viewDistance; ++ final int maxZ = chunkZ + viewDistance; ++ final int minX = chunkX - viewDistance; ++ final int minZ = chunkZ - viewDistance; ++ for (int x = minX; x <= maxX; ++x) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ this.addObjectTo(object, x, z, chunkX, chunkZ, prevChunkX, prevChunkZ); ++ } ++ } ++ } ++ ++ private void removeObject(final E object, final int chunkX, final int chunkZ, final int currentChunkX, final int currentChunkZ, final int viewDistance) { ++ final int maxX = chunkX + viewDistance; ++ final int maxZ = chunkZ + viewDistance; ++ final int minX = chunkX - viewDistance; ++ final int minZ = chunkZ - viewDistance; ++ for (int x = minX; x <= maxX; ++x) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ this.removeObjectFrom(object, x, z, currentChunkX, currentChunkZ, chunkX, chunkZ); ++ } ++ } ++ } ++ ++ /* math sign function except 0 returns 1 */ ++ protected static int sign(int val) { ++ return 1 | (val >> (Integer.SIZE - 1)); ++ } ++ ++ private void updateObject(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) { ++ final int toX = MCUtil.getCoordinateX(newPosition); ++ final int toZ = MCUtil.getCoordinateZ(newPosition); ++ final int fromX = MCUtil.getCoordinateX(oldPosition); ++ final int fromZ = MCUtil.getCoordinateZ(oldPosition); ++ ++ final int dx = toX - fromX; ++ final int dz = toZ - fromZ; ++ ++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX); ++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); ++ ++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { ++ // teleported? ++ this.removeObject(object, fromX, fromZ, fromX, fromZ, oldViewDistance); ++ this.addObject(object, toX, toZ, fromX, fromZ, newViewDistance); ++ return; ++ } ++ ++ if (oldViewDistance != newViewDistance) { ++ // remove loop ++ ++ final int oldMinX = fromX - oldViewDistance; ++ final int oldMinZ = fromZ - oldViewDistance; ++ final int oldMaxX = fromX + oldViewDistance; ++ final int oldMaxZ = fromZ + oldViewDistance; ++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) { ++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { ++ ++ // only remove if we're outside the new view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { ++ this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ); ++ } ++ } ++ } ++ ++ // add loop ++ ++ final int newMinX = toX - newViewDistance; ++ final int newMinZ = toZ - newViewDistance; ++ final int newMaxX = toX + newViewDistance; ++ final int newMaxZ = toZ + newViewDistance; ++ for (int currX = newMinX; currX <= newMaxX; ++currX) { ++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { ++ ++ // only add if we're outside the old view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { ++ this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ); ++ } ++ } ++ } ++ ++ return; ++ } ++ ++ // x axis is width ++ // z axis is height ++ // right refers to the x axis of where we moved ++ // top refers to the z axis of where we moved ++ ++ // same view distance ++ ++ // used for relative positioning ++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise ++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise ++ ++ // The area excluded by overlapping the two view distance squares creates four rectangles: ++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section ++ // and on the right the "added" section. ++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually ++ // exclusive to the regions they surround. ++ ++ // 4 points of the rectangle ++ int maxX; // exclusive ++ int minX; // inclusive ++ int maxZ; // exclusive ++ int minZ; // inclusive ++ ++ if (dx != 0) { ++ // handle right addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX + (oldViewDistance * right) + right; // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle up addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = toX - (oldViewDistance * right); // inclusive ++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive ++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ); ++ } ++ } ++ } ++ ++ if (dx != 0) { ++ // handle left removal ++ ++ maxX = toX - (oldViewDistance * right); // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle down removal ++ ++ maxX = fromX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = toZ - (oldViewDistance * up); // exclusive ++ minZ = fromZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ); ++ } ++ } ++ } ++ } ++ ++ @FunctionalInterface ++ public static interface ChangeCallback<E> { ++ ++ // if there is no previous position, then prevPos = Integer.MIN_VALUE ++ void accept(final E object, final int rangeX, final int rangeZ, final int currPosX, final int currPosZ, final int prevPosX, final int prevPosZ, ++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> newState); ++ ++ } ++ ++ @FunctionalInterface ++ public static interface ChangeSourceCallback<E> { ++ void accept(final E object, final long prevPos, final long newPos); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..896c3ff7ddb07f1f6f05f90e1e3fe7fb615071d4 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java +@@ -0,0 +1,175 @@ ++package com.destroystokyo.paper.util.misc; ++ ++import io.papermc.paper.util.IntegerUtil; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.world.level.ChunkPos; ++ ++/** @author Spottedleaf */ ++public abstract class DistanceTrackingAreaMap<E> extends AreaMap<E> { ++ ++ // use this map only if you need distance tracking, the tracking here is obviously going to hit harder. ++ ++ protected final Long2IntOpenHashMap chunkToNearestDistance = new Long2IntOpenHashMap(1024, 0.7f); ++ { ++ this.chunkToNearestDistance.defaultReturnValue(-1); ++ } ++ ++ protected final DistanceChangeCallback<E> distanceChangeCallback; ++ ++ public DistanceTrackingAreaMap() { ++ this(new PooledLinkedHashSets<>()); ++ } ++ ++ // let users define a "global" or "shared" pooled sets if they wish ++ public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets) { ++ this(pooledHashSets, null, null, null); ++ } ++ ++ public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback, ++ final DistanceChangeCallback<E> distanceChangeCallback) { ++ super(pooledHashSets, addCallback, removeCallback); ++ this.distanceChangeCallback = distanceChangeCallback; ++ } ++ ++ // ret -1 if there is nothing mapped ++ public final int getNearestObjectDistance(final long key) { ++ return this.chunkToNearestDistance.get(key); ++ } ++ ++ // ret -1 if there is nothing mapped ++ public final int getNearestObjectDistance(final ChunkPos chunkPos) { ++ return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkPos)); ++ } ++ ++ // ret -1 if there is nothing mapped ++ public final int getNearestObjectDistance(final int chunkX, final int chunkZ) { ++ return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkX, chunkZ)); ++ } ++ ++ protected final void recalculateDistance(final int chunkX, final int chunkZ) { ++ final long key = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state = this.areaMap.get(key); ++ if (state == null) { ++ final int oldDistance = this.chunkToNearestDistance.remove(key); ++ // nothing here. ++ if (oldDistance == -1) { ++ // nothing was here previously ++ return; ++ } ++ if (this.distanceChangeCallback != null) { ++ this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, -1, null); ++ } ++ return; ++ } ++ ++ int newDistance = Integer.MAX_VALUE; ++ ++ final Object[] rawData = state.getBackingSet(); ++ for (int i = 0, len = rawData.length; i < len; ++i) { ++ final Object raw = rawData[i]; ++ ++ if (raw == null) { ++ continue; ++ } ++ ++ final E object = (E)raw; ++ final long location = this.objectToLastCoordinate.getLong(object); ++ ++ final int distance = Math.max(IntegerUtil.branchlessAbs(chunkX - MCUtil.getCoordinateX(location)), IntegerUtil.branchlessAbs(chunkZ - MCUtil.getCoordinateZ(location))); ++ ++ if (distance < newDistance) { ++ newDistance = distance; ++ } ++ } ++ ++ final int oldDistance = this.chunkToNearestDistance.put(key, newDistance); ++ ++ if (oldDistance != newDistance) { ++ if (this.distanceChangeCallback != null) { ++ this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, newDistance, state); ++ } ++ } ++ } ++ ++ @Override ++ protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) { ++ final int maxX = chunkX + viewDistance; ++ final int maxZ = chunkZ + viewDistance; ++ final int minX = chunkX - viewDistance; ++ final int minZ = chunkZ - viewDistance; ++ for (int x = minX; x <= maxX; ++x) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ this.recalculateDistance(x, z); ++ } ++ } ++ } ++ ++ @Override ++ protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) { ++ final int maxX = chunkX + viewDistance; ++ final int maxZ = chunkZ + viewDistance; ++ final int minX = chunkX - viewDistance; ++ final int minZ = chunkZ - viewDistance; ++ for (int x = minX; x <= maxX; ++x) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ this.recalculateDistance(x, z); ++ } ++ } ++ } ++ ++ @Override ++ protected void updateObjectCallback(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) { ++ if (oldPosition == newPosition && newViewDistance == oldViewDistance) { ++ return; ++ } ++ ++ final int toX = MCUtil.getCoordinateX(newPosition); ++ final int toZ = MCUtil.getCoordinateZ(newPosition); ++ final int fromX = MCUtil.getCoordinateX(oldPosition); ++ final int fromZ = MCUtil.getCoordinateZ(oldPosition); ++ ++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX); ++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); ++ ++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { ++ // teleported? ++ this.removeObjectCallback(object, fromX, fromZ, oldViewDistance); ++ this.addObjectCallback(object, toX, toZ, newViewDistance); ++ return; ++ } ++ ++ final int minX = Math.min(fromX - oldViewDistance, toX - newViewDistance); ++ final int maxX = Math.max(fromX + oldViewDistance, toX + newViewDistance); ++ final int minZ = Math.min(fromZ - oldViewDistance, toZ - newViewDistance); ++ final int maxZ = Math.max(fromZ + oldViewDistance, toZ + newViewDistance); ++ ++ for (int x = minX; x <= maxX; ++x) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ final int distXOld = IntegerUtil.branchlessAbs(x - fromX); ++ final int distZOld = IntegerUtil.branchlessAbs(z - fromZ); ++ ++ if (Math.max(distXOld, distZOld) <= oldViewDistance) { ++ this.recalculateDistance(x, z); ++ continue; ++ } ++ ++ final int distXNew = IntegerUtil.branchlessAbs(x - toX); ++ final int distZNew = IntegerUtil.branchlessAbs(z - toZ); ++ ++ if (Math.max(distXNew, distZNew) <= newViewDistance) { ++ this.recalculateDistance(x, z); ++ continue; ++ } ++ } ++ } ++ } ++ ++ @FunctionalInterface ++ public static interface DistanceChangeCallback<E> { ++ ++ void accept(final int posX, final int posZ, final int oldNearestDistance, final int newNearestDistance, ++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state); ++ ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..46954db7ecd35ac4018fdf476df7c8020d7ce6c8 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java +@@ -0,0 +1,32 @@ ++package com.destroystokyo.paper.util.misc; ++ ++import net.minecraft.server.level.ServerPlayer; ++ ++/** ++ * @author Spottedleaf ++ */ ++public final class PlayerAreaMap extends AreaMap<ServerPlayer> { ++ ++ public PlayerAreaMap() { ++ super(); ++ } ++ ++ public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets) { ++ super(pooledHashSets); ++ } ++ ++ public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback, ++ final ChangeCallback<ServerPlayer> removeCallback) { ++ this(pooledHashSets, addCallback, removeCallback, null); ++ } ++ ++ public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback, ++ final ChangeCallback<ServerPlayer> removeCallback, final ChangeSourceCallback<ServerPlayer> changeSourceCallback) { ++ super(pooledHashSets, addCallback, removeCallback, changeSourceCallback); ++ } ++ ++ @Override ++ protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getEmptySetFor(final ServerPlayer player) { ++ return player.cachedSingleHashSet; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d05dcea15f7047b58736c7c0e07920a04d6c5abe +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java +@@ -0,0 +1,24 @@ ++package com.destroystokyo.paper.util.misc; ++ ++import net.minecraft.server.level.ServerPlayer; ++ ++public class PlayerDistanceTrackingAreaMap extends DistanceTrackingAreaMap<ServerPlayer> { ++ ++ public PlayerDistanceTrackingAreaMap() { ++ super(); ++ } ++ ++ public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets) { ++ super(pooledHashSets); ++ } ++ ++ public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback, ++ final ChangeCallback<ServerPlayer> removeCallback, final DistanceChangeCallback<ServerPlayer> distanceChangeCallback) { ++ super(pooledHashSets, addCallback, removeCallback, distanceChangeCallback); ++ } ++ ++ @Override ++ protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getEmptySetFor(final ServerPlayer player) { ++ return player.cachedSingleHashSet; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e51104e65a07b6ea7bbbcbb6afb066ef6401cc5b +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java +@@ -0,0 +1,287 @@ ++package com.destroystokyo.paper.util.misc; ++ ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; ++import java.lang.ref.WeakReference; ++ ++/** @author Spottedleaf */ ++public class PooledLinkedHashSets<E> { ++ ++ /* Tested via https://gist.github.com/Spottedleaf/a93bb7a8993d6ce142d3efc5932bf573 */ ++ ++ // we really want to avoid that equals() check as much as possible... ++ protected final Object2ObjectOpenHashMap<PooledObjectLinkedOpenHashSet<E>, PooledObjectLinkedOpenHashSet<E>> mapPool = new Object2ObjectOpenHashMap<>(128, 0.25f); ++ ++ protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet<E> current) { ++ if (current.referenceCount == 0) { ++ throw new IllegalStateException("Cannot decrement reference count for " + current); ++ } ++ if (current.referenceCount == -1 || --current.referenceCount > 0) { ++ return; ++ } ++ ++ this.mapPool.remove(current); ++ return; ++ } ++ ++ public PooledObjectLinkedOpenHashSet<E> findMapWith(final PooledObjectLinkedOpenHashSet<E> current, final E object) { ++ final PooledObjectLinkedOpenHashSet<E> cached = current.getAddCache(object); ++ ++ if (cached != null) { ++ decrementReferenceCount(current); ++ ++ if (cached.referenceCount == 0) { ++ // bring the map back from the dead ++ PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached); ++ if (contending != null) { ++ // a map already exists with the elements we want ++ if (contending.referenceCount != -1) { ++ ++contending.referenceCount; ++ } ++ current.updateAddCache(object, contending); ++ return contending; ++ } ++ ++ cached.referenceCount = 1; ++ } else if (cached.referenceCount != -1) { ++ ++cached.referenceCount; ++ } ++ ++ return cached; ++ } ++ ++ if (!current.add(object)) { ++ return current; ++ } ++ ++ // we use get/put since we use a different key on put ++ PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current); ++ ++ if (ret == null) { ++ ret = new PooledObjectLinkedOpenHashSet<>(current); ++ current.remove(object); ++ this.mapPool.put(ret, ret); ++ ret.referenceCount = 1; ++ } else { ++ if (ret.referenceCount != -1) { ++ ++ret.referenceCount; ++ } ++ current.remove(object); ++ } ++ ++ current.updateAddCache(object, ret); ++ ++ decrementReferenceCount(current); ++ return ret; ++ } ++ ++ // rets null if current.size() == 1 ++ public PooledObjectLinkedOpenHashSet<E> findMapWithout(final PooledObjectLinkedOpenHashSet<E> current, final E object) { ++ if (current.set.size() == 1) { ++ decrementReferenceCount(current); ++ return null; ++ } ++ ++ final PooledObjectLinkedOpenHashSet<E> cached = current.getRemoveCache(object); ++ ++ if (cached != null) { ++ decrementReferenceCount(current); ++ ++ if (cached.referenceCount == 0) { ++ // bring the map back from the dead ++ PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached); ++ if (contending != null) { ++ // a map already exists with the elements we want ++ if (contending.referenceCount != -1) { ++ ++contending.referenceCount; ++ } ++ current.updateRemoveCache(object, contending); ++ return contending; ++ } ++ ++ cached.referenceCount = 1; ++ } else if (cached.referenceCount != -1) { ++ ++cached.referenceCount; ++ } ++ ++ return cached; ++ } ++ ++ if (!current.remove(object)) { ++ return current; ++ } ++ ++ // we use get/put since we use a different key on put ++ PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current); ++ ++ if (ret == null) { ++ ret = new PooledObjectLinkedOpenHashSet<>(current); ++ current.add(object); ++ this.mapPool.put(ret, ret); ++ ret.referenceCount = 1; ++ } else { ++ if (ret.referenceCount != -1) { ++ ++ret.referenceCount; ++ } ++ current.add(object); ++ } ++ ++ current.updateRemoveCache(object, ret); ++ ++ decrementReferenceCount(current); ++ return ret; ++ } ++ ++ static final class RawSetObjectLinkedOpenHashSet<E> extends ObjectOpenHashSet<E> { ++ ++ public RawSetObjectLinkedOpenHashSet() { ++ super(); ++ } ++ ++ public RawSetObjectLinkedOpenHashSet(final int capacity) { ++ super(capacity); ++ } ++ ++ public RawSetObjectLinkedOpenHashSet(final int capacity, final float loadFactor) { ++ super(capacity, loadFactor); ++ } ++ ++ @Override ++ public RawSetObjectLinkedOpenHashSet<E> clone() { ++ return (RawSetObjectLinkedOpenHashSet<E>)super.clone(); ++ } ++ ++ public E[] getRawSet() { ++ return this.key; ++ } ++ } ++ ++ public static final class PooledObjectLinkedOpenHashSet<E> { ++ ++ private static final WeakReference NULL_REFERENCE = new WeakReference<>(null); ++ ++ final RawSetObjectLinkedOpenHashSet<E> set; ++ int referenceCount; // -1 if special ++ int hash; // optimize hashcode ++ ++ // add cache ++ WeakReference<E> lastAddObject = NULL_REFERENCE; ++ WeakReference<PooledObjectLinkedOpenHashSet<E>> lastAddMap = NULL_REFERENCE; ++ ++ // remove cache ++ WeakReference<E> lastRemoveObject = NULL_REFERENCE; ++ WeakReference<PooledObjectLinkedOpenHashSet<E>> lastRemoveMap = NULL_REFERENCE; ++ ++ public PooledObjectLinkedOpenHashSet(final PooledLinkedHashSets<E> pooledSets) { ++ this.set = new RawSetObjectLinkedOpenHashSet<>(2, 0.8f); ++ } ++ ++ public PooledObjectLinkedOpenHashSet(final E single) { ++ this((PooledLinkedHashSets<E>)null); ++ this.referenceCount = -1; ++ this.add(single); ++ } ++ ++ public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet<E> other) { ++ this.set = other.set.clone(); ++ this.hash = other.hash; ++ } ++ ++ // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java ++ // generated by https://github.com/skeeto/hash-prospector ++ private static int hash0(int x) { ++ x *= 0x36935555; ++ x ^= x >>> 16; ++ return x; ++ } ++ ++ PooledObjectLinkedOpenHashSet<E> getAddCache(final E element) { ++ final E currentAdd = this.lastAddObject.get(); ++ ++ if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) { ++ return null; ++ } ++ ++ return this.lastAddMap.get(); ++ } ++ ++ PooledObjectLinkedOpenHashSet<E> getRemoveCache(final E element) { ++ final E currentRemove = this.lastRemoveObject.get(); ++ ++ if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) { ++ return null; ++ } ++ ++ return this.lastRemoveMap.get(); ++ } ++ ++ void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) { ++ this.lastAddObject = new WeakReference<>(element); ++ this.lastAddMap = new WeakReference<>(map); ++ } ++ ++ void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) { ++ this.lastRemoveObject = new WeakReference<>(element); ++ this.lastRemoveMap = new WeakReference<>(map); ++ } ++ ++ boolean add(final E element) { ++ boolean added = this.set.add(element); ++ ++ if (added) { ++ this.hash += hash0(element.hashCode()); ++ } ++ ++ return added; ++ } ++ ++ boolean remove(Object element) { ++ boolean removed = this.set.remove(element); ++ ++ if (removed) { ++ this.hash -= hash0(element.hashCode()); ++ } ++ ++ return removed; ++ } ++ ++ public boolean contains(final Object element) { ++ return this.set.contains(element); ++ } ++ ++ public E[] getBackingSet() { ++ return this.set.getRawSet(); ++ } ++ ++ public int size() { ++ return this.set.size(); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.hash; ++ } ++ ++ @Override ++ public boolean equals(final Object other) { ++ if (!(other instanceof PooledObjectLinkedOpenHashSet)) { ++ return false; ++ } ++ if (this.referenceCount == 0) { ++ return other == this; ++ } else { ++ if (other == this) { ++ // Unfortunately we are never equal to our own instance while in use! ++ return false; ++ } ++ return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " + ++ this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString(); ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a743703502cea333bd4231b6557de50e8eaf81eb +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java +@@ -0,0 +1,85 @@ ++package com.destroystokyo.paper.util.pooled; ++ ++import io.papermc.paper.util.MCUtil; ++import org.apache.commons.lang3.mutable.MutableInt; ++ ++import java.util.ArrayDeque; ++import java.util.function.Consumer; ++import java.util.function.Supplier; ++ ++public final class PooledObjects<E> { ++ ++ /** ++ * Wrapper for an object that will be have a cleaner registered for it, and may be automatically returned to pool. ++ */ ++ public class AutoReleased { ++ private final E object; ++ private final Runnable cleaner; ++ ++ public AutoReleased(E object, Runnable cleaner) { ++ this.object = object; ++ this.cleaner = cleaner; ++ } ++ ++ public final E getObject() { ++ return object; ++ } ++ ++ public final Runnable getCleaner() { ++ return cleaner; ++ } ++ } ++ ++ public static final PooledObjects<MutableInt> POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024); ++ ++ private final Supplier<E> creator; ++ private final Consumer<E> releaser; ++ private final int maxPoolSize; ++ private final ArrayDeque<E> queue; ++ ++ public PooledObjects(final Supplier<E> creator, int maxPoolSize) { ++ this(creator, maxPoolSize, null); ++ } ++ public PooledObjects(final Supplier<E> creator, int maxPoolSize, Consumer<E> releaser) { ++ if (creator == null) { ++ throw new NullPointerException("Creator must not be null"); ++ } ++ if (maxPoolSize <= 0) { ++ throw new IllegalArgumentException("Max pool size must be greater-than 0"); ++ } ++ ++ this.queue = new ArrayDeque<>(maxPoolSize); ++ this.maxPoolSize = maxPoolSize; ++ this.creator = creator; ++ this.releaser = releaser; ++ } ++ ++ public AutoReleased acquireCleaner(Object holder) { ++ return acquireCleaner(holder, this::release); ++ } ++ ++ public AutoReleased acquireCleaner(Object holder, Consumer<E> releaser) { ++ E resource = acquire(); ++ Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser); ++ return new AutoReleased(resource, cleaner); ++ } ++ ++ public final E acquire() { ++ E value; ++ synchronized (queue) { ++ value = this.queue.pollLast(); ++ } ++ return value != null ? value : this.creator.get(); ++ } ++ ++ public final void release(final E value) { ++ if (this.releaser != null) { ++ this.releaser.accept(value); ++ } ++ synchronized (this.queue) { ++ if (queue.size() < this.maxPoolSize) { ++ this.queue.addLast(value); ++ } ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8066e27ff88454cb4bc8075d936e58a067dbe9b4 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java +@@ -0,0 +1,71 @@ ++package com.destroystokyo.paper.util.set; ++ ++import java.util.Collection; ++ ++/** ++ * @author Spottedleaf <[email protected]> ++ */ ++public final class OptimizedSmallEnumSet<E extends Enum<E>> { ++ ++ private final Class<E> enumClass; ++ private long backingSet; ++ ++ public OptimizedSmallEnumSet(final Class<E> clazz) { ++ if (clazz == null) { ++ throw new IllegalArgumentException("Null class"); ++ } ++ if (!clazz.isEnum()) { ++ throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName()); ++ } ++ this.enumClass = clazz; ++ } ++ ++ public boolean add(final E element) { ++ final int ordinal = element.ordinal(); ++ final long key = 1L << ordinal; ++ ++ final long prev = this.backingSet; ++ this.backingSet = prev | key; ++ ++ return (prev & key) == 0; ++ } ++ ++ public boolean remove(final E element) { ++ final int ordinal = element.ordinal(); ++ final long key = 1L << ordinal; ++ ++ final long prev = this.backingSet; ++ this.backingSet = prev & ~key; ++ ++ return (prev & key) != 0; ++ } ++ ++ public void clear() { ++ this.backingSet = 0L; ++ } ++ ++ public int size() { ++ return Long.bitCount(this.backingSet); ++ } ++ ++ public void addAll(final Collection<E> enums) { ++ for (final E element : enums) { ++ if (element == null) { ++ throw new NullPointerException("Null element"); ++ } ++ this.backingSet |= (1L << element.ordinal()); ++ } ++ } ++ ++ public long getBackingSet() { ++ return this.backingSet; ++ } ++ ++ public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) { ++ return (other.backingSet & this.backingSet) != 0; ++ } ++ ++ public boolean contains(final E element) { ++ return (this.backingSet & (1L << element.ordinal())) != 0; ++ } ++} +diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java +index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a5c896f85 100644 +--- a/src/main/java/com/mojang/logging/LogUtils.java ++++ b/src/main/java/com/mojang/logging/LogUtils.java +@@ -61,4 +61,9 @@ public class LogUtils { + public static Logger getLogger() { + return LoggerFactory.getLogger(STACK_WALKER.getCallerClass()); + } ++ // Paper start ++ public static Logger getClassLogger() { ++ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName()); ++ } ++ // Paper end + } +diff --git a/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java b/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a5f706d6f716b2a463ae58adcde69d9e665c7733 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java +@@ -0,0 +1,477 @@ ++package io.papermc.paper.chunk; ++ ++import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Supplier; ++ ++public final class SingleThreadChunkRegionManager { ++ ++ protected final int regionSectionMergeRadius; ++ protected final int regionSectionChunkSize; ++ public final int regionChunkShift; // log2(REGION_CHUNK_SIZE) ++ ++ public final ServerLevel world; ++ public final String name; ++ ++ protected final Long2ObjectOpenHashMap<RegionSection> regionsBySection = new Long2ObjectOpenHashMap<>(); ++ protected final ReferenceLinkedOpenHashSet<Region> needsRecalculation = new ReferenceLinkedOpenHashSet<>(); ++ protected final int minSectionRecalcCount; ++ protected final double maxDeadRegionPercent; ++ protected final Supplier<RegionData> regionDataSupplier; ++ protected final Supplier<RegionSectionData> regionSectionDataSupplier; ++ ++ public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount, ++ final double maxDeadRegionPercent, final int sectionMergeRadius, ++ final int regionSectionChunkShift, ++ final String name, final Supplier<RegionData> regionDataSupplier, ++ final Supplier<RegionSectionData> regionSectionDataSupplier) { ++ this.regionSectionMergeRadius = sectionMergeRadius; ++ this.regionSectionChunkSize = 1 << regionSectionChunkShift; ++ this.regionChunkShift = regionSectionChunkShift; ++ this.world = world; ++ this.name = name; ++ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); ++ this.maxDeadRegionPercent = maxDeadRegionPercent; ++ this.regionDataSupplier = regionDataSupplier; ++ this.regionSectionDataSupplier = regionSectionDataSupplier; ++ } ++ ++ // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f ++ ++ /* ++ protected void check() { ++ ReferenceOpenHashSet<Region<T>> checked = new ReferenceOpenHashSet<>(); ++ ++ for (RegionSection<T> section : this.regionsBySection.values()) { ++ if (!checked.add(section.region)) { ++ section.region.check(); ++ } ++ } ++ for (Region<T> region : this.needsRecalculation) { ++ region.check(); ++ } ++ } ++ */ ++ ++ protected void addToRecalcQueue(final Region region) { ++ this.needsRecalculation.add(region); ++ } ++ ++ protected void removeFromRecalcQueue(final Region region) { ++ this.needsRecalculation.remove(region); ++ } ++ ++ public RegionSection getRegionSection(final int chunkX, final int chunkZ) { ++ return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift)); ++ } ++ ++ public Region getRegion(final int chunkX, final int chunkZ) { ++ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> regionChunkShift, chunkZ >> regionChunkShift)); ++ return section != null ? section.region : null; ++ } ++ ++ private final List<Region> toMerge = new ArrayList<>(); ++ ++ protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) { ++ final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ); ++ ++ if (force == null) { ++ RegionSection region = this.regionsBySection.get(sectionKey); ++ if (region != null) { ++ return region; ++ } ++ } ++ ++ int mergeCandidateSectionSize = -1; ++ Region mergeIntoCandidate = null; ++ ++ // find optimal candidate to merge into ++ ++ final int minX = sectionX - this.regionSectionMergeRadius; ++ final int maxX = sectionX + this.regionSectionMergeRadius; ++ final int minZ = sectionZ - this.regionSectionMergeRadius; ++ final int maxZ = sectionZ + this.regionSectionMergeRadius; ++ for (int currX = minX; currX <= maxX; ++currX) { ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ)); ++ if (section == null) { ++ continue; ++ } ++ final Region region = section.region; ++ if (region.dead) { ++ throw new IllegalStateException("Dead region should not be in live region manager state: " + region); ++ } ++ final int sections = region.sections.size(); ++ ++ if (sections > mergeCandidateSectionSize) { ++ mergeCandidateSectionSize = sections; ++ mergeIntoCandidate = region; ++ } ++ this.toMerge.add(region); ++ } ++ } ++ ++ // merge ++ if (mergeIntoCandidate != null) { ++ for (int i = 0; i < this.toMerge.size(); ++i) { ++ final Region region = this.toMerge.get(i); ++ if (region.dead || mergeIntoCandidate == region) { ++ continue; ++ } ++ region.mergeInto(mergeIntoCandidate); ++ } ++ this.toMerge.clear(); ++ } else { ++ mergeIntoCandidate = new Region(this); ++ } ++ ++ final RegionSection section; ++ if (force == null) { ++ this.regionsBySection.put(sectionKey, section = new RegionSection(sectionKey, this)); ++ } else { ++ final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force); ++ if (existing != null) { ++ throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() + ++ ", with " + force.toStringWithRegion()); ++ } ++ ++ section = force; ++ } ++ ++ mergeIntoCandidate.addRegionSection(section); ++ //mergeIntoCandidate.check(); ++ //this.check(); ++ ++ return section; ++ } ++ ++ public void addChunk(final int chunkX, final int chunkZ) { ++ this.getOrCreateAndMergeSection(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift, null).addChunk(chunkX, chunkZ); ++ } ++ ++ public void removeChunk(final int chunkX, final int chunkZ) { ++ final RegionSection section = this.regionsBySection.get( ++ MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift) ++ ); ++ if (section != null) { ++ section.removeChunk(chunkX, chunkZ); ++ } else { ++ throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist"); ++ } ++ } ++ ++ public void recalculateRegions() { ++ for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) { ++ final Region region = this.needsRecalculation.removeFirst(); ++ ++ this.recalculateRegion(region); ++ //this.check(); ++ } ++ } ++ ++ protected void recalculateRegion(final Region region) { ++ region.markedForRecalc = false; ++ //region.check(); ++ // clear unused regions ++ for (final Iterator<RegionSection> iterator = region.deadSections.iterator(); iterator.hasNext();) { ++ final RegionSection deadSection = iterator.next(); ++ ++ if (deadSection.hasChunks()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); ++ } ++ if (!region.removeRegionSection(deadSection)) { ++ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); ++ } ++ if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) { ++ throw new IllegalStateException("Cannot remove dead section '" + ++ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + ++ this.regionsBySection.get(deadSection.regionCoordinate)); ++ } ++ } ++ region.deadSections.clear(); ++ ++ // implicitly cover cases where size == 0 ++ if (region.sections.size() < this.minSectionRecalcCount) { ++ //region.check(); ++ return; ++ } ++ ++ // run a test to see if we actually need to recalculate ++ // TODO ++ ++ // destroy and rebuild the region ++ region.dead = true; ++ ++ // destroy region state ++ for (final Iterator<RegionSection> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ final RegionSection aliveSection = iterator.next(); ++ if (!aliveSection.hasChunks()) { ++ throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!"); ++ } ++ if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) { ++ throw new IllegalStateException("Cannot remove alive section '" + ++ aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " + ++ this.regionsBySection.get(aliveSection.regionCoordinate)); ++ } ++ } ++ ++ // rebuild regions ++ for (final Iterator<RegionSection> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ final RegionSection aliveSection = iterator.next(); ++ this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection); ++ } ++ } ++ ++ public static final class Region { ++ protected final IteratorSafeOrderedReferenceSet<RegionSection> sections = new IteratorSafeOrderedReferenceSet<>(); ++ protected final ReferenceOpenHashSet<RegionSection> deadSections = new ReferenceOpenHashSet<>(16, 0.7f); ++ protected boolean dead; ++ protected boolean markedForRecalc; ++ ++ public final SingleThreadChunkRegionManager regionManager; ++ public final RegionData regionData; ++ ++ protected Region(final SingleThreadChunkRegionManager regionManager) { ++ this.regionManager = regionManager; ++ this.regionData = regionManager.regionDataSupplier.get(); ++ } ++ ++ public IteratorSafeOrderedReferenceSet.Iterator<RegionSection> getSections() { ++ return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); ++ } ++ ++ protected final double getDeadSectionPercent() { ++ return (double)this.deadSections.size() / (double)this.sections.size(); ++ } ++ ++ /* ++ protected void check() { ++ if (this.dead) { ++ throw new IllegalStateException("Dead region!"); ++ } ++ for (final Iterator<RegionSection<T>> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ final RegionSection<T> section = iterator.next(); ++ if (section.region != this) { ++ throw new IllegalStateException("Region section must point to us!"); ++ } ++ if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) { ++ throw new IllegalStateException("Region section must match the regionmanager state!"); ++ } ++ } ++ } ++ */ ++ ++ // note: it is not true that the region at this point is not in any region. use the region field on the section ++ // to see if it is currently in another region. ++ protected final boolean addRegionSection(final RegionSection section) { ++ if (!this.sections.add(section)) { ++ return false; ++ } ++ ++ section.sectionData.addToRegion(section, section.region, this); ++ ++ section.region = this; ++ return true; ++ } ++ ++ protected final boolean removeRegionSection(final RegionSection section) { ++ if (!this.sections.remove(section)) { ++ return false; ++ } ++ ++ section.sectionData.removeFromRegion(section, this); ++ ++ return true; ++ } ++ ++ protected void mergeInto(final Region mergeTarget) { ++ if (this == mergeTarget) { ++ throw new IllegalStateException("Cannot merge a region onto itself"); ++ } ++ if (this.dead) { ++ throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget); ++ } else if (mergeTarget.dead) { ++ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); ++ } ++ this.dead = true; ++ if (this.markedForRecalc) { ++ this.regionManager.removeFromRecalcQueue(this); ++ } ++ ++ for (final Iterator<RegionSection> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ final RegionSection section = iterator.next(); ++ ++ if (!mergeTarget.addRegionSection(section)) { ++ throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget); ++ } ++ } ++ ++ for (final RegionSection deadSection : this.deadSections) { ++ if (!this.sections.contains(deadSection)) { ++ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); ++ } ++ mergeTarget.deadSections.add(deadSection); ++ } ++ //mergeTarget.check(); ++ } ++ ++ protected void markSectionAlive(final RegionSection section) { ++ this.deadSections.remove(section); ++ if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) { ++ this.regionManager.removeFromRecalcQueue(this); ++ this.markedForRecalc = false; ++ } ++ } ++ ++ protected void markSectionDead(final RegionSection section) { ++ this.deadSections.add(section); ++ if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) { ++ this.regionManager.addToRecalcQueue(this); ++ this.markedForRecalc = true; ++ } ++ } ++ ++ @Override ++ public String toString() { ++ final StringBuilder ret = new StringBuilder(128); ++ ++ ret.append("Region{"); ++ ret.append("dead=").append(this.dead).append(','); ++ ret.append("markedForRecalc=").append(this.markedForRecalc).append(','); ++ ++ ret.append("sectionCount=").append(this.sections.size()).append(','); ++ ret.append("sections=["); ++ for (final Iterator<RegionSection> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ final RegionSection section = iterator.next(); ++ ret.append(section); ++ if (iterator.hasNext()) { ++ ret.append(','); ++ } ++ } ++ ret.append(']'); ++ ++ ret.append('}'); ++ return ret.toString(); ++ } ++ } ++ ++ public static final class RegionSection { ++ protected final long regionCoordinate; ++ protected final long[] chunksBitset; ++ protected int chunkCount; ++ protected Region region; ++ ++ public final SingleThreadChunkRegionManager regionManager; ++ public final RegionSectionData sectionData; ++ ++ protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) { ++ this.regionCoordinate = regionCoordinate; ++ this.regionManager = regionManager; ++ this.chunksBitset = new long[Math.max(1, regionManager.regionSectionChunkSize * regionManager.regionSectionChunkSize / Long.SIZE)]; ++ this.sectionData = regionManager.regionSectionDataSupplier.get(); ++ } ++ ++ public int getSectionX() { ++ return MCUtil.getCoordinateX(this.regionCoordinate); ++ } ++ ++ public int getSectionZ() { ++ return MCUtil.getCoordinateZ(this.regionCoordinate); ++ } ++ ++ public Region getRegion() { ++ return this.region; ++ } ++ ++ private int getChunkIndex(final int chunkX, final int chunkZ) { ++ return (chunkX & (this.regionManager.regionSectionChunkSize - 1)) | ((chunkZ & (this.regionManager.regionSectionChunkSize - 1)) << this.regionManager.regionChunkShift); ++ } ++ ++ protected boolean hasChunks() { ++ return this.chunkCount != 0; ++ } ++ ++ protected void addChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); ++ if (after == bitset) { ++ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ if (++this.chunkCount != 1) { ++ return; ++ } ++ this.region.markSectionAlive(this); ++ } ++ ++ protected void removeChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); ++ if (before == bitset) { ++ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ if (--this.chunkCount != 0) { ++ return; ++ } ++ this.region.markSectionDead(this); ++ } ++ ++ @Override ++ public String toString() { ++ return "RegionSection{" + ++ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "hash=" + this.hashCode() + ++ "}"; ++ } ++ ++ public String toStringWithRegion() { ++ return "RegionSection{" + ++ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "hash=" + this.hashCode() + "," + ++ "region=" + this.region + ++ "}"; ++ } ++ ++ private static String toString(final long[] array) { ++ final StringBuilder ret = new StringBuilder(); ++ for (final long value : array) { ++ // zero pad the hex string ++ final char[] zeros = new char[Long.SIZE / 4]; ++ Arrays.fill(zeros, '0'); ++ final String string = Long.toHexString(value); ++ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); ++ ++ ret.append(zeros); ++ } ++ ++ return ret.toString(); ++ } ++ } ++ ++ public static interface RegionData { ++ ++ } ++ ++ public static interface RegionSectionData { ++ ++ public void removeFromRegion(final RegionSection section, final Region from); ++ ++ // removal from the old region is handled via removeFromRegion ++ public void addToRegion(final RegionSection section, final Region oldRegion, final Region newRegion); ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cff2f04409fab9abca87ceec85a551e1d59f9e7d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +@@ -0,0 +1,296 @@ ++package io.papermc.paper.chunk.system; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import com.destroystokyo.paper.util.SneakyThrow; ++import com.mojang.datafixers.util.Either; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.CoordinateUtils; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunk; ++import org.bukkit.Bukkit; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.CompletableFuture; ++import java.util.function.Consumer; ++ ++public final class ChunkSystem { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { ++ scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { ++ level.chunkSource.mainThreadProcessor.execute(run); ++ } ++ ++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, ++ final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final Consumer<ChunkAccess> onComplete) { ++ if (gen) { ++ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ return; ++ } ++ scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { ++ if (chunk == null) { ++ onComplete.accept(null); ++ } else { ++ if (chunk.getStatus().isOrAfter(toStatus)) { ++ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } else { ++ onComplete.accept(null); ++ } ++ } ++ }); ++ } ++ ++ static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); ++ ++ private static long chunkLoadCounter = 0L; ++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ if (!Bukkit.isPrimaryThread()) { ++ scheduleChunkTask(level, chunkX, chunkZ, () -> { ++ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ ++ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); ++ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ level.chunkSource.runDistanceManagerUpdates(); ++ ++ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Exception handling chunk load callback", thr); ++ SneakyThrow.sneaky(thr); ++ } finally { ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); ++ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ } ++ }; ++ ++ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (holder == null || holder.getTicketLevel() > minLevel) { ++ loadCallback.accept(null); ++ return; ++ } ++ ++ final CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap); ++ ++ if (loadFuture.isDone()) { ++ loadCallback.accept(loadFuture.join().left().orElse(null)); ++ return; ++ } ++ ++ loadFuture.whenCompleteAsync((final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either, final Throwable thr) -> { ++ if (thr != null) { ++ loadCallback.accept(null); ++ return; ++ } ++ loadCallback.accept(either.left().orElse(null)); ++ }, (final Runnable r) -> { ++ scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); ++ }); ++ } ++ ++ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, ++ final FullChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) { ++ // This method goes unused until the chunk system rewrite ++ if (toStatus == FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); ++ } ++ ++ if (!Bukkit.isPrimaryThread()) { ++ scheduleChunkTask(level, chunkX, chunkZ, () -> { ++ scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ ++ final int minLevel = 33 - (toStatus.ordinal() - 1); ++ final int radius = toStatus.ordinal() - 1; ++ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ level.chunkSource.runDistanceManagerUpdates(); ++ ++ final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Exception handling chunk load callback", thr); ++ SneakyThrow.sneaky(thr); ++ } finally { ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); ++ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ } ++ }; ++ ++ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (holder == null || holder.getTicketLevel() > minLevel) { ++ loadCallback.accept(null); ++ return; ++ } ++ ++ final CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> tickingState; ++ switch (toStatus) { ++ case FULL: { ++ tickingState = holder.getFullChunkFuture(); ++ break; ++ } ++ case BLOCK_TICKING: { ++ tickingState = holder.getTickingChunkFuture(); ++ break; ++ } ++ case ENTITY_TICKING: { ++ tickingState = holder.getEntityTickingChunkFuture(); ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Cannot reach here"); ++ } ++ } ++ ++ if (tickingState.isDone()) { ++ loadCallback.accept(tickingState.join().left().orElse(null)); ++ return; ++ } ++ ++ tickingState.whenCompleteAsync((final Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either, final Throwable thr) -> { ++ if (thr != null) { ++ loadCallback.accept(null); ++ return; ++ } ++ loadCallback.accept(either.left().orElse(null)); ++ }, (final Runnable r) -> { ++ scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); ++ }); ++ } ++ ++ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) { ++ return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); ++ } ++ ++ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) { ++ return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); ++ } ++ ++ public static int getVisibleChunkHolderCount(final ServerLevel level) { ++ return level.chunkSource.chunkMap.visibleChunkMap.size(); ++ } ++ ++ public static int getUpdatingChunkHolderCount(final ServerLevel level) { ++ return level.chunkSource.chunkMap.updatingChunkMap.size(); ++ } ++ ++ public static boolean hasAnyChunkHolders(final ServerLevel level) { ++ return getUpdatingChunkHolderCount(level) != 0; ++ } ++ ++ public static void onEntityPreAdd(final ServerLevel level, final Entity entity) { ++ ++ } ++ ++ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { ++ final ChunkMap chunkMap = level.chunkSource.chunkMap; ++ for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { ++ chunkMap.regionManagers.get(index).addChunk(holder.getPos().x, holder.getPos().z); ++ } ++ } ++ ++ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { ++ final ChunkMap chunkMap = level.chunkSource.chunkMap; ++ for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { ++ chunkMap.regionManagers.get(index).removeChunk(holder.getPos().x, holder.getPos().z); ++ } ++ } ++ ++ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ chunk.playerChunk = holder; ++ } ++ ++ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ chunk.level.getChunkSource().tickingChunks.add(chunk); ++ } ++ ++ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ chunk.level.getChunkSource().tickingChunks.remove(chunk); ++ } ++ ++ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ chunk.level.getChunkSource().entityTickingChunks.add(chunk); ++ } ++ ++ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ chunk.level.getChunkSource().entityTickingChunks.remove(chunk); ++ } ++ ++ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { ++ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); ++ } ++ ++ public static int getSendViewDistance(final ServerPlayer player) { ++ return getLoadViewDistance(player); ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ if (level == null) { ++ return Bukkit.getViewDistance(); ++ } ++ return level.chunkSource.chunkMap.getPlayerViewDistance(player); ++ } ++ ++ public static int getTickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ if (level == null) { ++ return Bukkit.getSimulationDistance(); ++ } ++ return level.chunkSource.chunkMap.distanceManager.simulationDistance; ++ } ++ ++ private ChunkSystem() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java +new file mode 100644 +index 0000000000000000000000000000000000000000..be668387f65a633c6ac497fca632a4767a1bf3a2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/CachedLists.java +@@ -0,0 +1,8 @@ ++package io.papermc.paper.util; ++ ++public final class CachedLists { ++ ++ public static void reset() { ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..413e4b6da027876dbbe8eb78f2568a440f431547 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java +@@ -0,0 +1,128 @@ ++package io.papermc.paper.util; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++ ++public final class CoordinateUtils { ++ ++ // dx, dz are relative to the target chunk ++ // dx, dz in [-radius, radius] ++ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) { ++ return (dx + radius) + (2 * radius + 1)*(dz + radius); ++ } ++ ++ // the chunk keys are compatible with vanilla ++ ++ public static long getChunkKey(final BlockPos pos) { ++ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final Entity entity) { ++ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final ChunkPos pos) { ++ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final SectionPos pos) { ++ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public static int getChunkX(final long chunkKey) { ++ return (int)chunkKey; ++ } ++ ++ public static int getChunkZ(final long chunkKey) { ++ return (int)(chunkKey >>> 32); ++ } ++ ++ public static int getChunkCoordinate(final double blockCoordinate) { ++ return Mth.floor(blockCoordinate) >> 4; ++ } ++ ++ // the section keys are compatible with vanilla's ++ ++ static final int SECTION_X_BITS = 22; ++ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; ++ static final int SECTION_Y_BITS = 20; ++ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; ++ static final int SECTION_Z_BITS = 22; ++ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; ++ // format is y,z,x (in order of LSB to MSB) ++ static final int SECTION_Y_SHIFT = 0; ++ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; ++ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; ++ static final int SECTION_TO_BLOCK_SHIFT = 4; ++ ++ public static long getChunkSectionKey(final int x, final int y, final int z) { ++ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) ++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) ++ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); ++ } ++ ++ public static long getChunkSectionKey(final SectionPos pos) { ++ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) ++ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) ++ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); ++ } ++ ++ public static long getChunkSectionKey(final ChunkPos pos, final int y) { ++ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) ++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) ++ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); ++ } ++ ++ public static long getChunkSectionKey(final BlockPos pos) { ++ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | ++ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | ++ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); ++ } ++ ++ public static long getChunkSectionKey(final Entity entity) { ++ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | ++ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | ++ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); ++ } ++ ++ public static int getChunkSectionX(final long key) { ++ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); ++ } ++ ++ public static int getChunkSectionY(final long key) { ++ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); ++ } ++ ++ public static int getChunkSectionZ(final long key) { ++ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); ++ } ++ ++ // the block coordinates are not necessarily compatible with vanilla's ++ ++ public static int getBlockCoordinate(final double blockCoordinate) { ++ return Mth.floor(blockCoordinate); ++ } ++ ++ public static long getBlockKey(final int x, final int y, final int z) { ++ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); ++ } ++ ++ public static long getBlockKey(final BlockPos pos) { ++ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); ++ } ++ ++ public static long getBlockKey(final Entity entity) { ++ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); ++ } ++ ++ private CoordinateUtils() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/IntegerUtil.java b/src/main/java/io/papermc/paper/util/IntegerUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..16785bd5c0524f6bad0691ca7ecd4514608d2eab +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/IntegerUtil.java +@@ -0,0 +1,242 @@ ++package io.papermc.paper.util; ++ ++public final class IntegerUtil { ++ ++ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; ++ public static final long HIGH_BIT_U64 = Long.MIN_VALUE; ++ ++ public static int ceilLog2(final int value) { ++ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros ++ } ++ ++ public static long ceilLog2(final long value) { ++ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros ++ } ++ ++ public static int floorLog2(final int value) { ++ // xor is optimized subtract for 2^n -1 ++ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) ++ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros ++ } ++ ++ public static int floorLog2(final long value) { ++ // xor is optimized subtract for 2^n -1 ++ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) ++ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros ++ } ++ ++ public static int roundCeilLog2(final int value) { ++ // optimized variant of 1 << (32 - leading(val - 1)) ++ // given ++ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) ++ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) ++ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) ++ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) ++ // HIGH_BIT_32 >>> (-1 + leading(val - 1)) ++ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); ++ } ++ ++ public static long roundCeilLog2(final long value) { ++ // see logic documented above ++ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); ++ } ++ ++ public static int roundFloorLog2(final int value) { ++ // optimized variant of 1 << (31 - leading(val)) ++ // given ++ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) ++ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) ++ // HIGH_BIT_32 >> (31 - (31 - leading(val))) ++ // HIGH_BIT_32 >> (31 - 31 + leading(val)) ++ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); ++ } ++ ++ public static long roundFloorLog2(final long value) { ++ // see logic documented above ++ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); ++ } ++ ++ public static boolean isPowerOfTwo(final int n) { ++ // 2^n has one bit ++ // note: this rets true for 0 still ++ return IntegerUtil.getTrailingBit(n) == n; ++ } ++ ++ public static boolean isPowerOfTwo(final long n) { ++ // 2^n has one bit ++ // note: this rets true for 0 still ++ return IntegerUtil.getTrailingBit(n) == n; ++ } ++ ++ public static int getTrailingBit(final int n) { ++ return -n & n; ++ } ++ ++ public static long getTrailingBit(final long n) { ++ return -n & n; ++ } ++ ++ public static int trailingZeros(final int n) { ++ return Integer.numberOfTrailingZeros(n); ++ } ++ ++ public static int trailingZeros(final long n) { ++ return Long.numberOfTrailingZeros(n); ++ } ++ ++ // from hacker's delight (signed division magic value) ++ public static int getDivisorMultiple(final long numbers) { ++ return (int)(numbers >>> 32); ++ } ++ ++ // from hacker's delight (signed division magic value) ++ public static int getDivisorShift(final long numbers) { ++ return (int)numbers; ++ } ++ ++ // copied from hacker's delight (signed division magic value) ++ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt ++ public static long getDivisorNumbers(final int d) { ++ final int ad = branchlessAbs(d); ++ ++ if (ad < 2) { ++ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); ++ } ++ ++ final int two31 = 0x80000000; ++ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour ++ ++ /* ++ Signed usage: ++ int number; ++ long magic = getDivisorNumbers(div); ++ long mul = magic >>> 32; ++ int sign = number >> 31; ++ int result = (int)(((long)number * mul) >>> magic) - sign; ++ */ ++ /* ++ Unsigned usage: ++ int number; ++ long magic = getDivisorNumbers(div); ++ long mul = magic >>> 32; ++ int result = (int)(((long)number * mul) >>> magic); ++ */ ++ ++ int p = 31; ++ ++ // all these variables are UNSIGNED! ++ int t = two31 + (d >>> 31); ++ int anc = t - 1 - (int)((t & mask)%ad); ++ int q1 = (int)((two31 & mask)/(anc & mask)); ++ int r1 = two31 - q1*anc; ++ int q2 = (int)((two31 & mask)/(ad & mask)); ++ int r2 = two31 - q2*ad; ++ int delta; ++ ++ do { ++ p = p + 1; ++ q1 = 2*q1; // Update q1 = 2**p/|nc|. ++ r1 = 2*r1; // Update r1 = rem(2**p, |nc|). ++ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) ++ q1 = q1 + 1; ++ r1 = r1 - anc; ++ } ++ q2 = 2*q2; // Update q2 = 2**p/|d|. ++ r2 = 2*r2; // Update r2 = rem(2**p, |d|). ++ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) ++ q2 = q2 + 1; ++ r2 = r2 - ad; ++ } ++ delta = ad - r2; ++ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); ++ ++ int magicNum = q2 + 1; ++ if (d < 0) { ++ magicNum = -magicNum; ++ } ++ int shift = p; ++ return ((long)magicNum << 32) | shift; ++ } ++ ++ public static int branchlessAbs(final int val) { ++ // -n = -1 ^ n + 1 ++ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 ++ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 ++ } ++ ++ public static long branchlessAbs(final long val) { ++ // -n = -1 ^ n + 1 ++ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 ++ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 ++ } ++ ++ //https://github.com/skeeto/hash-prospector for hash functions ++ ++ //score = ~590.47984224483832 ++ public static int hash0(int x) { ++ x *= 0x36935555; ++ x ^= x >>> 16; ++ return x; ++ } ++ ++ //score = ~310.01596637036749 ++ public static int hash1(int x) { ++ x ^= x >>> 15; ++ x *= 0x356aaaad; ++ x ^= x >>> 17; ++ return x; ++ } ++ ++ public static int hash2(int x) { ++ x ^= x >>> 16; ++ x *= 0x7feb352d; ++ x ^= x >>> 15; ++ x *= 0x846ca68b; ++ x ^= x >>> 16; ++ return x; ++ } ++ ++ public static int hash3(int x) { ++ x ^= x >>> 17; ++ x *= 0xed5ad4bb; ++ x ^= x >>> 11; ++ x *= 0xac4c1b51; ++ x ^= x >>> 15; ++ x *= 0x31848bab; ++ x ^= x >>> 14; ++ return x; ++ } ++ ++ //score = ~365.79959673201887 ++ public static long hash1(long x) { ++ x ^= x >>> 27; ++ x *= 0xb24924b71d2d354bL; ++ x ^= x >>> 28; ++ return x; ++ } ++ ++ //h2 hash ++ public static long hash2(long x) { ++ x ^= x >>> 32; ++ x *= 0xd6e8feb86659fd93L; ++ x ^= x >>> 32; ++ x *= 0xd6e8feb86659fd93L; ++ x ^= x >>> 32; ++ return x; ++ } ++ ++ public static long hash3(long x) { ++ x ^= x >>> 45; ++ x *= 0xc161abe5704b6c79L; ++ x ^= x >>> 41; ++ x *= 0xe3e5389aedbc90f7L; ++ x ^= x >>> 56; ++ x *= 0x1f9aba75a52db073L; ++ x ^= x >>> 53; ++ return x; ++ } ++ ++ private IntegerUtil() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5279758f0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java +@@ -0,0 +1,115 @@ ++package io.papermc.paper.util; ++ ++public final class IntervalledCounter { ++ ++ protected long[] times; ++ protected long[] counts; ++ protected final long interval; ++ protected long minTime; ++ protected long sum; ++ protected int head; // inclusive ++ protected int tail; // exclusive ++ ++ public IntervalledCounter(final long interval) { ++ this.times = new long[8]; ++ this.counts = new long[8]; ++ this.interval = interval; ++ } ++ ++ public void updateCurrentTime() { ++ this.updateCurrentTime(System.nanoTime()); ++ } ++ ++ public void updateCurrentTime(final long currentTime) { ++ long sum = this.sum; ++ int head = this.head; ++ final int tail = this.tail; ++ final long minTime = currentTime - this.interval; ++ ++ final int arrayLen = this.times.length; ++ ++ // guard against overflow by using subtraction ++ while (head != tail && this.times[head] - minTime < 0) { ++ sum -= this.counts[head]; ++ // there are two ways we can do this: ++ // 1. free the count when adding ++ // 2. free it now ++ // option #2 ++ this.counts[head] = 0; ++ if (++head >= arrayLen) { ++ head = 0; ++ } ++ } ++ ++ this.sum = sum; ++ this.head = head; ++ this.minTime = minTime; ++ } ++ ++ public void addTime(final long currTime) { ++ this.addTime(currTime, 1L); ++ } ++ ++ public void addTime(final long currTime, final long count) { ++ // guard against overflow by using subtraction ++ if (currTime - this.minTime < 0) { ++ return; ++ } ++ int nextTail = (this.tail + 1) % this.times.length; ++ if (nextTail == this.head) { ++ this.resize(); ++ nextTail = (this.tail + 1) % this.times.length; ++ } ++ ++ this.times[this.tail] = currTime; ++ this.counts[this.tail] += count; ++ this.sum += count; ++ this.tail = nextTail; ++ } ++ ++ public void updateAndAdd(final int count) { ++ final long currTime = System.nanoTime(); ++ this.updateCurrentTime(currTime); ++ this.addTime(currTime, count); ++ } ++ ++ public void updateAndAdd(final int count, final long currTime) { ++ this.updateCurrentTime(currTime); ++ this.addTime(currTime, count); ++ } ++ ++ private void resize() { ++ final long[] oldElements = this.times; ++ final long[] oldCounts = this.counts; ++ final long[] newElements = new long[this.times.length * 2]; ++ final long[] newCounts = new long[this.times.length * 2]; ++ this.times = newElements; ++ this.counts = newCounts; ++ ++ final int head = this.head; ++ final int tail = this.tail; ++ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head)); ++ this.head = 0; ++ this.tail = size; ++ ++ if (tail >= head) { ++ System.arraycopy(oldElements, head, newElements, 0, size); ++ System.arraycopy(oldCounts, head, newCounts, 0, size); ++ } else { ++ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); ++ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); ++ ++ System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head); ++ System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail); ++ } ++ } ++ ++ // returns in units per second ++ public double getRate() { ++ return this.size() / (this.interval * 1.0e-9); ++ } ++ ++ public long size() { ++ return this.sum; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9e21566fc4d76ae2e305f0e1d47d8c2f1c1f2c4c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -0,0 +1,554 @@ ++package io.papermc.paper.util; ++ ++import com.google.common.collect.ImmutableList; ++import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import io.papermc.paper.math.BlockPosition; ++import io.papermc.paper.math.FinePosition; ++import io.papermc.paper.math.Position; ++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; ++import java.lang.ref.Cleaner; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.core.Vec3i; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.ClipContext; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.phys.Vec3; ++import org.apache.commons.lang.exception.ExceptionUtils; ++import org.bukkit.Location; ++import org.bukkit.block.BlockFace; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.craftbukkit.util.Waitable; ++import org.jetbrains.annotations.NotNull; ++import org.spigotmc.AsyncCatcher; ++ ++import javax.annotation.Nonnull; ++import javax.annotation.Nullable; ++import java.util.List; ++import java.util.Queue; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.ExecutionException; ++import java.util.concurrent.LinkedBlockingQueue; ++import java.util.concurrent.ThreadPoolExecutor; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.TimeoutException; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.BiConsumer; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++import java.util.function.Supplier; ++ ++public final class MCUtil { ++ public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor( ++ 0, 2, 60L, TimeUnit.SECONDS, ++ new LinkedBlockingQueue<>(), ++ new ThreadFactoryBuilder() ++ .setNameFormat("Paper Async Task Handler Thread - %1$d") ++ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) ++ .build() ++ ); ++ public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor( ++ 1, 1, 0L, TimeUnit.SECONDS, ++ new LinkedBlockingQueue<>(), ++ new ThreadFactoryBuilder() ++ .setNameFormat("Paper Object Cleaner") ++ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) ++ .build() ++ ); ++ ++ public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE); ++ ++ ++ public static Runnable once(Runnable run) { ++ AtomicBoolean ran = new AtomicBoolean(false); ++ return () -> { ++ if (ran.compareAndSet(false, true)) { ++ run.run(); ++ } ++ }; ++ } ++ ++ public static <T> Runnable once(List<T> list, Consumer<T> cb) { ++ return once(() -> { ++ list.forEach(cb); ++ }); ++ } ++ ++ private static Runnable makeCleanerCallback(Runnable run) { ++ return once(() -> cleanerExecutor.execute(run)); ++ } ++ ++ /** ++ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky! ++ * @param obj ++ * @param run ++ * @return ++ */ ++ public static Runnable registerCleaner(Object obj, Runnable run) { ++ // Wrap callback in its own method above or the lambda will leak object ++ Runnable cleaner = makeCleanerCallback(run); ++ CleanerHolder.CLEANER.register(obj, cleaner); ++ return cleaner; ++ } ++ ++ private static final class CleanerHolder { ++ private static final Cleaner CLEANER = Cleaner.create(); ++ } ++ ++ /** ++ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky! ++ * @param obj ++ * @param list ++ * @param cleaner ++ * @param <T> ++ * @return ++ */ ++ public static <T> Runnable registerListCleaner(Object obj, List<T> list, Consumer<T> cleaner) { ++ return registerCleaner(obj, () -> { ++ list.forEach(cleaner); ++ list.clear(); ++ }); ++ } ++ ++ /** ++ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky! ++ * @param obj ++ * @param resource ++ * @param cleaner ++ * @param <T> ++ * @return ++ */ ++ public static <T> Runnable registerCleaner(Object obj, T resource, java.util.function.Consumer<T> cleaner) { ++ return registerCleaner(obj, () -> cleaner.accept(resource)); ++ } ++ ++ public static List<ChunkPos> getSpiralOutChunks(BlockPos blockposition, int radius) { ++ List<ChunkPos> list = com.google.common.collect.Lists.newArrayList(); ++ ++ list.add(new ChunkPos(blockposition.getX() >> 4, blockposition.getZ() >> 4)); ++ for (int r = 1; r <= radius; r++) { ++ int x = -r; ++ int z = r; ++ ++ // Iterates the edge of half of the box; then negates for other half. ++ while (x <= r && z > -r) { ++ list.add(new ChunkPos((blockposition.getX() + (x << 4)) >> 4, (blockposition.getZ() + (z << 4)) >> 4)); ++ list.add(new ChunkPos((blockposition.getX() - (x << 4)) >> 4, (blockposition.getZ() - (z << 4)) >> 4)); ++ ++ if (x < r) { ++ x++; ++ } else { ++ z--; ++ } ++ } ++ } ++ return list; ++ } ++ ++ public static int fastFloor(double x) { ++ int truncated = (int)x; ++ return x < (double)truncated ? truncated - 1 : truncated; ++ } ++ ++ public static int fastFloor(float x) { ++ int truncated = (int)x; ++ return x < (double)truncated ? truncated - 1 : truncated; ++ } ++ ++ public static float normalizeYaw(float f) { ++ float f1 = f % 360.0F; ++ ++ if (f1 >= 180.0F) { ++ f1 -= 360.0F; ++ } ++ ++ if (f1 < -180.0F) { ++ f1 += 360.0F; ++ } ++ ++ return f1; ++ } ++ ++ /** ++ * Quickly generate a stack trace for current location ++ * ++ * @return Stacktrace ++ */ ++ public static String stack() { ++ return ExceptionUtils.getFullStackTrace(new Throwable()); ++ } ++ ++ /** ++ * Quickly generate a stack trace for current location with message ++ * ++ * @param str ++ * @return Stacktrace ++ */ ++ public static String stack(String str) { ++ return ExceptionUtils.getFullStackTrace(new Throwable(str)); ++ } ++ ++ public static long getCoordinateKey(final BlockPos blockPos) { ++ return ((long)(blockPos.getZ() >> 4) << 32) | ((blockPos.getX() >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getCoordinateKey(final Entity entity) { ++ return ((long)(MCUtil.fastFloor(entity.getZ()) >> 4) << 32) | ((MCUtil.fastFloor(entity.getX()) >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getCoordinateKey(final ChunkPos pair) { ++ return ((long)pair.z << 32) | (pair.x & 0xFFFFFFFFL); ++ } ++ ++ public static long getCoordinateKey(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public static int getCoordinateX(final long key) { ++ return (int)key; ++ } ++ ++ public static int getCoordinateZ(final long key) { ++ return (int)(key >>> 32); ++ } ++ ++ public static int getChunkCoordinate(final double coordinate) { ++ return MCUtil.fastFloor(coordinate) >> 4; ++ } ++ ++ public static int getBlockCoordinate(final double coordinate) { ++ return MCUtil.fastFloor(coordinate); ++ } ++ ++ public static long getBlockKey(final int x, final int y, final int z) { ++ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); ++ } ++ ++ public static long getBlockKey(final BlockPos pos) { ++ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); ++ } ++ ++ public static long getBlockKey(final Entity entity) { ++ return getBlockKey(getBlockCoordinate(entity.getX()), getBlockCoordinate(entity.getY()), getBlockCoordinate(entity.getZ())); ++ } ++ ++ // assumes the sets have the same comparator, and if this comparator is null then assume T is Comparable ++ public static <T> void mergeSortedSets(final java.util.function.Consumer<T> consumer, final java.util.Comparator<? super T> comparator, final java.util.SortedSet<T>...sets) { ++ final ObjectRBTreeSet<T> all = new ObjectRBTreeSet<>(comparator); ++ // note: this is done in log(n!) ~ nlogn time. It could be improved if it were to mimic what mergesort does. ++ for (java.util.SortedSet<T> set : sets) { ++ if (set != null) { ++ all.addAll(set); ++ } ++ } ++ all.forEach(consumer); ++ } ++ ++ private MCUtil() {} ++ ++ public static final java.util.concurrent.Executor MAIN_EXECUTOR = (run) -> { ++ if (!isMainThread()) { ++ MinecraftServer.getServer().execute(run); ++ } else { ++ run.run(); ++ } ++ }; ++ ++ public static <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) { ++ return future.thenApplyAsync(r -> r, MAIN_EXECUTOR); ++ } ++ ++ public static <T> void thenOnMain(CompletableFuture<T> future, Consumer<T> consumer) { ++ future.thenAcceptAsync(consumer, MAIN_EXECUTOR); ++ } ++ public static <T> void thenOnMain(CompletableFuture<T> future, BiConsumer<T, Throwable> consumer) { ++ future.whenCompleteAsync(consumer, MAIN_EXECUTOR); ++ } ++ ++ public static boolean isMainThread() { ++ return MinecraftServer.getServer().isSameThread(); ++ } ++ ++ public static org.bukkit.scheduler.BukkitTask scheduleTask(int ticks, Runnable runnable) { ++ return scheduleTask(ticks, runnable, null); ++ } ++ ++ public static org.bukkit.scheduler.BukkitTask scheduleTask(int ticks, Runnable runnable, String taskName) { ++ return MinecraftServer.getServer().server.getScheduler().scheduleInternalTask(runnable, ticks, taskName); ++ } ++ ++ public static void processQueue() { ++ Runnable runnable; ++ Queue<Runnable> processQueue = getProcessQueue(); ++ while ((runnable = processQueue.poll()) != null) { ++ try { ++ runnable.run(); ++ } catch (Exception e) { ++ MinecraftServer.LOGGER.error("Error executing task", e); ++ } ++ } ++ } ++ public static <T> T processQueueWhileWaiting(CompletableFuture <T> future) { ++ try { ++ if (isMainThread()) { ++ while (!future.isDone()) { ++ try { ++ return future.get(1, TimeUnit.MILLISECONDS); ++ } catch (TimeoutException ignored) { ++ processQueue(); ++ } ++ } ++ } ++ return future.get(); ++ } catch (Exception e) { ++ throw new RuntimeException(e); ++ } ++ } ++ ++ public static void ensureMain(Runnable run) { ++ ensureMain(null, run); ++ } ++ /** ++ * Ensures the target code is running on the main thread ++ * @param reason ++ * @param run ++ */ ++ public static void ensureMain(String reason, Runnable run) { ++ if (!isMainThread()) { ++ if (reason != null) { ++ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException()); ++ } ++ getProcessQueue().add(run); ++ return; ++ } ++ run.run(); ++ } ++ ++ private static Queue<Runnable> getProcessQueue() { ++ return MinecraftServer.getServer().processQueue; ++ } ++ ++ public static <T> T ensureMain(Supplier<T> run) { ++ return ensureMain(null, run); ++ } ++ /** ++ * Ensures the target code is running on the main thread ++ * @param reason ++ * @param run ++ * @param <T> ++ * @return ++ */ ++ public static <T> T ensureMain(String reason, Supplier<T> run) { ++ if (!isMainThread()) { ++ if (reason != null) { ++ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "! Blocking thread until it returns ", new IllegalStateException()); ++ } ++ Waitable<T> wait = new Waitable<T>() { ++ @Override ++ protected T evaluate() { ++ return run.get(); ++ } ++ }; ++ getProcessQueue().add(wait); ++ try { ++ return wait.get(); ++ } catch (InterruptedException | ExecutionException e) { ++ MinecraftServer.LOGGER.warn("Encountered exception", e); ++ } ++ return null; ++ } ++ return run.get(); ++ } ++ ++ /** ++ * Calculates distance between 2 entities ++ * @param e1 ++ * @param e2 ++ * @return ++ */ ++ public static double distance(Entity e1, Entity e2) { ++ return Math.sqrt(distanceSq(e1, e2)); ++ } ++ ++ ++ /** ++ * Calculates distance between 2 block positions ++ * @param e1 ++ * @param e2 ++ * @return ++ */ ++ public static double distance(BlockPos e1, BlockPos e2) { ++ return Math.sqrt(distanceSq(e1, e2)); ++ } ++ ++ /** ++ * Gets the distance between 2 positions ++ * @param x1 ++ * @param y1 ++ * @param z1 ++ * @param x2 ++ * @param y2 ++ * @param z2 ++ * @return ++ */ ++ public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) { ++ return Math.sqrt(distanceSq(x1, y1, z1, x2, y2, z2)); ++ } ++ ++ /** ++ * Get's the distance squared between 2 entities ++ * @param e1 ++ * @param e2 ++ * @return ++ */ ++ public static double distanceSq(Entity e1, Entity e2) { ++ return distanceSq(e1.getX(),e1.getY(),e1.getZ(), e2.getX(),e2.getY(),e2.getZ()); ++ } ++ ++ /** ++ * Gets the distance sqaured between 2 block positions ++ * @param pos1 ++ * @param pos2 ++ * @return ++ */ ++ public static double distanceSq(BlockPos pos1, BlockPos pos2) { ++ return distanceSq(pos1.getX(), pos1.getY(), pos1.getZ(), pos2.getX(), pos2.getY(), pos2.getZ()); ++ } ++ ++ /** ++ * Gets the distance squared between 2 positions ++ * @param x1 ++ * @param y1 ++ * @param z1 ++ * @param x2 ++ * @param y2 ++ * @param z2 ++ * @return ++ */ ++ public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) { ++ return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2); ++ } ++ ++ /** ++ * Converts a NMS World/BlockPosition to Bukkit Location ++ * @param world ++ * @param x ++ * @param y ++ * @param z ++ * @return ++ */ ++ public static Location toLocation(Level world, double x, double y, double z) { ++ return new Location(world.getWorld(), x, y, z); ++ } ++ ++ /** ++ * Converts a NMS World/BlockPosition to Bukkit Location ++ * @param world ++ * @param pos ++ * @return ++ */ ++ public static Location toLocation(Level world, BlockPos pos) { ++ return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); ++ } ++ ++ /** ++ * Converts an NMS entity's current location to a Bukkit Location ++ * @param entity ++ * @return ++ */ ++ public static Location toLocation(Entity entity) { ++ return new Location(entity.getCommandSenderWorld().getWorld(), entity.getX(), entity.getY(), entity.getZ()); ++ } ++ ++ public static BlockPos toBlockPosition(Location loc) { ++ return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); ++ } ++ ++ public static BlockPos toBlockPos(Position pos) { ++ return new BlockPos(pos.blockX(), pos.blockY(), pos.blockZ()); ++ } ++ ++ public static FinePosition toPosition(Vec3 vector) { ++ return Position.fine(vector.x, vector.y, vector.z); ++ } ++ ++ public static BlockPosition toPosition(Vec3i vector) { ++ return Position.block(vector.getX(), vector.getY(), vector.getZ()); ++ } ++ ++ public static Vec3 toVec3(Position position) { ++ return new Vec3(position.x(), position.y(), position.z()); ++ } ++ ++ public static boolean isEdgeOfChunk(BlockPos pos) { ++ final int modX = pos.getX() & 15; ++ final int modZ = pos.getZ() & 15; ++ return (modX == 0 || modX == 15 || modZ == 0 || modZ == 15); ++ } ++ ++ /** ++ * Posts a task to be executed asynchronously ++ * @param run ++ */ ++ public static void scheduleAsyncTask(Runnable run) { ++ asyncExecutor.execute(run); ++ } ++ ++ @Nonnull ++ public static ServerLevel getNMSWorld(@Nonnull org.bukkit.World world) { ++ return ((CraftWorld) world).getHandle(); ++ } ++ ++ public static ServerLevel getNMSWorld(@Nonnull org.bukkit.entity.Entity entity) { ++ return getNMSWorld(entity.getWorld()); ++ } ++ ++ public static BlockFace toBukkitBlockFace(Direction enumDirection) { ++ switch (enumDirection) { ++ case DOWN: ++ return BlockFace.DOWN; ++ case UP: ++ return BlockFace.UP; ++ case NORTH: ++ return BlockFace.NORTH; ++ case SOUTH: ++ return BlockFace.SOUTH; ++ case WEST: ++ return BlockFace.WEST; ++ case EAST: ++ return BlockFace.EAST; ++ default: ++ return null; ++ } ++ } ++ ++ public static int getTicketLevelFor(net.minecraft.world.level.chunk.status.ChunkStatus status) { ++ return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status); ++ } ++ ++ @NotNull ++ public static <T> List<T> copyListAndAdd(@NotNull final List<T> original, ++ @NotNull final T newElement) { ++ return ImmutableList.<T>builderWithExpectedSize(original.size() + 1) ++ .addAll(original) ++ .add(newElement) ++ .build(); ++ } ++ ++ @NotNull ++ public static <T> List<T> copyListAndRemoveIf(@NotNull final List<T> original, ++ @NotNull final Predicate<T> removalPredicate) { ++ final ImmutableList.Builder<T> builder = ImmutableList.builderWithExpectedSize(original.size()); ++ for (int i = 0; i < original.size(); i++) { ++ final T value = original.get(i); ++ if (removalPredicate.test(value)) continue; ++ ++ builder.add(value); ++ } ++ ++ return builder.build(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f7114d5b8f2f93f62883e24da29afaf9f74ee1a6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java +@@ -0,0 +1,24 @@ ++package io.papermc.paper.util; ++ ++import org.bukkit.plugin.java.JavaPlugin; ++import org.bukkit.plugin.java.PluginClassLoader; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.Optional; ++ ++public class StackWalkerUtil { ++ ++ @Nullable ++ public static JavaPlugin getFirstPluginCaller() { ++ Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) ++ .walk(stream -> stream ++ .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader) ++ .map((frame) -> { ++ PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader(); ++ return classLoader.getPlugin(); ++ }) ++ .findFirst()); ++ ++ return foundFrame.orElse(null); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/WorldUtil.java b/src/main/java/io/papermc/paper/util/WorldUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..67bb91fcfb532a919954cd9d7733d09a6c3fec35 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/WorldUtil.java +@@ -0,0 +1,46 @@ ++package io.papermc.paper.util; ++ ++import net.minecraft.world.level.LevelHeightAccessor; ++ ++public final class WorldUtil { ++ ++ // min, max are inclusive ++ ++ public static int getMaxSection(final LevelHeightAccessor world) { ++ return world.getMaxSection() - 1; // getMaxSection() is exclusive ++ } ++ ++ public static int getMinSection(final LevelHeightAccessor world) { ++ return world.getMinSection(); ++ } ++ ++ public static int getMaxLightSection(final LevelHeightAccessor world) { ++ return getMaxSection(world) + 1; ++ } ++ ++ public static int getMinLightSection(final LevelHeightAccessor world) { ++ return getMinSection(world) - 1; ++ } ++ ++ ++ ++ public static int getTotalSections(final LevelHeightAccessor world) { ++ return getMaxSection(world) - getMinSection(world) + 1; ++ } ++ ++ public static int getTotalLightSections(final LevelHeightAccessor world) { ++ return getMaxLightSection(world) - getMinLightSection(world) + 1; ++ } ++ ++ public static int getMinBlockY(final LevelHeightAccessor world) { ++ return getMinSection(world) << 4; ++ } ++ ++ public static int getMaxBlockY(final LevelHeightAccessor world) { ++ return (getMaxSection(world) << 4) | 15; ++ } ++ ++ private WorldUtil() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0fd814f1d65c111266a2b20f86561839a4cef755 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java +@@ -0,0 +1,334 @@ ++package io.papermc.paper.util.maplist; ++ ++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2IntMap; ++import org.bukkit.Bukkit; ++import java.util.Arrays; ++import java.util.NoSuchElementException; ++ ++public final class IteratorSafeOrderedReferenceSet<E> { ++ ++ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0; ++ ++ protected final Reference2IntLinkedOpenHashMap<E> indexMap; ++ protected int firstInvalidIndex = -1; ++ ++ /* list impl */ ++ protected E[] listElements; ++ protected int listSize; ++ ++ protected final double maxFragFactor; ++ ++ protected int iteratorCount; ++ ++ private final boolean threadRestricted; ++ ++ public IteratorSafeOrderedReferenceSet() { ++ this(16, 0.75f, 16, 0.2); ++ } ++ ++ public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) { ++ this(16, 0.75f, 16, 0.2, threadRestricted); ++ } ++ ++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, ++ final double maxFragFactor) { ++ this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false); ++ } ++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, ++ final double maxFragFactor, final boolean threadRestricted) { ++ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor); ++ this.indexMap.defaultReturnValue(-1); ++ this.maxFragFactor = maxFragFactor; ++ this.listElements = (E[])new Object[arrayCapacity]; ++ this.threadRestricted = threadRestricted; ++ } ++ ++ /* ++ public void check() { ++ int iterated = 0; ++ ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>(); ++ if (this.listElements != null) { ++ for (int i = 0; i < this.listSize; ++i) { ++ Object obj = this.listElements[i]; ++ if (obj != null) { ++ iterated++; ++ if (!check.add((E)obj)) { ++ throw new IllegalStateException("contains duplicate"); ++ } ++ if (!this.contains((E)obj)) { ++ throw new IllegalStateException("desync"); ++ } ++ } ++ } ++ } ++ ++ if (iterated != this.size()) { ++ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size()); ++ } ++ ++ check.clear(); ++ iterated = 0; ++ for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ final E element = iterator.next(); ++ iterated++; ++ if (!check.add(element)) { ++ throw new IllegalStateException("contains duplicate (iterator is wrong)"); ++ } ++ if (!this.contains(element)) { ++ throw new IllegalStateException("desync (iterator is wrong)"); ++ } ++ } ++ ++ if (iterated != this.size()) { ++ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size()); ++ } ++ } ++ */ ++ ++ protected final boolean allowSafeIteration() { ++ return !this.threadRestricted || Bukkit.isPrimaryThread(); ++ } ++ ++ protected final double getFragFactor() { ++ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize); ++ } ++ ++ public int createRawIterator() { ++ if (this.allowSafeIteration()) { ++ ++this.iteratorCount; ++ } ++ if (this.indexMap.isEmpty()) { ++ return -1; ++ } else { ++ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0; ++ } ++ } ++ ++ public int advanceRawIterator(final int index) { ++ final E[] elements = this.listElements; ++ int ret = index + 1; ++ for (int len = this.listSize; ret < len; ++ret) { ++ if (elements[ret] != null) { ++ return ret; ++ } ++ } ++ ++ return -1; ++ } ++ ++ public void finishRawIterator() { ++ if (this.allowSafeIteration() && --this.iteratorCount == 0) { ++ if (this.getFragFactor() >= this.maxFragFactor) { ++ this.defrag(); ++ } ++ } ++ } ++ ++ public boolean remove(final E element) { ++ final int index = this.indexMap.removeInt(element); ++ if (index >= 0) { ++ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) { ++ this.firstInvalidIndex = index; ++ } ++ if (this.listElements[index] != element) { ++ throw new IllegalStateException(); ++ } ++ this.listElements[index] = null; ++ if (this.allowSafeIteration() && this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) { ++ this.defrag(); ++ } ++ //this.check(); ++ return true; ++ } ++ return false; ++ } ++ ++ public boolean contains(final E element) { ++ return this.indexMap.containsKey(element); ++ } ++ ++ public boolean add(final E element) { ++ final int listSize = this.listSize; ++ ++ final int previous = this.indexMap.putIfAbsent(element, listSize); ++ if (previous != -1) { ++ return false; ++ } ++ ++ if (listSize >= this.listElements.length) { ++ this.listElements = Arrays.copyOf(this.listElements, listSize * 2); ++ } ++ this.listElements[listSize] = element; ++ this.listSize = listSize + 1; ++ ++ //this.check(); ++ return true; ++ } ++ ++ protected void defrag() { ++ if (this.firstInvalidIndex < 0) { ++ return; // nothing to do ++ } ++ ++ if (this.indexMap.isEmpty()) { ++ Arrays.fill(this.listElements, 0, this.listSize, null); ++ this.listSize = 0; ++ this.firstInvalidIndex = -1; ++ //this.check(); ++ return; ++ } ++ ++ final E[] backingArray = this.listElements; ++ ++ int lastValidIndex; ++ java.util.Iterator<Reference2IntMap.Entry<E>> iterator; ++ ++ if (this.firstInvalidIndex == 0) { ++ iterator = this.indexMap.reference2IntEntrySet().fastIterator(); ++ lastValidIndex = 0; ++ } else { ++ lastValidIndex = this.firstInvalidIndex; ++ final E key = backingArray[lastValidIndex - 1]; ++ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() { ++ @Override ++ public int getIntValue() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int setValue(int i) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public E getKey() { ++ return key; ++ } ++ }); ++ } ++ ++ while (iterator.hasNext()) { ++ final Reference2IntMap.Entry<E> entry = iterator.next(); ++ ++ final int newIndex = lastValidIndex++; ++ backingArray[newIndex] = entry.getKey(); ++ entry.setValue(newIndex); ++ } ++ ++ // cleanup end ++ Arrays.fill(backingArray, lastValidIndex, this.listSize, null); ++ this.listSize = lastValidIndex; ++ this.firstInvalidIndex = -1; ++ //this.check(); ++ } ++ ++ public E rawGet(final int index) { ++ return this.listElements[index]; ++ } ++ ++ public int size() { ++ // always returns the correct amount - listSize can be different ++ return this.indexMap.size(); ++ } ++ ++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator() { ++ return this.iterator(0); ++ } ++ ++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) { ++ if (this.allowSafeIteration()) { ++ ++this.iteratorCount; ++ } ++ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); ++ } ++ ++ public java.util.Iterator<E> unsafeIterator() { ++ return this.unsafeIterator(0); ++ } ++ public java.util.Iterator<E> unsafeIterator(final int flags) { ++ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); ++ } ++ ++ public static interface Iterator<E> extends java.util.Iterator<E> { ++ ++ public void finishedIterating(); ++ ++ } ++ ++ protected static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> { ++ ++ protected final IteratorSafeOrderedReferenceSet<E> set; ++ protected final boolean canFinish; ++ protected final int maxIndex; ++ protected int nextIndex; ++ protected E pendingValue; ++ protected boolean finished; ++ protected E lastReturned; ++ ++ protected BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) { ++ this.set = set; ++ this.canFinish = canFinish; ++ this.maxIndex = maxIndex; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ if (this.finished) { ++ return false; ++ } ++ if (this.pendingValue != null) { ++ return true; ++ } ++ ++ final E[] elements = this.set.listElements; ++ int index, len; ++ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) { ++ final E element = elements[index]; ++ if (element != null) { ++ this.pendingValue = element; ++ this.nextIndex = index + 1; ++ return true; ++ } ++ } ++ ++ this.nextIndex = index; ++ return false; ++ } ++ ++ @Override ++ public E next() { ++ if (!this.hasNext()) { ++ throw new NoSuchElementException(); ++ } ++ final E ret = this.pendingValue; ++ ++ this.pendingValue = null; ++ this.lastReturned = ret; ++ ++ return ret; ++ } ++ ++ @Override ++ public void remove() { ++ final E lastReturned = this.lastReturned; ++ if (lastReturned == null) { ++ throw new IllegalStateException(); ++ } ++ this.lastReturned = null; ++ this.set.remove(lastReturned); ++ } ++ ++ @Override ++ public void finishedIterating() { ++ if (this.finished || !this.canFinish) { ++ throw new IllegalStateException(); ++ } ++ this.lastReturned = null; ++ this.finished = true; ++ if (this.set.allowSafeIteration()) { ++ this.set.finishRawIterator(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java +new file mode 100644 +index 0000000000000000000000000000000000000000..470402573bc31106d5a63e415b958fb7f9c36aa9 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java +@@ -0,0 +1,297 @@ ++package io.papermc.paper.util.misc; ++ ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++ ++public final class Delayed26WayDistancePropagator3D { ++ ++ // this map is considered "stale" unless updates are propagated. ++ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f); ++ ++ // this map is never stale ++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); ++ ++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when ++ // propagating updates ++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); ++ ++ @FunctionalInterface ++ public static interface LevelChangeCallback { ++ ++ /** ++ * This can be called for intermediate updates. So do not rely on newLevel being close to or ++ * the exact level that is expected after a full propagation has occured. ++ */ ++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); ++ ++ } ++ ++ protected final LevelChangeCallback changeCallback; ++ ++ public Delayed26WayDistancePropagator3D() { ++ this(null); ++ } ++ ++ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) { ++ this.changeCallback = changeCallback; ++ } ++ ++ public int getLevel(final long pos) { ++ return this.levels.get(pos); ++ } ++ ++ public int getLevel(final int x, final int y, final int z) { ++ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z)); ++ } ++ ++ public void setSource(final int x, final int y, final int z, final int level) { ++ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level); ++ } ++ ++ public void setSource(final long coordinate, final int level) { ++ if ((level & 63) != level || level == 0) { ++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level); ++ } ++ ++ final byte byteLevel = (byte)level; ++ final byte oldLevel = this.sources.put(coordinate, byteLevel); ++ ++ if (oldLevel == byteLevel) { ++ return; // nothing to do ++ } ++ ++ // queue to update later ++ this.updatedSources.add(coordinate); ++ } ++ ++ public void removeSource(final int x, final int y, final int z) { ++ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z)); ++ } ++ ++ public void removeSource(final long coordinate) { ++ if (this.sources.remove(coordinate) != 0) { ++ this.updatedSources.add(coordinate); ++ } ++ } ++ ++ // queues used for BFS propagating levels ++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; ++ { ++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { ++ this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); ++ } ++ } ++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; ++ { ++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { ++ this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); ++ } ++ } ++ protected long levelIncreaseWorkQueueBitset; ++ protected long levelRemoveWorkQueueBitset; ++ ++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { ++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level]; ++ queue.queuedCoordinates.enqueue(coordinate); ++ queue.queuedLevels.enqueue(level); ++ ++ this.levelIncreaseWorkQueueBitset |= (1L << level); ++ } ++ ++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { ++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index]; ++ queue.queuedCoordinates.enqueue(coordinate); ++ queue.queuedLevels.enqueue(level); ++ ++ this.levelIncreaseWorkQueueBitset |= (1L << index); ++ } ++ ++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { ++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level]; ++ queue.queuedCoordinates.enqueue(coordinate); ++ queue.queuedLevels.enqueue(level); ++ ++ this.levelRemoveWorkQueueBitset |= (1L << level); ++ } ++ ++ public boolean propagateUpdates() { ++ if (this.updatedSources.isEmpty()) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { ++ final long coordinate = iterator.nextLong(); ++ ++ final byte currentLevel = this.levels.get(coordinate); ++ final byte updatedSource = this.sources.get(coordinate); ++ ++ if (currentLevel == updatedSource) { ++ continue; ++ } ++ ret = true; ++ ++ if (updatedSource > currentLevel) { ++ // level increase ++ this.addToIncreaseWorkQueue(coordinate, updatedSource); ++ } else { ++ // level decrease ++ this.addToRemoveWorkQueue(coordinate, currentLevel); ++ // if the current coordinate is a source, then the decrease propagation will detect that and queue ++ // the source propagation ++ } ++ } ++ ++ this.updatedSources.clear(); ++ ++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions ++ // make the removes remove less) ++ this.propagateIncreases(); ++ ++ // now we propagate the decreases (which will then re-propagate clobbered sources) ++ this.propagateDecreases(); ++ ++ return ret; ++ } ++ ++ protected void propagateIncreases() { ++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); ++ this.levelIncreaseWorkQueueBitset != 0L; ++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { ++ ++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; ++ while (!queue.queuedLevels.isEmpty()) { ++ final long coordinate = queue.queuedCoordinates.removeFirstLong(); ++ byte level = queue.queuedLevels.removeFirstByte(); ++ ++ final boolean neighbourCheck = level < 0; ++ ++ final byte currentLevel; ++ if (neighbourCheck) { ++ level = (byte)-level; ++ currentLevel = this.levels.get(coordinate); ++ } else { ++ currentLevel = this.levels.putIfGreater(coordinate, level); ++ } ++ ++ if (neighbourCheck) { ++ // used when propagating from decrease to indicate that this level needs to check its neighbours ++ // this means the level at coordinate could be equal, but would still need neighbours checked ++ ++ if (currentLevel != level) { ++ // something caused the level to change, which means something propagated to it (which means ++ // us propagating here is redundant), or something removed the level (which means we ++ // cannot propagate further) ++ continue; ++ } ++ } else if (currentLevel >= level) { ++ // something higher/equal propagated ++ continue; ++ } ++ if (this.changeCallback != null) { ++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); ++ } ++ ++ if (level == 1) { ++ // can't propagate 0 to neighbours ++ continue; ++ } ++ ++ // propagate to neighbours ++ final byte neighbourLevel = (byte)(level - 1); ++ final int x = CoordinateUtils.getChunkSectionX(coordinate); ++ final int y = CoordinateUtils.getChunkSectionY(coordinate); ++ final int z = CoordinateUtils.getChunkSectionZ(coordinate); ++ ++ for (int dy = -1; dy <= 1; ++dy) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if ((dy | dz | dx) == 0) { ++ // already propagated to coordinate ++ continue; ++ } ++ ++ // sure we can check the neighbour level in the map right now and avoid a propagation, ++ // but then we would still have to recheck it when popping the value off of the queue! ++ // so just avoid the double lookup ++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); ++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ protected void propagateDecreases() { ++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); ++ this.levelRemoveWorkQueueBitset != 0L; ++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { ++ ++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; ++ while (!queue.queuedLevels.isEmpty()) { ++ final long coordinate = queue.queuedCoordinates.removeFirstLong(); ++ final byte level = queue.queuedLevels.removeFirstByte(); ++ ++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); ++ if (currentLevel == 0) { ++ // something else removed ++ continue; ++ } ++ ++ if (currentLevel > level) { ++ // something higher propagated here or we hit the propagation of another source ++ // in the second case we need to re-propagate because we could have just clobbered another source's ++ // propagation ++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking ++ continue; ++ } ++ ++ if (this.changeCallback != null) { ++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); ++ } ++ ++ final byte source = this.sources.get(coordinate); ++ if (source != 0) { ++ // must re-propagate source later ++ this.addToIncreaseWorkQueue(coordinate, source); ++ } ++ ++ if (level == 0) { ++ // can't propagate -1 to neighbours ++ // we have to check neighbours for removing 1 just in case the neighbour is 2 ++ continue; ++ } ++ ++ // propagate to neighbours ++ final byte neighbourLevel = (byte)(level - 1); ++ final int x = CoordinateUtils.getChunkSectionX(coordinate); ++ final int y = CoordinateUtils.getChunkSectionY(coordinate); ++ final int z = CoordinateUtils.getChunkSectionZ(coordinate); ++ ++ for (int dy = -1; dy <= 1; ++dy) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if ((dy | dz | dx) == 0) { ++ // already propagated to coordinate ++ continue; ++ } ++ ++ // sure we can check the neighbour level in the map right now and avoid a propagation, ++ // but then we would still have to recheck it when popping the value off of the queue! ++ // so just avoid the double lookup ++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); ++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); ++ } ++ } ++ } ++ } ++ } ++ ++ // propagate sources we clobbered in the process ++ this.propagateIncreases(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java +new file mode 100644 +index 0000000000000000000000000000000000000000..808d1449ac44ae86a650932365081fbaf178d141 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java +@@ -0,0 +1,718 @@ ++package io.papermc.paper.util.misc; ++ ++import it.unimi.dsi.fastutil.HashCommon; ++import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++import io.papermc.paper.util.MCUtil; ++ ++public final class Delayed8WayDistancePropagator2D { ++ ++ // Test ++ /* ++ protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) { ++ int got = test.getLevel(x, z); ++ ++ int expect = 0; ++ Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet(); ++ if (nearest != null) { ++ for (Object _obj : nearest) { ++ if (_obj instanceof Ticket) { ++ Ticket ticket = (Ticket)_obj; ++ long ticketCoord = reference.getLastCoordinate(ticket); ++ int viewDistance = reference.getLastViewDistance(ticket); ++ int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x), ++ com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z)); ++ int level = viewDistance - distance; ++ if (level > expect) { ++ expect = level; ++ } ++ } ++ } ++ } ++ ++ if (expect != got) { ++ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got); ++ } ++ } ++ ++ static class Ticket { ++ ++ int x; ++ int z; ++ ++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> empty ++ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); ++ ++ } ++ ++ public static void main(final String[] args) { ++ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() { ++ @Override ++ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) { ++ return object.empty; ++ } ++ }; ++ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D(); ++ ++ final int maxDistance = 64; ++ // test origin ++ { ++ Ticket originTicket = new Ticket(); ++ int originDistance = 31; ++ // test single source ++ reference.add(originTicket, 0, 0, originDistance); ++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate ++ for (int dx = -originDistance; dx <= originDistance; ++dx) { ++ for (int dz = -originDistance; dz <= originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ // test single source decrease ++ reference.update(originTicket, 0, 0, originDistance/2); ++ test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate ++ for (int dx = -originDistance; dx <= originDistance; ++dx) { ++ for (int dz = -originDistance; dz <= originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ // test source increase ++ originDistance = 2*originDistance; ++ reference.update(originTicket, 0, 0, originDistance); ++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate ++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { ++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ reference.remove(originTicket); ++ test.removeSource(0, 0); test.propagateUpdates(); ++ } ++ ++ // test multiple sources at origin ++ { ++ int originDistance = 31; ++ java.util.List<Ticket> list = new java.util.ArrayList<>(); ++ for (int i = 0; i < 10; ++i) { ++ Ticket a = new Ticket(); ++ list.add(a); ++ a.x = (i & 1) == 1 ? -i : i; ++ a.z = (i & 1) == 1 ? -i : i; ++ } ++ for (Ticket ticket : list) { ++ reference.add(ticket, ticket.x, ticket.z, originDistance); ++ test.setSource(ticket.x, ticket.z, originDistance); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { ++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ // test ticket level decrease ++ ++ for (Ticket ticket : list) { ++ reference.update(ticket, ticket.x, ticket.z, originDistance/2); ++ test.setSource(ticket.x, ticket.z, originDistance/2); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { ++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ // test ticket level increase ++ ++ for (Ticket ticket : list) { ++ reference.update(ticket, ticket.x, ticket.z, originDistance*2); ++ test.setSource(ticket.x, ticket.z, originDistance*2); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { ++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ // test ticket remove ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ if ((i & 3) != 0) { ++ continue; ++ } ++ Ticket ticket = list.get(i); ++ reference.remove(ticket); ++ test.removeSource(ticket.x, ticket.z); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { ++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ } ++ ++ // now test at coordinate offsets ++ // test offset ++ { ++ Ticket originTicket = new Ticket(); ++ int originDistance = 31; ++ int offX = 54432; ++ int offZ = -134567; ++ // test single source ++ reference.add(originTicket, offX, offZ, originDistance); ++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate ++ for (int dx = -originDistance; dx <= originDistance; ++dx) { ++ for (int dz = -originDistance; dz <= originDistance; ++dz) { ++ test(dx + offX, dz + offZ, reference, test); ++ } ++ } ++ // test single source decrease ++ reference.update(originTicket, offX, offZ, originDistance/2); ++ test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate ++ for (int dx = -originDistance; dx <= originDistance; ++dx) { ++ for (int dz = -originDistance; dz <= originDistance; ++dz) { ++ test(dx + offX, dz + offZ, reference, test); ++ } ++ } ++ // test source increase ++ originDistance = 2*originDistance; ++ reference.update(originTicket, offX, offZ, originDistance); ++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate ++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { ++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { ++ test(dx + offX, dz + offZ, reference, test); ++ } ++ } ++ ++ reference.remove(originTicket); ++ test.removeSource(offX, offZ); test.propagateUpdates(); ++ } ++ ++ // test multiple sources at origin ++ { ++ int originDistance = 31; ++ int offX = 54432; ++ int offZ = -134567; ++ java.util.List<Ticket> list = new java.util.ArrayList<>(); ++ for (int i = 0; i < 10; ++i) { ++ Ticket a = new Ticket(); ++ list.add(a); ++ a.x = offX + ((i & 1) == 1 ? -i : i); ++ a.z = offZ + ((i & 1) == 1 ? -i : i); ++ } ++ for (Ticket ticket : list) { ++ reference.add(ticket, ticket.x, ticket.z, originDistance); ++ test.setSource(ticket.x, ticket.z, originDistance); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { ++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ // test ticket level decrease ++ ++ for (Ticket ticket : list) { ++ reference.update(ticket, ticket.x, ticket.z, originDistance/2); ++ test.setSource(ticket.x, ticket.z, originDistance/2); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { ++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ // test ticket level increase ++ ++ for (Ticket ticket : list) { ++ reference.update(ticket, ticket.x, ticket.z, originDistance*2); ++ test.setSource(ticket.x, ticket.z, originDistance*2); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { ++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ ++ // test ticket remove ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ if ((i & 3) != 0) { ++ continue; ++ } ++ Ticket ticket = list.get(i); ++ reference.remove(ticket); ++ test.removeSource(ticket.x, ticket.z); ++ } ++ test.propagateUpdates(); ++ ++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { ++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { ++ test(dx, dz, reference, test); ++ } ++ } ++ } ++ } ++ */ ++ ++ // this map is considered "stale" unless updates are propagated. ++ protected final LevelMap levels = new LevelMap(8192*2, 0.6f); ++ ++ // this map is never stale ++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); ++ ++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when ++ // propagating updates ++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); ++ ++ @FunctionalInterface ++ public static interface LevelChangeCallback { ++ ++ /** ++ * This can be called for intermediate updates. So do not rely on newLevel being close to or ++ * the exact level that is expected after a full propagation has occured. ++ */ ++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); ++ ++ } ++ ++ protected final LevelChangeCallback changeCallback; ++ ++ public Delayed8WayDistancePropagator2D() { ++ this(null); ++ } ++ ++ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) { ++ this.changeCallback = changeCallback; ++ } ++ ++ public int getLevel(final long pos) { ++ return this.levels.get(pos); ++ } ++ ++ public int getLevel(final int x, final int z) { ++ return this.levels.get(MCUtil.getCoordinateKey(x, z)); ++ } ++ ++ public void setSource(final int x, final int z, final int level) { ++ this.setSource(MCUtil.getCoordinateKey(x, z), level); ++ } ++ ++ public void setSource(final long coordinate, final int level) { ++ if ((level & 63) != level || level == 0) { ++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level); ++ } ++ ++ final byte byteLevel = (byte)level; ++ final byte oldLevel = this.sources.put(coordinate, byteLevel); ++ ++ if (oldLevel == byteLevel) { ++ return; // nothing to do ++ } ++ ++ // queue to update later ++ this.updatedSources.add(coordinate); ++ } ++ ++ public void removeSource(final int x, final int z) { ++ this.removeSource(MCUtil.getCoordinateKey(x, z)); ++ } ++ ++ public void removeSource(final long coordinate) { ++ if (this.sources.remove(coordinate) != 0) { ++ this.updatedSources.add(coordinate); ++ } ++ } ++ ++ // queues used for BFS propagating levels ++ protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64]; ++ { ++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { ++ this.levelIncreaseWorkQueues[i] = new WorkQueue(); ++ } ++ } ++ protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64]; ++ { ++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { ++ this.levelRemoveWorkQueues[i] = new WorkQueue(); ++ } ++ } ++ protected long levelIncreaseWorkQueueBitset; ++ protected long levelRemoveWorkQueueBitset; ++ ++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { ++ final WorkQueue queue = this.levelIncreaseWorkQueues[level]; ++ queue.queuedCoordinates.enqueue(coordinate); ++ queue.queuedLevels.enqueue(level); ++ ++ this.levelIncreaseWorkQueueBitset |= (1L << level); ++ } ++ ++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { ++ final WorkQueue queue = this.levelIncreaseWorkQueues[index]; ++ queue.queuedCoordinates.enqueue(coordinate); ++ queue.queuedLevels.enqueue(level); ++ ++ this.levelIncreaseWorkQueueBitset |= (1L << index); ++ } ++ ++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { ++ final WorkQueue queue = this.levelRemoveWorkQueues[level]; ++ queue.queuedCoordinates.enqueue(coordinate); ++ queue.queuedLevels.enqueue(level); ++ ++ this.levelRemoveWorkQueueBitset |= (1L << level); ++ } ++ ++ public boolean propagateUpdates() { ++ if (this.updatedSources.isEmpty()) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { ++ final long coordinate = iterator.nextLong(); ++ ++ final byte currentLevel = this.levels.get(coordinate); ++ final byte updatedSource = this.sources.get(coordinate); ++ ++ if (currentLevel == updatedSource) { ++ continue; ++ } ++ ret = true; ++ ++ if (updatedSource > currentLevel) { ++ // level increase ++ this.addToIncreaseWorkQueue(coordinate, updatedSource); ++ } else { ++ // level decrease ++ this.addToRemoveWorkQueue(coordinate, currentLevel); ++ // if the current coordinate is a source, then the decrease propagation will detect that and queue ++ // the source propagation ++ } ++ } ++ ++ this.updatedSources.clear(); ++ ++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions ++ // make the removes remove less) ++ this.propagateIncreases(); ++ ++ // now we propagate the decreases (which will then re-propagate clobbered sources) ++ this.propagateDecreases(); ++ ++ return ret; ++ } ++ ++ protected void propagateIncreases() { ++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); ++ this.levelIncreaseWorkQueueBitset != 0L; ++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { ++ ++ final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; ++ while (!queue.queuedLevels.isEmpty()) { ++ final long coordinate = queue.queuedCoordinates.removeFirstLong(); ++ byte level = queue.queuedLevels.removeFirstByte(); ++ ++ final boolean neighbourCheck = level < 0; ++ ++ final byte currentLevel; ++ if (neighbourCheck) { ++ level = (byte)-level; ++ currentLevel = this.levels.get(coordinate); ++ } else { ++ currentLevel = this.levels.putIfGreater(coordinate, level); ++ } ++ ++ if (neighbourCheck) { ++ // used when propagating from decrease to indicate that this level needs to check its neighbours ++ // this means the level at coordinate could be equal, but would still need neighbours checked ++ ++ if (currentLevel != level) { ++ // something caused the level to change, which means something propagated to it (which means ++ // us propagating here is redundant), or something removed the level (which means we ++ // cannot propagate further) ++ continue; ++ } ++ } else if (currentLevel >= level) { ++ // something higher/equal propagated ++ continue; ++ } ++ if (this.changeCallback != null) { ++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); ++ } ++ ++ if (level == 1) { ++ // can't propagate 0 to neighbours ++ continue; ++ } ++ ++ // propagate to neighbours ++ final byte neighbourLevel = (byte)(level - 1); ++ final int x = (int)coordinate; ++ final int z = (int)(coordinate >>> 32); ++ ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ if ((dx | dz) == 0) { ++ // already propagated to coordinate ++ continue; ++ } ++ ++ // sure we can check the neighbour level in the map right now and avoid a propagation, ++ // but then we would still have to recheck it when popping the value off of the queue! ++ // so just avoid the double lookup ++ final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); ++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); ++ } ++ } ++ } ++ } ++ } ++ ++ protected void propagateDecreases() { ++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); ++ this.levelRemoveWorkQueueBitset != 0L; ++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { ++ ++ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; ++ while (!queue.queuedLevels.isEmpty()) { ++ final long coordinate = queue.queuedCoordinates.removeFirstLong(); ++ final byte level = queue.queuedLevels.removeFirstByte(); ++ ++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); ++ if (currentLevel == 0) { ++ // something else removed ++ continue; ++ } ++ ++ if (currentLevel > level) { ++ // something higher propagated here or we hit the propagation of another source ++ // in the second case we need to re-propagate because we could have just clobbered another source's ++ // propagation ++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking ++ continue; ++ } ++ ++ if (this.changeCallback != null) { ++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); ++ } ++ ++ final byte source = this.sources.get(coordinate); ++ if (source != 0) { ++ // must re-propagate source later ++ this.addToIncreaseWorkQueue(coordinate, source); ++ } ++ ++ if (level == 0) { ++ // can't propagate -1 to neighbours ++ // we have to check neighbours for removing 1 just in case the neighbour is 2 ++ continue; ++ } ++ ++ // propagate to neighbours ++ final byte neighbourLevel = (byte)(level - 1); ++ final int x = (int)coordinate; ++ final int z = (int)(coordinate >>> 32); ++ ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ if ((dx | dz) == 0) { ++ // already propagated to coordinate ++ continue; ++ } ++ ++ // sure we can check the neighbour level in the map right now and avoid a propagation, ++ // but then we would still have to recheck it when popping the value off of the queue! ++ // so just avoid the double lookup ++ final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); ++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); ++ } ++ } ++ } ++ } ++ ++ // propagate sources we clobbered in the process ++ this.propagateIncreases(); ++ } ++ ++ protected static final class LevelMap extends Long2ByteOpenHashMap { ++ public LevelMap() { ++ super(); ++ } ++ ++ public LevelMap(final int expected, final float loadFactor) { ++ super(expected, loadFactor); ++ } ++ ++ // copied from superclass ++ private int find(final long k) { ++ if (k == 0L) { ++ return this.containsNullKey ? this.n : -(this.n + 1); ++ } else { ++ final long[] key = this.key; ++ long curr; ++ int pos; ++ if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) { ++ return -(pos + 1); ++ } else if (k == curr) { ++ return pos; ++ } else { ++ while((curr = key[pos = pos + 1 & this.mask]) != 0L) { ++ if (k == curr) { ++ return pos; ++ } ++ } ++ ++ return -(pos + 1); ++ } ++ } ++ } ++ ++ // copied from superclass ++ private void insert(final int pos, final long k, final byte v) { ++ if (pos == this.n) { ++ this.containsNullKey = true; ++ } ++ ++ this.key[pos] = k; ++ this.value[pos] = v; ++ if (this.size++ >= this.maxFill) { ++ this.rehash(HashCommon.arraySize(this.size + 1, this.f)); ++ } ++ } ++ ++ // copied from superclass ++ public byte putIfGreater(final long key, final byte value) { ++ final int pos = this.find(key); ++ if (pos < 0) { ++ if (this.defRetValue < value) { ++ this.insert(-pos - 1, key, value); ++ } ++ return this.defRetValue; ++ } else { ++ final byte curr = this.value[pos]; ++ if (value > curr) { ++ this.value[pos] = value; ++ return curr; ++ } ++ return curr; ++ } ++ } ++ ++ // copied from superclass ++ private void removeEntry(final int pos) { ++ --this.size; ++ this.shiftKeys(pos); ++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { ++ this.rehash(this.n / 2); ++ } ++ } ++ ++ // copied from superclass ++ private void removeNullEntry() { ++ this.containsNullKey = false; ++ --this.size; ++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { ++ this.rehash(this.n / 2); ++ } ++ } ++ ++ // copied from superclass ++ public byte removeIfGreaterOrEqual(final long key, final byte value) { ++ if (key == 0L) { ++ if (!this.containsNullKey) { ++ return this.defRetValue; ++ } ++ final byte current = this.value[this.n]; ++ if (value >= current) { ++ this.removeNullEntry(); ++ return current; ++ } ++ return current; ++ } else { ++ long[] keys = this.key; ++ byte[] values = this.value; ++ long curr; ++ int pos; ++ if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) { ++ return this.defRetValue; ++ } else if (key == curr) { ++ final byte current = values[pos]; ++ if (value >= current) { ++ this.removeEntry(pos); ++ return current; ++ } ++ return current; ++ } else { ++ while((curr = keys[pos = pos + 1 & this.mask]) != 0L) { ++ if (key == curr) { ++ final byte current = values[pos]; ++ if (value >= current) { ++ this.removeEntry(pos); ++ return current; ++ } ++ return current; ++ } ++ } ++ ++ return this.defRetValue; ++ } ++ } ++ } ++ } ++ ++ protected static final class WorkQueue { ++ ++ public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque(); ++ public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque(); ++ ++ } ++ ++ protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue { ++ ++ /** ++ * Assumes non-empty. If empty, undefined behaviour. ++ */ ++ public long removeFirstLong() { ++ // copied from superclass ++ long t = this.array[this.start]; ++ if (++this.start == this.length) { ++ this.start = 0; ++ } ++ ++ return t; ++ } ++ } ++ ++ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue { ++ ++ /** ++ * Assumes non-empty. If empty, undefined behaviour. ++ */ ++ public byte removeFirstByte() { ++ // copied from superclass ++ byte t = this.array[this.start]; ++ if (++this.start == this.length) { ++ this.start = 0; ++ } ++ ++ return t; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java +@@ -0,0 +1,203 @@ ++package io.papermc.paper.util.player; ++ ++import com.destroystokyo.paper.util.maplist.ReferenceList; ++import io.papermc.paper.chunk.system.ChunkSystem; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.ChunkPos; ++ ++public final class NearbyPlayers { ++ ++ public static enum NearbyMapType { ++ GENERAL, ++ GENERAL_SMALL, ++ GENERAL_REALLY_SMALL, ++ TICK_VIEW_DISTANCE, ++ VIEW_DISTANCE; ++ } ++ ++ private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values(); ++ public static final int TOTAL_MAP_TYPES = MOB_TYPES.length; ++ ++ private static final int GENERAL_AREA_VIEW_DISTANCE = 33; ++ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; ++ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; ++ ++ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); ++ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); ++ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); ++ ++ private final ServerLevel world; ++ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>(); ++ private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>(); ++ ++ public NearbyPlayers(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES]; ++ if (this.players.putIfAbsent(player, newTrackers) != null) { ++ throw new IllegalStateException("Already have player " + player); ++ } ++ ++ final ChunkPos chunk = player.chunkPosition(); ++ ++ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) { ++ // use 0 for default, will be updated by tickPlayer ++ (newTrackers[i] = new TrackedPlayer(player, MOB_TYPES[i])).add(chunk.x, chunk.z, 0); ++ } ++ ++ // update view distances ++ this.tickPlayer(player); ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ final TrackedPlayer[] players = this.players.remove(player); ++ if (players == null) { ++ return; // May be called during teleportation before the player is actually placed ++ } ++ ++ for (final TrackedPlayer tracker : players) { ++ tracker.remove(); ++ } ++ } ++ ++ public void tickPlayer(final ServerPlayer player) { ++ final TrackedPlayer[] players = this.players.get(player); ++ if (players == null) { ++ throw new IllegalStateException("Don't have player " + player); ++ } ++ ++ final ChunkPos chunk = player.chunkPosition(); ++ ++ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE); ++ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE); ++ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); ++ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player)); ++ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player)); ++ } ++ ++ public TrackedChunk getChunk(final ChunkPos pos) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public TrackedChunk getChunk(final BlockPos pos) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public static final class TrackedChunk { ++ ++ public final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES]; ++ private int nonEmptyLists; ++ private int updateCount; ++ ++ public boolean isEmpty() { ++ return this.nonEmptyLists == 0; ++ } ++ ++ public int getUpdateCount() { ++ return this.updateCount; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) { ++ return this.players[type.ordinal()]; ++ } ++ ++ public void addPlayer(final ServerPlayer player, final NearbyMapType type) { ++ ++this.updateCount; ++ final int idx = type.ordinal(); ++ final ReferenceList<ServerPlayer> list = this.players[idx]; ++ if (list == null) { ++ ++this.nonEmptyLists; ++ (this.players[idx] = new ReferenceList<>()).add(player); ++ return; ++ } ++ ++ if (!list.add(player)) { ++ throw new IllegalStateException("Already contains player " + player); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player, final NearbyMapType type) { ++ ++this.updateCount; ++ final int idx = type.ordinal(); ++ final ReferenceList<ServerPlayer> list = this.players[idx]; ++ if (list == null) { ++ throw new IllegalStateException("Does not contain player " + player); ++ } ++ ++ if (!list.remove(player)) { ++ throw new IllegalStateException("Does not contain player " + player); ++ } ++ ++ if (list.size() == 0) { ++ this.players[idx] = null; ++ --this.nonEmptyLists; ++ } ++ } ++ } ++ ++ private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> { ++ ++ final NearbyMapType type; ++ ++ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) { ++ super(player); ++ this.type = type; ++ } ++ ++ @Override ++ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> { ++ return new TrackedChunk(); ++ }).addPlayer(parameter, this.type); ++ } ++ ++ @Override ++ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); ++ if (chunk == null) { ++ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey)); ++ } ++ ++ chunk.removePlayer(parameter, this.type); ++ ++ if (chunk.isEmpty()) { ++ NearbyPlayers.this.byChunk.remove(chunkKey); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java b/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d603887f4d0464f4463172fd79bcd5298d54983e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java +@@ -0,0 +1,232 @@ ++package io.papermc.paper.util.player; ++ ++import io.papermc.paper.util.IntegerUtil; ++ ++public abstract class SingleUserAreaMap<T> { ++ ++ private static final int NOT_SET = Integer.MIN_VALUE; ++ ++ private final T parameter; ++ private int lastChunkX = NOT_SET; ++ private int lastChunkZ = NOT_SET; ++ private int distance = NOT_SET; ++ ++ public SingleUserAreaMap(final T parameter) { ++ this.parameter = parameter; ++ } ++ ++ /* math sign function except 0 returns 1 */ ++ protected static int sign(int val) { ++ return 1 | (val >> (Integer.SIZE - 1)); ++ } ++ ++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ); ++ ++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ); ++ ++ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) { ++ final int maxX = chunkX + distance; ++ final int maxZ = chunkZ + distance; ++ ++ for (int cx = chunkX - distance; cx <= maxX; ++cx) { ++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { ++ this.addCallback(parameter, cx, cz); ++ } ++ } ++ } ++ ++ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) { ++ final int maxX = chunkX + distance; ++ final int maxZ = chunkZ + distance; ++ ++ for (int cx = chunkX - distance; cx <= maxX; ++cx) { ++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { ++ this.removeCallback(parameter, cx, cz); ++ } ++ } ++ } ++ ++ public final boolean add(final int chunkX, final int chunkZ, final int distance) { ++ if (distance < 0) { ++ throw new IllegalArgumentException(Integer.toString(distance)); ++ } ++ if (this.lastChunkX != NOT_SET) { ++ return false; ++ } ++ this.lastChunkX = chunkX; ++ this.lastChunkZ = chunkZ; ++ this.distance = distance; ++ ++ this.addToNew(this.parameter, chunkX, chunkZ, distance); ++ ++ return true; ++ } ++ ++ public final boolean update(final int toX, final int toZ, final int newViewDistance) { ++ if (newViewDistance < 0) { ++ throw new IllegalArgumentException(Integer.toString(newViewDistance)); ++ } ++ final int fromX = this.lastChunkX; ++ final int fromZ = this.lastChunkZ; ++ final int oldViewDistance = this.distance; ++ if (fromX == NOT_SET) { ++ return false; ++ } ++ ++ this.lastChunkX = toX; ++ this.lastChunkZ = toZ; ++ this.distance = newViewDistance; ++ ++ final T parameter = this.parameter; ++ ++ ++ final int dx = toX - fromX; ++ final int dz = toZ - fromZ; ++ ++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX); ++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); ++ ++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { ++ // teleported? ++ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance); ++ this.addToNew(parameter, toX, toZ, newViewDistance); ++ return true; ++ } ++ ++ if (oldViewDistance != newViewDistance) { ++ // remove loop ++ ++ final int oldMinX = fromX - oldViewDistance; ++ final int oldMinZ = fromZ - oldViewDistance; ++ final int oldMaxX = fromX + oldViewDistance; ++ final int oldMaxZ = fromZ + oldViewDistance; ++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) { ++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { ++ ++ // only remove if we're outside the new view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ // add loop ++ ++ final int newMinX = toX - newViewDistance; ++ final int newMinZ = toZ - newViewDistance; ++ final int newMaxX = toX + newViewDistance; ++ final int newMaxZ = toZ + newViewDistance; ++ for (int currX = newMinX; currX <= newMaxX; ++currX) { ++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { ++ ++ // only add if we're outside the old view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ // x axis is width ++ // z axis is height ++ // right refers to the x axis of where we moved ++ // top refers to the z axis of where we moved ++ ++ // same view distance ++ ++ // used for relative positioning ++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise ++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise ++ ++ // The area excluded by overlapping the two view distance squares creates four rectangles: ++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section ++ // and on the right the "added" section. ++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually ++ // exclusive to the regions they surround. ++ ++ // 4 points of the rectangle ++ int maxX; // exclusive ++ int minX; // inclusive ++ int maxZ; // exclusive ++ int minZ; // inclusive ++ ++ if (dx != 0) { ++ // handle right addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX + (oldViewDistance * right) + right; // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle up addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = toX - (oldViewDistance * right); // inclusive ++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive ++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dx != 0) { ++ // handle left removal ++ ++ maxX = toX - (oldViewDistance * right); // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle down removal ++ ++ maxX = fromX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = toZ - (oldViewDistance * up); // exclusive ++ minZ = fromZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public final boolean remove() { ++ final int chunkX = this.lastChunkX; ++ final int chunkZ = this.lastChunkZ; ++ final int distance = this.distance; ++ if (chunkX == NOT_SET) { ++ return false; ++ } ++ ++ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET; ++ ++ this.removeFromOld(this.parameter, chunkX, chunkZ, distance); ++ ++ return true; ++ } ++} +diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java +index 5135cd504ec5864a4603c004e748947a7d88d2b4..396f368a7e21a7c7b1630b4e20cdbc452c4b0f84 100644 +--- a/src/main/java/net/minecraft/Util.java ++++ b/src/main/java/net/minecraft/Util.java +@@ -133,7 +133,7 @@ public class Util { + } + + public static long getNanos() { +- return timeSource.getAsLong(); ++ return System.nanoTime(); // Paper + } + + public static long getEpochMillis() { +diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java +index 6b588a4e639da11edeb933ec2bc4afde8f0b47f1..1cbc42c44911b71dfadebc2d60e0e5cb9b6cafe6 100644 +--- a/src/main/java/net/minecraft/nbt/CompoundTag.java ++++ b/src/main/java/net/minecraft/nbt/CompoundTag.java +@@ -159,7 +159,7 @@ public class CompoundTag implements Tag { + return "TAG_Compound"; + } + }; +- private final Map<String, Tag> tags; ++ public final Map<String, Tag> tags; // Paper + + protected CompoundTag(Map<String, Tag> entries) { + this.tags = entries; +@@ -235,6 +235,10 @@ public class CompoundTag implements Tag { + this.tags.put(key, NbtUtils.createUUID(value)); + } + ++ ++ /** ++ * You must use {@link #hasUUID(String)} before or else it <b>will</b> throw an NPE. ++ */ + public UUID getUUID(String key) { + return NbtUtils.loadUUID(this.get(key)); + } +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 821de8e17031bf54edb61fc577d7c5a90aabfd2e..77985072928a1b892fb4f7dec1d0899324780082 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -121,6 +121,18 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> { + BandwidthDebugMonitor bandwidthDebugMonitor; + public String hostname = ""; // CraftBukkit - add field + ++ // Paper start - add utility methods ++ public final net.minecraft.server.level.ServerPlayer getPlayer() { ++ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) { ++ return impl.player; ++ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) { ++ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer(); ++ return player == null ? null : player.getHandle(); ++ } ++ return null; ++ } ++ // Paper end - add utility methods ++ + public Connection(PacketFlow side) { + this.receiving = side; + } +diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java +index e82263b4b48c4544a5ade6613ea284f4de2b4c80..a58f67973b4ed986065860263c7a42214640520d 100644 +--- a/src/main/java/net/minecraft/network/PacketEncoder.java ++++ b/src/main/java/net/minecraft/network/PacketEncoder.java +@@ -31,7 +31,7 @@ public class PacketEncoder<T extends PacketListener> extends MessageToByteEncode + + JvmProfiler.INSTANCE.onPacketSent(this.protocolInfo.id(), packetType, channelHandlerContext.channel().remoteAddress(), i); + } catch (Throwable var9) { +- LOGGER.error("Error sending packet {}", packetType, var9); ++ LOGGER.error("Error sending packet {} (skippable? {})", packetType, packet.isSkippable(), var9); + if (packet.isSkippable()) { + throw new SkipPacketException(var9); + } +diff --git a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java +index e113cd9d93750cf59712b06db62591876b4efbac..1789c0c71d968b386060bd6dc2630e8a078c32e2 100644 +--- a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java +@@ -47,4 +47,14 @@ public record ClientboundCustomQueryPacket(int transactionId, CustomQueryPayload + public void handle(ClientLoginPacketListener listener) { + listener.handleCustomQuery(this); + } ++ ++ // Paper start - MC Utils - default query payloads ++ public static record PlayerInfoChannelPayload(ResourceLocation id, FriendlyByteBuf buffer) implements CustomQueryPayload { ++ ++ @Override ++ public void write(final FriendlyByteBuf buf) { ++ buf.writeBytes(this.buffer.copy()); ++ } ++ } ++ // Paper end - MC Utils - default query payloads + } +diff --git a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java +index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b22404a031 100644 +--- a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java +@@ -20,7 +20,17 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu + } + + private static CustomQueryAnswerPayload readPayload(int queryId, FriendlyByteBuf buf) { +- return readUnknownPayload(buf); ++ // Paper start - MC Utils - default query payloads ++ FriendlyByteBuf buffer = buf.readNullable((buf2) -> { ++ int i = buf2.readableBytes(); ++ if (i >= 0 && i <= MAX_PAYLOAD_SIZE) { ++ return new FriendlyByteBuf(buf2.readBytes(i)); ++ } else { ++ throw new IllegalArgumentException("Payload may not be larger than " + MAX_PAYLOAD_SIZE + " bytes"); ++ } ++ }); ++ return buffer == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buffer); ++ // Paper end - MC Utils - default query payloads + } + + private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buf) { +@@ -47,4 +57,21 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu + public void handle(ServerLoginPacketListener listener) { + listener.handleCustomQueryPacket(this); + } ++ ++ // Paper start - MC Utils - default query payloads ++ public static final class QueryAnswerPayload implements CustomQueryAnswerPayload { ++ ++ public final FriendlyByteBuf buffer; ++ ++ public QueryAnswerPayload(final net.minecraft.network.FriendlyByteBuf buffer) { ++ this.buffer = buffer; ++ } ++ ++ @Override ++ public void write(final net.minecraft.network.FriendlyByteBuf buf) { ++ buf.writeBytes(this.buffer.copy()); ++ } ++ } ++ // Paper end - MC Utils - default query payloads ++ + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 40adb6117b9e0d5f70103113202a07715e403e2a..b1325e090f2c7aff31d27fc38ca7173efe31ed7c 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -310,6 +310,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + public final double[] recentTps = new double[ 3 ]; + // Spigot end + public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files ++ public static long currentTickLong = 0L; // Paper - track current tick as a long + + public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) { + AtomicReference<S> atomicreference = new AtomicReference(); +@@ -973,6 +974,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1); + } + // Spigot start ++ io.papermc.paper.util.MCUtil.asyncExecutor.shutdown(); // Paper ++ try { io.papermc.paper.util.MCUtil.asyncExecutor.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper ++ } catch (java.lang.InterruptedException ignored) {} // Paper + if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { + MinecraftServer.LOGGER.info("Saving usercache.json"); + this.getProfileCache().save(); +@@ -1046,6 +1050,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + } + // Spigot start ++ ++MinecraftServer.currentTickLong; // Paper - track current tick as a long + if ( tickCount++ % MinecraftServer.SAMPLE_INTERVAL == 0 ) + { + long curTime = Util.getMillis(); +@@ -1337,7 +1342,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + MinecraftServer.LOGGER.debug("Autosave finished"); + SpigotTimings.worldSaveTimer.stopTiming(); // Spigot + } +- ++ io.papermc.paper.util.CachedLists.reset(); // Paper + this.profiler.push("tallying"); + long j = Util.getNanos() - i; + int k = this.tickCount % 100; +@@ -1470,6 +1475,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + try { + worldserver.timings.doTick.startTiming(); // Spigot + 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 + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index 5d4336210e11ee39521b4096a5f0874329053cdc..526d5b9bd6ce8eade59d3d3cf8bd7ad700483eff 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -37,9 +37,9 @@ public class ChunkHolder extends GenerationChunkHolder { + public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); + private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); + private final LevelHeightAccessor levelHeightAccessor; +- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; +- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; +- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; ++ private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage ++ private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage ++ private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage + public int oldTicketLevel; + private int ticketLevel; + private int queueLevel; +@@ -55,6 +55,18 @@ public class ChunkHolder extends GenerationChunkHolder { + private CompletableFuture<?> sendSync; + private CompletableFuture<?> saveSync; + ++ private final ChunkMap chunkMap; // Paper ++ ++ // Paper start ++ public void onChunkAdd() { ++ ++ } ++ ++ public void onChunkRemove() { ++ ++ } ++ // Paper end ++ + public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { + super(pos); + this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +@@ -74,8 +86,23 @@ public class ChunkHolder extends GenerationChunkHolder { + this.queueLevel = this.oldTicketLevel; + this.setTicketLevel(level); + this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; ++ this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper + } + ++ // Paper start ++ public @Nullable ChunkAccess getAvailableChunkNow() { ++ // TODO can we just getStatusFuture(EMPTY)? ++ for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { ++ CompletableFuture<ChunkResult<ChunkAccess>> future = this.getFutureIfPresentUnchecked(curr); ++ ChunkResult<ChunkAccess> either = future.getNow(null); ++ if (either == null || either.isSuccess()) { ++ continue; ++ } ++ return either.orElseThrow(IllegalStateException::new); ++ } ++ return null; ++ } ++ // Paper end + // CraftBukkit start + public LevelChunk getFullChunkNow() { + return (LevelChunk) this.getChunkIfPresent(ChunkStatus.FULL); +@@ -86,20 +113,20 @@ public class ChunkHolder extends GenerationChunkHolder { + } + // CraftBukkit end + +- public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() { ++ public final CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() { // Paper - final for inline + return this.tickingChunkFuture; + } + +- public CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() { ++ public final CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() { // Paper - final for inline + return this.entityTickingChunkFuture; + } + +- public CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() { ++ public final CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() { // Paper - final for inline + return this.fullChunkFuture; + } + + @Nullable +- public LevelChunk getTickingChunk() { ++ public final LevelChunk getTickingChunk() { // Paper - final for inline + return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error + } + +@@ -123,6 +150,21 @@ public class ChunkHolder extends GenerationChunkHolder { + + } + ++ // Paper start ++ public @Nullable ChunkStatus getChunkHolderStatus() { ++ for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { ++ CompletableFuture<ChunkResult<ChunkAccess>> future = this.getFutureIfPresentUnchecked(curr); ++ ChunkResult<ChunkAccess> either = future.getNow(null); ++ if (either == null || !either.isSuccess()) { ++ continue; ++ } ++ return curr; ++ } ++ ++ return null; ++ } ++ // Paper end ++ + public CompletableFuture<?> getSaveSyncFuture() { + return this.saveSync; + } +@@ -266,7 +308,7 @@ public class ChunkHolder extends GenerationChunkHolder { + } + + @Override +- public int getTicketLevel() { ++ public final int getTicketLevel() { // Paper - final for inline + return this.ticketLevel; + } + +@@ -335,12 +377,28 @@ public class ChunkHolder extends GenerationChunkHolder { + + this.wasAccessibleSinceLastSave |= flag1; + if (!flag && flag1) { ++ int expectCreateCount = ++this.fullChunkCreateCount; // Paper + this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this); + this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL); ++ // Paper start - cache ticking ready status ++ this.fullChunkFuture.thenAccept(chunkResult -> { ++ chunkResult.ifSuccess(chunk -> { ++ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { ++ ChunkHolder.this.isFullChunkReady = true; ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this); ++ } ++ }); ++ }); ++ // Paper end - cache ticking ready status + this.addSaveDependency(this.fullChunkFuture); + } + + if (flag && !flag1) { ++ // Paper start ++ if (this.isFullChunkReady) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper ++ } ++ // Paper end + this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); + this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + } +@@ -351,11 +409,25 @@ public class ChunkHolder extends GenerationChunkHolder { + if (!flag2 && flag3) { + this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this); + this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING); ++ // Paper start - cache ticking ready status ++ this.tickingChunkFuture.thenAccept(chunkResult -> { ++ chunkResult.ifSuccess(chunk -> { ++ // note: Here is a very good place to add callbacks to logic waiting on this. ++ ChunkHolder.this.isTickingReady = true; ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this); ++ }); ++ }); ++ // Paper end + this.addSaveDependency(this.tickingChunkFuture); + } + + if (flag2 && !flag3) { +- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); ++ // Paper start ++ if (this.isTickingReady) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper ++ } ++ // Paper end ++ this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage + this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + } + +@@ -369,11 +441,24 @@ public class ChunkHolder extends GenerationChunkHolder { + + this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this); + this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING); ++ // Paper start - cache ticking ready status ++ this.entityTickingChunkFuture.thenAccept(chunkResult -> { ++ chunkResult.ifSuccess(chunk -> { ++ ChunkHolder.this.isEntityTickingReady = true; ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this); ++ }); ++ }); ++ // Paper end + this.addSaveDependency(this.entityTickingChunkFuture); + } + + if (flag4 && !flag5) { +- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); ++ // Paper start ++ if (this.isEntityTickingReady) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); ++ } ++ // Paper end ++ this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage + this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + } + +@@ -423,4 +508,18 @@ public class ChunkHolder extends GenerationChunkHolder { + + List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge); + } ++ ++ // Paper start ++ public final boolean isEntityTickingReady() { ++ return this.isEntityTickingReady; ++ } ++ ++ public final boolean isTickingReady() { ++ return this.isTickingReady; ++ } ++ ++ public final boolean isFullChunkReady() { ++ return this.isFullChunkReady; ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 419a27a8bdc8adfeb6ea89e3bfe1838a80d75a33..ce0d22452171857e3cf070bf01450a7653ec7142 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -170,6 +170,62 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + }; + // CraftBukkit end + ++ // Paper start - distance maps ++ private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); ++ ++ void addPlayerToDistanceMaps(ServerPlayer player) { ++ int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); ++ int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); ++ // Note: players need to be explicitly added to distance maps before they can be updated ++ this.nearbyPlayers.addPlayer(player); ++ } ++ ++ void removePlayerFromDistanceMaps(ServerPlayer player) { ++ int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); ++ int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); ++ // Note: players need to be explicitly added to distance maps before they can be updated ++ this.nearbyPlayers.removePlayer(player); ++ } ++ ++ void updateMaps(ServerPlayer player) { ++ int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); ++ int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); ++ // Note: players need to be explicitly added to distance maps before they can be updated ++ this.nearbyPlayers.tickPlayer(player); ++ } ++ // Paper end ++ // Paper start ++ public final List<io.papermc.paper.chunk.SingleThreadChunkRegionManager> regionManagers = new java.util.ArrayList<>(); ++ public final io.papermc.paper.chunk.SingleThreadChunkRegionManager dataRegionManager; ++ ++ public static final class DataRegionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionData { ++ } ++ ++ public static final class DataRegionSectionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSectionData { ++ ++ @Override ++ public void removeFromRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, ++ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region from) { ++ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; ++ final DataRegionData fromData = (DataRegionData)from.regionData; ++ } ++ ++ @Override ++ public void addToRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, ++ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region oldRegion, ++ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region newRegion) { ++ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; ++ final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData; ++ final DataRegionData newRegionData = (DataRegionData)newRegion.regionData; ++ } ++ } ++ ++ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { ++ return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; ++ // Paper end ++ + public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) { + super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); + this.visibleChunkMap = this.updatingChunkMap.clone(); +@@ -221,8 +277,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world); + this.setServerViewDistance(viewDistance); + this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox); ++ // Paper start ++ this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new); ++ this.regionManagers.add(this.dataRegionManager); ++ this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level); ++ // Paper end ++ } ++ ++ // Paper start ++ // always use accessor, so folia can override ++ public final io.papermc.paper.util.player.NearbyPlayers getNearbyPlayers() { ++ return this.nearbyPlayers; + } + ++ public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { ++ return -1; ++ } ++ // Paper end ++ + protected ChunkGenerator generator() { + return this.worldGenContext.generator(); + } +@@ -378,9 +450,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + }; + + stringbuilder.append("Updating:").append(System.lineSeparator()); +- this.updatingChunkMap.values().forEach(consumer); ++ io.papermc.paper.chunk.system.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper + stringbuilder.append("Visible:").append(System.lineSeparator()); +- this.visibleChunkMap.values().forEach(consumer); ++ io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper + CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading"); + CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading"); + +@@ -422,8 +494,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + holder.setTicketLevel(level); + } else { + holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); ++ // Paper start ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder); ++ // Paper end + } + ++ // Paper start ++ holder.onChunkAdd(); ++ // Paper end + this.updatingChunkMap.put(pos, holder); + this.modified = true; + } +@@ -445,7 +523,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + protected void saveAllChunks(boolean flush) { + if (flush) { +- List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); ++ List<ChunkHolder> list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper + MutableBoolean mutableboolean = new MutableBoolean(); + + do { +@@ -468,7 +546,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + }); + this.flushWorker(); + } else { +- this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded); ++ io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded); + } + + } +@@ -487,7 +565,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public boolean hasWork() { +- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); ++ return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper + } + + private void processUnloads(BooleanSupplier shouldKeepTicking) { +@@ -504,6 +582,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + this.updatingChunkMap.remove(j); ++ playerchunk.onChunkRemove(); // Paper + this.pendingUnloads.put(j, playerchunk); + this.modified = true; + ++i; +@@ -523,7 +602,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + int l = 0; +- ObjectIterator<ChunkHolder> objectiterator = this.visibleChunkMap.values().iterator(); ++ Iterator<ChunkHolder> objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper + + while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { + if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { +@@ -541,7 +620,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } else { + ChunkAccess ichunkaccess = holder.getLatestChunk(); + +- if (this.pendingUnloads.remove(pos, holder) && ichunkaccess != null) { ++ // Paper start ++ boolean removed; ++ if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); ++ // Paper end + LevelChunk chunk; + + if (ichunkaccess instanceof LevelChunk) { +@@ -559,7 +642,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.lightEngine.tryScheduleUpdate(); + this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); + this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong()); +- } ++ } else if (removed) { // Paper start ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); ++ } // Paper end + + } + }; +@@ -896,7 +981,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + } + +- protected void setServerViewDistance(int watchDistance) { ++ public void setServerViewDistance(int watchDistance) { // Paper - public + int j = Mth.clamp(watchDistance, 2, 32); + + if (j != this.serverViewDistance) { +@@ -913,7 +998,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + } + +- int getPlayerViewDistance(ServerPlayer player) { ++ public int getPlayerViewDistance(ServerPlayer player) { // Paper - public + return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); + } + +@@ -942,7 +1027,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public int size() { +- return this.visibleChunkMap.size(); ++ return io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper + } + + public DistanceManager getDistanceManager() { +@@ -950,19 +1035,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + protected Iterable<ChunkHolder> getChunks() { +- return Iterables.unmodifiableIterable(this.visibleChunkMap.values()); ++ return Iterables.unmodifiableIterable(io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper + } + + void dumpChunks(Writer writer) throws IOException { + CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer); + TickingTracker tickingtracker = this.distanceManager.tickingTracker(); +- ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator(); ++ Iterator<ChunkHolder> objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper + + while (objectbidirectionaliterator.hasNext()) { +- Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next(); +- long i = entry.getLongKey(); ++ ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper ++ long i = playerchunk.pos.toLong(); // Paper + ChunkPos chunkcoordintpair = new ChunkPos(i); +- ChunkHolder playerchunk = (ChunkHolder) entry.getValue(); ++ // Paper - move up + Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk()); + Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> { + return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty(); +@@ -1083,6 +1168,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + player.setChunkTrackingView(ChunkTrackingView.EMPTY); + this.updateChunkTracking(player); ++ this.addPlayerToDistanceMaps(player); // Paper - distance maps + } else { + SectionPos sectionposition = player.getLastSectionPos(); + +@@ -1091,6 +1177,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.distanceManager.removePlayer(sectionposition, player); + } + ++ this.removePlayerFromDistanceMaps(player); // Paper - distance maps + this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); + } + +@@ -1142,6 +1229,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.updateChunkTracking(player); + } + ++ this.updateMaps(player); // Paper - distance maps + } + + private void updateChunkTracking(ServerPlayer player) { +@@ -1385,10 +1473,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + }); + } + +- private class ChunkDistanceManager extends DistanceManager { ++ public class ChunkDistanceManager extends DistanceManager { // Paper - public + + protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) { +- super(workerExecutor, mainThreadExecutor); ++ super(workerExecutor, mainThreadExecutor, ChunkMap.this); // Paper + } + + @Override +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index b6cc33943fe7e4667944f3e6f868b3033ea9ca18..27065ffc5473c518acee3a3096b83fac61eb7860 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -55,8 +55,9 @@ public abstract class DistanceManager { + final Executor mainThreadExecutor; + private long ticketTickCounter; + public int simulationDistance = 10; ++ private final ChunkMap chunkMap; // Paper + +- protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) { ++ protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) { + Objects.requireNonNull(mainThreadExecutor); + ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute); + ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4); +@@ -65,6 +66,7 @@ public abstract class DistanceManager { + this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true); + this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox); + this.mainThreadExecutor = mainThreadExecutor; ++ this.chunkMap = chunkMap; // Paper + } + + protected void purgeStaleTickets() { +@@ -365,7 +367,7 @@ public abstract class DistanceManager { + } + + public void removeTicketsOnClosing() { +- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT); ++ ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve + ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); + + while (objectiterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 47ae79cb2a03b43cbb881bcdea7ca231082b6614..ff68eff051c5f50b20b26d33001e12741dd3000e 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; + + public class ServerChunkCache extends ChunkSource { + ++ public static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper + private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList(); + private final DistanceManager distanceManager; + final ServerLevel level; +@@ -66,6 +67,14 @@ public class ServerChunkCache extends ChunkSource { + @Nullable + @VisibleForDebug + private NaturalSpawner.SpawnState lastSpawnState; ++ // Paper start ++ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); ++ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); ++ final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); ++ final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<LevelChunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); ++ long chunkFutureAwaitCounter; ++ private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; ++ // Paper end + + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) { + this.level = world; +@@ -91,6 +100,124 @@ public class ServerChunkCache extends ChunkSource { + return chunk.getFullChunkNow() != null; + } + // CraftBukkit end ++ // Paper start ++ private static int getChunkCacheKey(int x, int z) { ++ return x & 3 | ((z & 3) << 2); ++ } ++ ++ public void addLoadedChunk(LevelChunk chunk) { ++ this.loadedChunkMapSeqLock.acquireWrite(); ++ try { ++ this.loadedChunkMap.put(chunk.coordinateKey, chunk); ++ } finally { ++ this.loadedChunkMapSeqLock.releaseWrite(); ++ } ++ ++ // rewrite cache if we have to ++ // we do this since we also cache null chunks ++ int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ); ++ ++ this.lastLoadedChunks[cacheKey] = chunk; ++ } ++ ++ public void removeLoadedChunk(LevelChunk chunk) { ++ this.loadedChunkMapSeqLock.acquireWrite(); ++ try { ++ this.loadedChunkMap.remove(chunk.coordinateKey); ++ } finally { ++ this.loadedChunkMapSeqLock.releaseWrite(); ++ } ++ ++ // rewrite cache if we have to ++ // we do this since we also cache null chunks ++ int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ); ++ ++ LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; ++ if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) { ++ this.lastLoadedChunks[cacheKey] = null; ++ } ++ } ++ ++ public final LevelChunk getChunkAtIfLoadedMainThread(int x, int z) { ++ int cacheKey = getChunkCacheKey(x, z); ++ ++ LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; ++ if (cachedChunk != null && cachedChunk.locX == x & cachedChunk.locZ == z) { ++ return cachedChunk; ++ } ++ ++ long chunkKey = ChunkPos.asLong(x, z); ++ ++ cachedChunk = this.loadedChunkMap.get(chunkKey); ++ // Skipping a null check to avoid extra instructions to improve inline capability ++ this.lastLoadedChunks[cacheKey] = cachedChunk; ++ return cachedChunk; ++ } ++ ++ public final LevelChunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) { ++ return this.loadedChunkMap.get(ChunkPos.asLong(x, z)); ++ } ++ ++ @Nullable ++ public ChunkAccess getChunkAtImmediately(int x, int z) { ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); ++ if (holder == null) { ++ return null; ++ } ++ ++ return holder.getLastAvailable(); ++ } ++ ++ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { ++ this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier); ++ } ++ ++ public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { ++ this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier); ++ } ++ ++ // "real" get chunk if loaded ++ // Note: Partially copied from the getChunkAt method below ++ @Nullable ++ public LevelChunk getChunkAtIfCachedImmediately(int x, int z) { ++ long k = ChunkPos.asLong(x, z); ++ ++ // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe ++ ++ ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k); ++ if (playerChunk == null) { ++ return null; ++ } ++ ++ return playerChunk.getFullChunkNowUnchecked(); ++ } ++ ++ @Nullable ++ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { ++ long k = ChunkPos.asLong(x, z); ++ ++ if (Thread.currentThread() == this.mainThread) { ++ return this.getChunkAtIfLoadedMainThread(x, z); ++ } ++ ++ LevelChunk ret = null; ++ long readlock; ++ do { ++ readlock = this.loadedChunkMapSeqLock.acquireRead(); ++ try { ++ ret = this.loadedChunkMap.get(k); ++ } catch (Throwable thr) { ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ // re-try, this means a CME occurred... ++ continue; ++ } ++ } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock)); ++ ++ return ret; ++ } ++ // Paper end + + @Override + public ThreadedLevelLightEngine getLightEngine() { +@@ -286,7 +413,7 @@ public class ServerChunkCache extends ChunkSource { + return this.mainThreadProcessor.pollTask(); + } + +- boolean runDistanceManagerUpdates() { ++ public boolean runDistanceManagerUpdates() { // Paper - public + boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); + boolean flag1 = this.chunkMap.promoteChunkMap(); + +@@ -299,6 +426,12 @@ public class ServerChunkCache extends ChunkSource { + } + } + ++ // Paper start ++ public boolean isPositionTicking(Entity entity) { ++ return this.isPositionTicking(ChunkPos.asLong(net.minecraft.util.Mth.floor(entity.getX()) >> 4, net.minecraft.util.Mth.floor(entity.getZ()) >> 4)); ++ } ++ // Paper end ++ + public boolean isPositionTicking(long pos) { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 9d11fcb3df12182ae00ce73f7e30091fd199a341..eea8bafd98e3a8d82b3216488537ab898cc4ae7a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -236,6 +236,98 @@ public class ServerLevel extends Level implements WorldGenLevel { + return this.convertable.dimensionType; + } + ++ // Paper start ++ public final boolean areChunksLoadedForMove(AABB axisalignedbb) { ++ // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override ++ // ICollisionAccess methods for VoxelShapes) ++ // be more strict too, add a block (dumb plugins in move events?) ++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; ++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; ++ ++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; ++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ ServerChunkCache chunkProvider = this.getChunkSource(); ++ ++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) { ++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { ++ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { ++ if (Thread.currentThread() != this.thread) { ++ this.getChunkSource().mainThreadProcessor.execute(() -> { ++ this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); ++ }); ++ return; ++ } ++ List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>(); ++ it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList(); ++ ++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; ++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; ++ ++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; ++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ ServerChunkCache chunkProvider = this.getChunkSource(); ++ ++ int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); ++ int[] loadedChunks = new int[1]; ++ ++ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); ++ ++ java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { ++ if (chunk != null) { ++ int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); ++ ret.add(chunk); ++ ticketLevels.add(ticketLevel); ++ chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); ++ } ++ if (++loadedChunks[0] == requiredChunks) { ++ try { ++ onLoad.accept(java.util.Collections.unmodifiableList(ret)); ++ } finally { ++ for (int i = 0, len = ret.size(); i < len; ++i) { ++ ChunkPos chunkPos = ret.get(i).getPos(); ++ int ticketLevel = ticketLevels.getInt(i); ++ ++ chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); ++ chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); ++ } ++ } ++ } ++ }; ++ ++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) { ++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { ++ io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad( ++ this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer ++ ); ++ } ++ } ++ } ++ // Paper end ++ + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { + // IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index e2eb76e8993dd556606b5bb38814fe848fbf01a6..5f8adf662e5ab9114e2891cfacb8db5d4d40a297 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -280,6 +280,8 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + public boolean sentListPacket = false; + public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent + // CraftBukkit end ++ public boolean isRealPlayer; // Paper ++ public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper + + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); +@@ -349,6 +351,8 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + this.updateOptions(clientOptions); + this.object = null; + ++ this.cachedSingleHashSet = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper ++ + // CraftBukkit start + this.displayName = this.getScoreboardName(); + this.bukkitPickUpLoot = true; +diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java +index 045b754b5b70bbd1e7732ad2142dfadd6cc2305c..f56e5c0f53f9b52a9247b9be9265b949494fc924 100644 +--- a/src/main/java/net/minecraft/server/level/TicketType.java ++++ b/src/main/java/net/minecraft/server/level/TicketType.java +@@ -7,6 +7,7 @@ import net.minecraft.util.Unit; + import net.minecraft.world.level.ChunkPos; + + public class TicketType<T> { ++ public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper + + private final String name; + private final Comparator<T> comparator; +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 7d69da7e761ccfe736656e8c89dd1ae08956695f..421f146ea9c35b852251c0ddb29856c13e11aef3 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -169,6 +169,26 @@ public class WorldGenRegion implements WorldGenLevel { + return k < this.generatingStep.directDependencies().size(); + } + ++ // Paper start - if loaded util ++ @Nullable ++ @Override ++ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) { ++ return this.getChunk(x, z, ChunkStatus.FULL, false); ++ } ++ ++ @Override ++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { ++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4); ++ return chunk == null ? null : chunk.getBlockState(blockposition); ++ } ++ ++ @Override ++ public final FluidState getFluidIfLoaded(BlockPos blockposition) { ++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4); ++ return chunk == null ? null : chunk.getFluidState(blockposition); ++ } ++ // Paper end ++ + @Override + public BlockState getBlockState(BlockPos pos) { + return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos); +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 0914b2f9fef1f49df9f0ce7c85cdde94c2c39b6c..6abe921099ff00ecfaf0f423ef27d708420f6f48 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -180,6 +180,7 @@ public abstract class PlayerList { + } + + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) { ++ player.isRealPlayer = true; // Paper + GameProfile gameprofile = player.getGameProfile(); + GameProfileCache usercache = this.server.getProfileCache(); + // Optional optional; // CraftBukkit - decompile error +diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java +index aede9b65e799a1f123f71f9390fb05acddda676b..2510589400b3012b827efcab477c6483d9d55901 100644 +--- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java ++++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java +@@ -79,6 +79,13 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler + runnable.run(); + } + } ++ // Paper start ++ public void scheduleOnMain(Runnable r0) { ++ // postToMainThread does not work the same as older versions of mc ++ // This method is actually used to create a TickTask, which can then be posted onto main ++ this.tell(this.wrapRunnable(r0)); ++ } ++ // Paper end + + @Override + public void tell(R runnable) { +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 687c0683ee5b3366d936a178fb4bf9faffc2a556..6041033b3ea201bde1a73ce4e429e8b80e05e2eb 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -339,6 +339,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4); + } + // CraftBukkit end ++ // Paper start ++ public final AABB getBoundingBoxAt(double x, double y, double z) { ++ return this.dimensions.makeBoundingBox(x, y, z); ++ } ++ // Paper end + + public Entity(EntityType<?> type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 791c647de951f996f25bcc4418a6e820ab0512c9..9deb6b90f3c4281280deb2f609a55923147a675a 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -282,6 +282,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + public boolean collides = true; + public Set<UUID> collidableExemptions = new HashSet<>(); + public boolean bukkitPickUpLoot; ++ public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper + + @Override + public float getBukkitYaw() { +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 930b5002aa6eaa1137314f7b38fad99778b6edaa..fcc8b66702f761c443fb647a8ab6e1ab49e5acfe 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -282,6 +282,8 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + return this.target; + } + ++ public org.bukkit.craftbukkit.entity.CraftMob getBukkitMob() { return (org.bukkit.craftbukkit.entity.CraftMob) super.getBukkitEntity(); } // Paper ++ + @Nullable + protected final LivingEntity getTargetFromBrain() { + return (LivingEntity) this.getBrain().getMemory(MemoryModuleType.ATTACK_TARGET).orElse(null); // CraftBukkit - decompile error +diff --git a/src/main/java/net/minecraft/world/entity/PathfinderMob.java b/src/main/java/net/minecraft/world/entity/PathfinderMob.java +index 812aecb88641c09fb5030d145620b95aff19c9cb..bd8d56aa5a3557e7a2aa1dd066b27c2054d1eef8 100644 +--- a/src/main/java/net/minecraft/world/entity/PathfinderMob.java ++++ b/src/main/java/net/minecraft/world/entity/PathfinderMob.java +@@ -22,6 +22,8 @@ public abstract class PathfinderMob extends Mob { + super(type, world); + } + ++ public org.bukkit.craftbukkit.entity.CraftCreature getBukkitCreature() { return (org.bukkit.craftbukkit.entity.CraftCreature) super.getBukkitEntity(); } // Paper ++ + public float getWalkTargetValue(BlockPos pos) { + return this.getWalkTargetValue(pos, this.level()); + } +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 f73604d762efbac400d40f536ec1782fca584efa..4701bf9ee203f2f15b0b68e84bbfa2c489b66631 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Monster.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Monster.java +@@ -27,6 +27,7 @@ import net.minecraft.world.level.ServerLevelAccessor; + import net.minecraft.world.level.dimension.DimensionType; + + public abstract class Monster extends PathfinderMob implements Enemy { ++ public org.bukkit.craftbukkit.entity.CraftMonster getBukkitMonster() { return (org.bukkit.craftbukkit.entity.CraftMonster) super.getBukkitEntity(); } // Paper + protected Monster(EntityType<? extends Monster> type, Level world) { + super(type, world); + this.xpReward = 5; +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index 15f0b0977fe7b41e29346df9d49120f14f02ea43..b6cb1de08e955dfc99a0cb736b2ed456c99fde5c 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -953,6 +953,25 @@ public final class ItemStack implements DataComponentHolder { + } + } + ++ // Paper start - (this is just a good no conflict location) ++ public org.bukkit.inventory.ItemStack asBukkitMirror() { ++ return CraftItemStack.asCraftMirror(this); ++ } ++ public org.bukkit.inventory.ItemStack asBukkitCopy() { ++ return CraftItemStack.asCraftMirror(this.copy()); ++ } ++ public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) { ++ return CraftItemStack.asNMSCopy(itemstack); ++ } ++ private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack; ++ public org.bukkit.inventory.ItemStack getBukkitStack() { ++ if (bukkitStack == null || bukkitStack.handle != this) { ++ bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this); ++ } ++ return bukkitStack; ++ } ++ // Paper end ++ + public void applyComponents(DataComponentPatch changes) { + this.components.applyPatch(changes); + this.getItem().verifyComponentsAfterLoad(this); +@@ -1219,6 +1238,7 @@ public final class ItemStack implements DataComponentHolder { + // CraftBukkit start + @Deprecated + public void setItem(Item item) { ++ this.bukkitStack = null; // Paper + this.item = item; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java +index 1c71d2c1b16bdba1e14a8230787e4cb4ad530163..d6d8bbc98fc71997cb52521d59ebb59d727d3c22 100644 +--- a/src/main/java/net/minecraft/world/level/BlockGetter.java ++++ b/src/main/java/net/minecraft/world/level/BlockGetter.java +@@ -9,6 +9,7 @@ import javax.annotation.Nullable; + import net.minecraft.core.BlockPos; + import net.minecraft.core.Direction; + import net.minecraft.util.Mth; ++import net.minecraft.world.level.block.Block; + import net.minecraft.world.level.block.entity.BlockEntity; + import net.minecraft.world.level.block.entity.BlockEntityType; + import net.minecraft.world.level.block.state.BlockState; +@@ -30,6 +31,15 @@ public interface BlockGetter extends LevelHeightAccessor { + } + + BlockState getBlockState(BlockPos pos); ++ // Paper start - if loaded util ++ @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition); ++ ++ default @Nullable Block getBlockIfLoaded(BlockPos blockposition) { ++ BlockState type = this.getBlockStateIfLoaded(blockposition); ++ return type == null ? null : type.getBlock(); ++ } ++ @Nullable FluidState getFluidIfLoaded(BlockPos blockposition); ++ // Paper end + + FluidState getFluidState(BlockPos pos); + +diff --git a/src/main/java/net/minecraft/world/level/ChunkPos.java b/src/main/java/net/minecraft/world/level/ChunkPos.java +index 171c9c4ab2d1a7988935e09b49286f30e36741e2..fa58eeec2b652f0fa251eedf11cfabde5fd3198b 100644 +--- a/src/main/java/net/minecraft/world/level/ChunkPos.java ++++ b/src/main/java/net/minecraft/world/level/ChunkPos.java +@@ -20,6 +20,7 @@ public class ChunkPos { + public static final int REGION_MAX_INDEX = 31; + public final int x; + public final int z; ++ public final long longKey; // Paper + private static final int HASH_A = 1664525; + private static final int HASH_C = 1013904223; + private static final int HASH_Z_XOR = -559038737; +@@ -27,16 +28,19 @@ public class ChunkPos { + public ChunkPos(int x, int z) { + this.x = x; + this.z = z; ++ this.longKey = asLong(this.x, this.z); // Paper + } + + public ChunkPos(BlockPos pos) { + this.x = SectionPos.blockToSectionCoord(pos.getX()); + this.z = SectionPos.blockToSectionCoord(pos.getZ()); ++ this.longKey = asLong(this.x, this.z); // Paper + } + + public ChunkPos(long pos) { + this.x = (int)pos; + this.z = (int)(pos >> 32); ++ this.longKey = asLong(this.x, this.z); // Paper + } + + public static ChunkPos minFromRegion(int x, int z) { +@@ -48,7 +52,7 @@ public class ChunkPos { + } + + public long toLong() { +- return asLong(this.x, this.z); ++ return longKey; // Paper + } + + public static long asLong(int chunkX, int chunkZ) { +diff --git a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java +index 3c707d6674b2594b09503b959a31c1f4ad3981e6..db61b6b0158a9bcc0e1d735e34fe3671f8c89e21 100644 +--- a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java ++++ b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java +@@ -17,6 +17,18 @@ public enum EmptyBlockGetter implements BlockGetter { + return null; + } + ++ // Paper start - If loaded util ++ @Override ++ public final FluidState getFluidIfLoaded(BlockPos blockposition) { ++ return Fluids.EMPTY.defaultFluidState(); ++ } ++ ++ @Override ++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { ++ return Blocks.AIR.defaultBlockState(); ++ } ++ // Paper end ++ + @Override + public BlockState getBlockState(BlockPos pos) { + return Blocks.AIR.defaultBlockState(); +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index c061813d275fbc48d7629cc59d90dbb4c347516c..55b30e1df4a05802977b0c3f3b518ef0676eae2d 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -95,6 +95,7 @@ import org.bukkit.craftbukkit.CraftServer; + import org.bukkit.craftbukkit.CraftWorld; + import org.bukkit.craftbukkit.SpigotTimings; // Spigot + import org.bukkit.craftbukkit.block.CapturedBlockState; ++import org.bukkit.craftbukkit.block.CraftBlockState; + import org.bukkit.craftbukkit.block.data.CraftBlockData; + import org.bukkit.craftbukkit.util.CraftSpawnCategory; + import org.bukkit.entity.SpawnCategory; +@@ -274,6 +275,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return null; + } + ++ // Paper start ++ public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) { ++ // To be patched over ++ return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType(); ++ } ++ // Paper end ++ + public boolean isInWorldBounds(BlockPos pos) { + return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos); + } +@@ -290,18 +298,52 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return y < -20000000 || y >= 20000000; + } + +- public LevelChunk getChunkAt(BlockPos pos) { ++ public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline + return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); + } + + @Override +- public LevelChunk getChunk(int chunkX, int chunkZ) { +- return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL); ++ public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline ++ return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump + } + ++ // Paper start - if loaded ++ @Nullable ++ @Override ++ public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) { ++ return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z); ++ } ++ ++ @Override + @Nullable ++ public final BlockState getBlockStateIfLoaded(BlockPos pos) { ++ // CraftBukkit start - tree generation ++ if (this.captureTreeGeneration) { ++ CraftBlockState previous = this.capturedBlockStates.get(pos); ++ if (previous != null) { ++ return previous.getHandle(); ++ } ++ } ++ // CraftBukkit end ++ if (this.isOutsideBuildHeight(pos)) { ++ return Blocks.VOID_AIR.defaultBlockState(); ++ } else { ++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4); ++ ++ return chunk == null ? null : chunk.getBlockState(pos); ++ } ++ } ++ ++ @Override ++ public final FluidState getFluidIfLoaded(BlockPos blockposition) { ++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4); ++ ++ return chunk == null ? null : chunk.getFluidState(blockposition); ++ } ++ + @Override +- public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { ++ public final ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { // Paper - final for inline ++ // Paper end + ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create); + + if (ichunkaccess == null && create) { +@@ -312,7 +354,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + @Override +- public boolean setBlock(BlockPos pos, BlockState state, int flags) { ++ public final boolean setBlock(BlockPos pos, BlockState state, int flags) { // Paper - final for inline + return this.setBlock(pos, state, flags, 512); + } + +@@ -551,7 +593,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + if (this.isOutsideBuildHeight(pos)) { + return Blocks.VOID_AIR.defaultBlockState(); + } else { +- LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); ++ ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine + + return chunk.getBlockState(pos); + } +diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java +index 749e4ea1be56b393877b5fdd72dc3669dbf5a3dd..a0ae26d6197e1069ca09982b4f8b706c55ae8491 100644 +--- a/src/main/java/net/minecraft/world/level/LevelReader.java ++++ b/src/main/java/net/minecraft/world/level/LevelReader.java +@@ -26,6 +26,9 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal + @Nullable + ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create); + ++ @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading) ++ @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);} ++ + @Deprecated + boolean hasChunk(int chunkX, int chunkZ); + +diff --git a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java +index 497792978bdf0e6a53d772304770e8df3e7416ea..c5454b92ca2565461c799d7340160f9fb72c1b0f 100644 +--- a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java ++++ b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java +@@ -9,6 +9,7 @@ import net.minecraft.core.Holder; + import net.minecraft.core.SectionPos; + import net.minecraft.core.registries.Registries; + import net.minecraft.util.profiling.ProfilerFiller; ++import net.minecraft.server.level.ServerLevel; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.biome.Biome; + import net.minecraft.world.level.biome.Biomes; +@@ -67,7 +68,7 @@ public class PathNavigationRegion implements BlockGetter, CollisionGetter { + private ChunkAccess getChunk(int chunkX, int chunkZ) { + int i = chunkX - this.centerX; + int j = chunkZ - this.centerZ; +- if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { ++ if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below + ChunkAccess chunkAccess = this.chunks[i][j]; + return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get())); + } else { +@@ -75,6 +76,30 @@ public class PathNavigationRegion implements BlockGetter, CollisionGetter { + } + } + ++ // Paper start - if loaded util ++ private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) { ++ // Based on getChunk(int, int) ++ int xx = x - this.centerX; ++ int zz = z - this.centerZ; ++ ++ if (xx >= 0 && xx < this.chunks.length && zz >= 0 && zz < this.chunks[xx].length) { ++ return this.chunks[xx][zz]; ++ } ++ return null; ++ } ++ @Override ++ public final FluidState getFluidIfLoaded(BlockPos blockposition) { ++ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4); ++ return chunk == null ? null : chunk.getFluidState(blockposition); ++ } ++ ++ @Override ++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { ++ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4); ++ return chunk == null ? null : chunk.getBlockState(blockposition); ++ } ++ // Paper end ++ + @Override + public WorldBorder getWorldBorder() { + return this.level.getWorldBorder(); +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 9569d9d1c8f809420e08038e5bc38c2c7887ff90..4ac248f9f5f4f7aa95ddd6e3c3dab1ce94e73d66 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 +@@ -834,12 +834,20 @@ public abstract class BlockBehaviour implements FeatureElement { + } + } + ++ // Paper start ++ protected boolean shapeExceedsCube = true; ++ public final boolean shapeExceedsCube() { ++ return this.shapeExceedsCube; ++ } ++ // Paper end ++ + public void initCache() { + this.fluidState = ((Block) this.owner).getFluidState(this.asState()); + this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState()); + if (!this.getBlock().hasDynamicShape()) { + this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); + } ++ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here + + this.legacySolid = this.calculateSolid(); + } +@@ -886,8 +894,8 @@ public abstract class BlockBehaviour implements FeatureElement { + return this.getBlock().getOcclusionShape(this.asState(), world, pos); + } + +- public boolean hasLargeCollisionShape() { +- return this.cache == null || this.cache.largeCollisionShape; ++ public final boolean hasLargeCollisionShape() { // Paper ++ return this.shapeExceedsCube; // Paper - moved into shape cache init + } + + public boolean useShapeForLightOcclusion() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index 35c4bf87870c0dfa1f648547115238dacbb87426..db4d95ce98eb1490d5306d1f74b282d27264871a 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -65,7 +65,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + protected final ShortList[] postProcessing; + protected volatile boolean unsaved; + private volatile boolean isLightCorrect; +- protected final ChunkPos chunkPos; ++ protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key + private long inhabitedTime; + /** @deprecated */ + @Nullable +@@ -91,7 +91,8 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + // CraftBukkit end + + public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) { +- this.chunkPos = pos; ++ this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups ++ this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key + this.upgradeData = upgradeData; + this.levelHeightAccessor = heightLimitView; + this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()]; +diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +index a52077f0d93c94b0ea644bc14b9b28e84fd1b154..dcc0acd259920463a4464213b9a5e793603852f9 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +@@ -25,6 +25,12 @@ public class EmptyLevelChunk extends LevelChunk { + public BlockState getBlockState(BlockPos pos) { + return Blocks.VOID_AIR.defaultBlockState(); + } ++ // Paper start ++ @Override ++ public BlockState getBlockState(final int x, final int y, final int z) { ++ return Blocks.VOID_AIR.defaultBlockState(); ++ } ++ // Paper end + + @Nullable + @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 b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f4303ea677f 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -116,6 +116,109 @@ public class LevelChunk extends ChunkAccess { + public boolean needsDecoration; + // CraftBukkit end + ++ // Paper start ++ public @Nullable net.minecraft.server.level.ChunkHolder playerChunk; ++ ++ static final int NEIGHBOUR_CACHE_RADIUS = 3; ++ public static int getNeighbourCacheRadius() { ++ return NEIGHBOUR_CACHE_RADIUS; ++ } ++ ++ boolean loadedTicketLevel; ++ private long neighbourChunksLoadedBitset; ++ private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)]; ++ ++ private static int getNeighbourIndex(final int relativeX, final int relativeZ) { ++ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) ++ // optimised variant of the above by moving some of the ops to compile time ++ return relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))); ++ } ++ ++ public final LevelChunk getRelativeNeighbourIfLoaded(final int relativeX, final int relativeZ) { ++ return this.loadedNeighbourChunks[getNeighbourIndex(relativeX, relativeZ)]; ++ } ++ ++ public final boolean isNeighbourLoaded(final int relativeX, final int relativeZ) { ++ return (this.neighbourChunksLoadedBitset & (1L << getNeighbourIndex(relativeX, relativeZ))) != 0; ++ } ++ ++ public final void setNeighbourLoaded(final int relativeX, final int relativeZ, final LevelChunk chunk) { ++ if (chunk == null) { ++ throw new IllegalArgumentException("Chunk must be non-null, neighbour: (" + relativeX + "," + relativeZ + "), chunk: " + this.chunkPos); ++ } ++ final long before = this.neighbourChunksLoadedBitset; ++ final int index = getNeighbourIndex(relativeX, relativeZ); ++ this.loadedNeighbourChunks[index] = chunk; ++ this.neighbourChunksLoadedBitset |= (1L << index); ++ this.onNeighbourChange(before, this.neighbourChunksLoadedBitset); ++ } ++ ++ public final void setNeighbourUnloaded(final int relativeX, final int relativeZ) { ++ final long before = this.neighbourChunksLoadedBitset; ++ final int index = getNeighbourIndex(relativeX, relativeZ); ++ this.loadedNeighbourChunks[index] = null; ++ this.neighbourChunksLoadedBitset &= ~(1L << index); ++ this.onNeighbourChange(before, this.neighbourChunksLoadedBitset); ++ } ++ ++ public final void resetNeighbours() { ++ final long before = this.neighbourChunksLoadedBitset; ++ this.neighbourChunksLoadedBitset = 0L; ++ java.util.Arrays.fill(this.loadedNeighbourChunks, null); ++ this.onNeighbourChange(before, 0L); ++ } ++ ++ protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { ++ ++ } ++ ++ public final boolean isAnyNeighborsLoaded() { ++ return neighbourChunksLoadedBitset != 0; ++ } ++ public final boolean areNeighboursLoaded(final int radius) { ++ return LevelChunk.areNeighboursLoaded(this.neighbourChunksLoadedBitset, radius); ++ } ++ ++ public static boolean areNeighboursLoaded(final long bitset, final int radius) { ++ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))) ++ switch (radius) { ++ case 0: { ++ return (bitset & (1L << getNeighbourIndex(0, 0))) != 0; ++ } ++ case 1: { ++ long mask = 0L; ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ mask |= (1L << getNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ case 2: { ++ long mask = 0L; ++ for (int dx = -2; dx <= 2; ++dx) { ++ for (int dz = -2; dz <= 2; ++dz) { ++ mask |= (1L << getNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ case 3: { ++ long mask = 0L; ++ for (int dx = -3; dx <= 3; ++dx) { ++ for (int dz = -3; dz <= 3; ++dz) { ++ mask |= (1L << getNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ ++ default: ++ throw new IllegalArgumentException("Radius not recognized: " + radius); ++ } ++ } ++ // Paper end ++ + public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) { + this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData()); + Iterator iterator = protoChunk.getBlockEntities().values().iterator(); +@@ -181,8 +284,25 @@ public class LevelChunk extends ChunkAccess { + } + } + ++ // Paper start - Perf: Reduce instructions and provide final method ++ public BlockState getBlockState(final int x, final int y, final int z) { ++ return this.getBlockStateFinal(x, y, z); ++ } ++ public BlockState getBlockStateFinal(final int x, final int y, final int z) { ++ // Copied and modified from below ++ final int sectionIndex = this.getSectionIndex(y); ++ if (sectionIndex < 0 || sectionIndex >= this.sections.length ++ || this.sections[sectionIndex].nonEmptyBlockCount == 0) { ++ return Blocks.AIR.defaultBlockState(); ++ } ++ return this.sections[sectionIndex].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15); ++ } + @Override + public BlockState getBlockState(BlockPos pos) { ++ if (true) { ++ return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ()); ++ } ++ // Paper end - Perf: Reduce instructions and provide final method + int i = pos.getX(); + int j = pos.getY(); + int k = pos.getZ(); +@@ -224,6 +344,18 @@ public class LevelChunk extends ChunkAccess { + } + } + ++ // Paper start - If loaded util ++ @Override ++ public final FluidState getFluidIfLoaded(BlockPos blockposition) { ++ return this.getFluidState(blockposition); ++ } ++ ++ @Override ++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { ++ return this.getBlockState(blockposition); ++ } ++ // Paper end ++ + @Override + public FluidState getFluidState(BlockPos pos) { + return this.getFluidState(pos.getX(), pos.getY(), pos.getZ()); +@@ -549,7 +681,25 @@ public class LevelChunk extends ChunkAccess { + + // CraftBukkit start + public void loadCallback() { ++ // Paper start - neighbour cache ++ int chunkX = this.chunkPos.x; ++ int chunkZ = this.chunkPos.z; ++ net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource(); ++ for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) { ++ for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) { ++ LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz); ++ if (neighbour != null) { ++ neighbour.setNeighbourLoaded(-dx, -dz, this); ++ // should be in cached already ++ this.setNeighbourLoaded(dx, dz, neighbour); ++ } ++ } ++ } ++ this.setNeighbourLoaded(0, 0, this); ++ this.loadedTicketLevel = true; ++ // Paper end - neighbour cache + org.bukkit.Server server = this.level.getCraftServer(); ++ this.level.getChunkSource().addLoadedChunk(this); // Paper + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside +@@ -590,6 +740,22 @@ public class LevelChunk extends ChunkAccess { + server.getPluginManager().callEvent(unloadEvent); + // note: saving can be prevented, but not forced if no saving is actually required + this.mustNotSave = !unloadEvent.isSaveChunk(); ++ this.level.getChunkSource().removeLoadedChunk(this); // Paper ++ // Paper start - neighbour cache ++ int chunkX = this.chunkPos.x; ++ int chunkZ = this.chunkPos.z; ++ net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource(); ++ for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) { ++ for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) { ++ LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz); ++ if (neighbour != null) { ++ neighbour.setNeighbourUnloaded(-dx, -dz); ++ } ++ } ++ } ++ this.loadedTicketLevel = false; ++ this.resetNeighbours(); ++ // Paper end + } + + @Override +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 ca4c8f74a1ab2a8b36e193a2c40c3bd76565d258..2c153af611399e884752f8256bee4fe32de5c572 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -19,7 +19,7 @@ public class LevelChunkSection { + public static final int SECTION_HEIGHT = 16; + public static final int SECTION_SIZE = 4096; + public static final int BIOME_CONTAINER_BITS = 2; +- private short nonEmptyBlockCount; ++ short nonEmptyBlockCount; // Paper - package private + private short tickingBlockCount; + private short tickingFluidCount; + public final PalettedContainer<BlockState> states; +diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +index ae16b014abd52ee10d523fb003cce166b846b222..7f302405a88766c2112539d24d3dd2e513f94985 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +@@ -83,6 +83,18 @@ public class ProtoChunk extends ChunkAccess { + return new ChunkAccess.TicksToSave(this.blockTicks, this.fluidTicks); + } + ++ // Paper start - If loaded util ++ @Override ++ public final FluidState getFluidIfLoaded(BlockPos blockposition) { ++ return this.getFluidState(blockposition); ++ } ++ ++ @Override ++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { ++ return this.getBlockState(blockposition); ++ } ++ // Paper end ++ + @Override + public BlockState getBlockState(BlockPos pos) { + int i = pos.getY(); +diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +index 34933c5324126f9afdc5cba9dea997ace8f01806..219062cff8a05c765b092f1525043d9d9a1153ae 100644 +--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java ++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +@@ -91,6 +91,18 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A + } + + private boolean addEntity(T entity, boolean existing) { ++ // Paper start - chunk system hooks ++ if (existing) { ++ // I don't want to know why this is a generic type. ++ Entity entityCasted = (Entity)entity; ++ boolean wasRemoved = entityCasted.isRemoved(); ++ io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd((net.minecraft.server.level.ServerLevel) entityCasted.level(), entityCasted); ++ if (!wasRemoved && entityCasted.isRemoved()) { ++ // removed by callback ++ return false; ++ } ++ } ++ // Paper end - chunk system hooks + if (!this.addEntityUuid(entity)) { + return false; + } else { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index fe0f57dbeecc4b5a0c81863f33e41d11eb60943a..9babfd8e6c847ea26863be6243f17fc252dc9e1d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2593,4 +2593,9 @@ public final class CraftServer implements Server { + return this.spigot; + } + // Spigot end ++ ++ @Override ++ public double[] getTPS() { ++ return new double[]{0, 0, 0}; // TODO ++ } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 508419378c88ba8688edbd5142d9d8ba52396507..a59eebb89d11788b999d1e5cb4fd2f4e55e023ab 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -252,8 +252,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public Chunk[] getLoadedChunks() { +- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap; +- return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new); ++ List<ChunkHolder> chunks = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.world); // Paper ++ return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new); + } + + @Override +@@ -328,7 +328,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean refreshChunk(int x, int z) { +- ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z)); ++ ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + + playerChunk.getTickingChunkFuture().thenAccept(either -> { +@@ -2100,4 +2100,55 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return this.spigot; + } + // Spigot end ++ // Paper start ++ public java.util.concurrent.CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) { ++ if (Bukkit.isPrimaryThread()) { ++ net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); ++ if (immediate != null) { ++ return java.util.concurrent.CompletableFuture.completedFuture(new CraftChunk(immediate)); ++ } ++ } ++ ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority; ++ if (urgent) { ++ priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER; ++ } else { ++ priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL; ++ } ++ ++ java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>(); ++ ++ io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> { ++ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { ++ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; ++ ret.complete(chunk == null ? null : new CraftChunk(chunk)); ++ }); ++ }); ++ ++ return ret; ++ } ++ ++ @Override ++ public void setViewDistance(final int viewDistance) { ++ if (viewDistance < 2 || viewDistance > 32) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ this.getHandle().chunkSource.chunkMap.setServerViewDistance(viewDistance); ++ } ++ ++ @Override ++ public void setSimulationDistance(final int simulationDistance) { ++ throw new UnsupportedOperationException("Not implemented yet"); ++ } ++ ++ @Override ++ public int getSendViewDistance() { ++ return this.getViewDistance(); ++ } ++ ++ @Override ++ public void setSendViewDistance(final int viewDistance) { ++ throw new UnsupportedOperationException("Not implemented yet"); ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 91539582b65a87e2ed9901f2837c3148455d2669..c025542b1073ce7e6e30a43744276fd1fa1ca484 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2419,4 +2419,34 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.spigot; + } + // Spigot end ++ ++ @Override ++ public int getViewDistance() { ++ return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()); ++ } ++ ++ @Override ++ public void setViewDistance(final int viewDistance) { ++ throw new UnsupportedOperationException("Not implemented yet"); ++ } ++ ++ @Override ++ public int getSimulationDistance() { ++ return io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(this.getHandle()); ++ } ++ ++ @Override ++ public void setSimulationDistance(final int simulationDistance) { ++ throw new UnsupportedOperationException("Not implemented yet"); ++ } ++ ++ @Override ++ public int getSendViewDistance() { ++ return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(this.getHandle()); ++ } ++ ++ @Override ++ public void setSendViewDistance(final int viewDistance) { ++ throw new UnsupportedOperationException("Not implemented yet"); ++ } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +index 9e8526a1d1f4f66031903cac7eecbc4edc22a33b..c557a54f15943e07d272c3ad41ca4f90c4dc82ac 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -22,6 +22,20 @@ import org.bukkit.material.MaterialData; + @DelegateDeserialization(ItemStack.class) + public final class CraftItemStack extends ItemStack { + ++ // Paper start - MC Utils ++ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) { ++ if (bukkit instanceof CraftItemStack craftItemStack) { ++ return craftItemStack.handle != null ? craftItemStack.handle : net.minecraft.world.item.ItemStack.EMPTY; ++ } else { ++ return asNMSCopy(bukkit); ++ } ++ } ++ ++ public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) { ++ return old == newInstance ? unwrap(old) : asNMSCopy(newInstance); ++ } ++ // Paper end - MC Utils ++ + public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) { + if (original instanceof CraftItemStack) { + CraftItemStack stack = (CraftItemStack) original; +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index c017ce2ca1bc535795c958a2e509af2adf88efa9..0f7c3a44acf3c59ae43605e573f9da7f7c594647 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -46,6 +46,7 @@ import org.bukkit.scheduler.BukkitWorker; + */ + public class CraftScheduler implements BukkitScheduler { + ++ static Plugin MINECRAFT = new MinecraftInternalPlugin(); + /** + * The start ID for the counter. + */ +@@ -194,6 +195,11 @@ public class CraftScheduler implements BukkitScheduler { + this.runTaskTimer(plugin, (Object) task, delay, period); + } + ++ public BukkitTask scheduleInternalTask(Runnable run, int delay, String taskName) { ++ final CraftTask task = new CraftTask(run, nextId(), taskName); ++ return handle(task, delay); ++ } ++ + public BukkitTask runTaskTimer(Plugin plugin, Object runnable, long delay, long period) { + CraftScheduler.validate(plugin, runnable); + if (delay < 0L) { +@@ -417,13 +423,20 @@ public class CraftScheduler implements BukkitScheduler { + task.run(); + task.timings.stopTiming(); // Spigot + } catch (final Throwable throwable) { +- task.getOwner().getLogger().log( ++ // Paper start ++ String msg = String.format( ++ "Task #%s for %s generated an exception", ++ task.getTaskId(), ++ task.getOwner().getDescription().getFullName()); ++ if (task.getOwner() == MINECRAFT) { ++ net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); ++ } else { ++ task.getOwner().getLogger().log( + Level.WARNING, +- String.format( +- "Task #%s for %s generated an exception", +- task.getTaskId(), +- task.getOwner().getDescription().getFullName()), ++ msg, + throwable); ++ } ++ // Paper end + } finally { + this.currentTask = null; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +index e4d1eb4a0ce2c9874922585f6bb0d9ead433fde1..d56abf283f38548faa790c57045033f7ade6f958 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +@@ -40,6 +40,21 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + CraftTask(final Object task) { + this(null, task, CraftTask.NO_REPEATING, CraftTask.NO_REPEATING); + } ++ // Paper start ++ public String taskName = null; ++ boolean internal = false; ++ CraftTask(final Object task, int id, String taskName) { ++ this.rTask = (Runnable) task; ++ this.cTask = null; ++ this.plugin = CraftScheduler.MINECRAFT; ++ this.taskName = taskName; ++ this.internal = true; ++ this.id = id; ++ this.period = CraftTask.NO_REPEATING; ++ this.taskName = taskName; ++ this.timings = null; // Will be changed in later patch ++ } ++ // Paper end + + CraftTask(final Plugin plugin, final Object task, final int id, final long period) { + this.plugin = plugin; +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +new file mode 100644 +index 0000000000000000000000000000000000000000..909b2c98e7a9117d2f737245e4661792ffafb744 +--- /dev/null ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +@@ -0,0 +1,140 @@ ++package org.bukkit.craftbukkit.scheduler; ++ ++ ++import org.bukkit.Server; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.configuration.file.FileConfiguration; ++import org.bukkit.generator.BiomeProvider; ++import org.bukkit.generator.ChunkGenerator; ++import org.bukkit.plugin.PluginBase; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.PluginLoader; ++import org.bukkit.plugin.PluginLogger; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.io.File; ++import java.io.InputStream; ++import java.util.List; ++ ++public class MinecraftInternalPlugin extends PluginBase { ++ private boolean enabled = true; ++ ++ private final String pluginName; ++ private PluginDescriptionFile pdf; ++ ++ public MinecraftInternalPlugin() { ++ this.pluginName = "Minecraft"; ++ pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); ++ } ++ ++ public void setEnabled(boolean enabled) { ++ this.enabled = enabled; ++ } ++ ++ @Override ++ public File getDataFolder() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginDescriptionFile getDescription() { ++ return pdf; ++ } ++ ++ @Override ++ public FileConfiguration getConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public InputStream getResource(String filename) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void saveConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void saveDefaultConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void saveResource(String resourcePath, boolean replace) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void reloadConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginLogger getLogger() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginLoader getPluginLoader() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public Server getServer() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public boolean isEnabled() { ++ return enabled; ++ } ++ ++ @Override ++ public void onDisable() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void onLoad() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void onEnable() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public boolean isNaggable() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void setNaggable(boolean canNag) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index e08d4a45e313ef1b9005ef00ee0185a188171207..2fc68d129e2fdfd51e310ea5bdfb83322666c87b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -110,8 +110,17 @@ public final class CraftMagicNumbers implements UnsafeValues { + private static final Map<Item, Material> ITEM_MATERIAL = new HashMap<>(); + private static final Map<Material, Item> MATERIAL_ITEM = new HashMap<>(); + private static final Map<Material, Block> MATERIAL_BLOCK = new HashMap<>(); ++ // Paper start ++ private static final Map<org.bukkit.entity.EntityType, net.minecraft.world.entity.EntityType<?>> ENTITY_TYPE_ENTITY_TYPES = new HashMap<>(); ++ private static final Map<net.minecraft.world.entity.EntityType<?>, org.bukkit.entity.EntityType> ENTITY_TYPES_ENTITY_TYPE = new HashMap<>(); + + static { ++ for (org.bukkit.entity.EntityType type : org.bukkit.entity.EntityType.values()) { ++ if (type == org.bukkit.entity.EntityType.UNKNOWN) continue; ++ ENTITY_TYPE_ENTITY_TYPES.put(type, BuiltInRegistries.ENTITY_TYPE.get(CraftNamespacedKey.toMinecraft(type.getKey()))); ++ ENTITY_TYPES_ENTITY_TYPE.put(BuiltInRegistries.ENTITY_TYPE.get(CraftNamespacedKey.toMinecraft(type.getKey())), type); ++ } ++ // Paper end + for (Block block : BuiltInRegistries.BLOCK) { + BLOCK_MATERIAL.put(block, Material.getMaterial(BuiltInRegistries.BLOCK.getKey(block).getPath().toUpperCase(Locale.ROOT))); + } +@@ -162,6 +171,14 @@ public final class CraftMagicNumbers implements UnsafeValues { + public static ResourceLocation key(Material mat) { + return CraftNamespacedKey.toMinecraft(mat.getKey()); + } ++ // Paper start ++ public static net.minecraft.world.entity.EntityType<?> getEntityTypes(org.bukkit.entity.EntityType type) { ++ return ENTITY_TYPE_ENTITY_TYPES.get(type); ++ } ++ public static org.bukkit.entity.EntityType getEntityType(net.minecraft.world.entity.EntityType<?> entityTypes) { ++ return ENTITY_TYPES_ENTITY_TYPE.get(entityTypes); ++ } ++ // Paper end + // ======================================================================== + + public static byte toLegacyData(BlockState data) { +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..0794d92c42b0db6b367505ae28f09f1fd39fa312 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +@@ -58,6 +58,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; + import net.minecraft.world.ticks.LevelTickAccess; + import net.minecraft.world.ticks.TickPriority; + import org.bukkit.event.entity.CreatureSpawnEvent; ++import org.jetbrains.annotations.Nullable; + + public abstract class DelegatedGeneratorAccess implements WorldGenLevel { + +@@ -807,4 +808,24 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { + public int getMoonPhase() { + return this.handle.getMoonPhase(); + } ++ ++ // Paper start ++ @Nullable ++ @Override ++ public BlockState getBlockStateIfLoaded(final BlockPos blockposition) { ++ return null; ++ } ++ ++ @Nullable ++ @Override ++ public FluidState getFluidIfLoaded(final BlockPos blockposition) { ++ return null; ++ } ++ ++ @Nullable ++ @Override ++ public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) { ++ return null; ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +index e837d76e833d73d888bc1dad3515c2b82bc0e437..4705aed1dd98378c146bf9e346df1a17f719ad36 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +@@ -212,7 +212,23 @@ public class DummyGeneratorAccess implements WorldGenLevel { + public FluidState getFluidState(BlockPos pos) { + return Fluids.EMPTY.defaultFluidState(); // SPIGOT-6634 + } ++ // Paper start - if loaded util ++ @javax.annotation.Nullable ++ @Override ++ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ ++ @Override ++ public BlockState getBlockStateIfLoaded(BlockPos blockposition) { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } + ++ @Override ++ public FluidState getFluidIfLoaded(BlockPos blockposition) { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ // Paper end + @Override + public WorldBorder getWorldBorder() { + throw new UnsupportedOperationException("Not supported yet."); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +index 96c4f7aed548a181f6b1487e58dcf157bae52daa..837e3d6ee71566b5a6f37a49438291333c47f5d1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +@@ -120,6 +120,32 @@ public class UnsafeList<E> extends AbstractList<E> implements List<E>, RandomAcc + return this.indexOf(o) >= 0; + } + ++ // Paper start ++ protected transient int maxSize; ++ public void setSize(int size) { ++ if (this.maxSize < this.size) { ++ this.maxSize = this.size; ++ } ++ this.size = size; ++ } ++ ++ public void completeReset() { ++ if (this.data != null) { ++ Arrays.fill(this.data, 0, Math.max(this.size, this.maxSize), null); ++ } ++ this.size = 0; ++ this.maxSize = 0; ++ if (this.iterPool != null) { ++ for (Iterator temp : this.iterPool) { ++ if (temp == null) { ++ continue; ++ } ++ ((Itr)temp).valid = false; ++ } ++ } ++ } ++ // Paper end ++ + @Override + public void clear() { + // Create new array to reset memory usage to initial capacity +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 0c7c97f27853843ec714e47f5b570f9d09bbba14..ff422d4d4f2b764370f0ee2af13034853c1d3fe1 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -34,6 +34,9 @@ public class ActivationRange + + public enum ActivationType + { ++ WATER, // Paper ++ FLYING_MONSTER, // Paper ++ VILLAGER, // Paper + MONSTER, + ANIMAL, + RAIDER, +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index 08b0ca7b68bf238366f4d6904478852ecbe9394a..fbbc08c5a189b99f8047e0f0f5cd31101149dbec 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -118,7 +118,11 @@ public class SpigotConfig + } + } + } +- ++ // Paper start ++ SpigotConfig.save(); ++ } ++ public static void save() { ++ // Paper end + try + { + SpigotConfig.config.save( SpigotConfig.CONFIG_FILE ); diff --git a/patches/server/0010-Adventure.patch b/patches/server/0010-Adventure.patch new file mode 100644 index 0000000000..c47b6a48fc --- /dev/null +++ b/patches/server/0010-Adventure.patch @@ -0,0 +1,6156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Riley Park <[email protected]> +Date: Fri, 29 Jan 2021 17:54:03 +0100 +Subject: [PATCH] Adventure + +== AT == +public net.minecraft.network.chat.HoverEvent$ItemStackInfo item +public net.minecraft.network.chat.HoverEvent$ItemStackInfo count +public net.minecraft.network.chat.HoverEvent$ItemStackInfo components +public net.minecraft.network.chat.contents.TranslatableContents filterAllowedArguments(Ljava/lang/Object;)Lcom/mojang/serialization/DataResult; + +Co-authored-by: zml <[email protected]> +Co-authored-by: Jake Potrebic <[email protected]> + +diff --git a/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8dcedc5f4d4453fd942787dbcb9c757274ec7715 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java +@@ -0,0 +1,446 @@ ++package io.papermc.paper.adventure; ++ ++import com.google.gson.JsonElement; ++import com.google.gson.JsonParser; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.datafixers.util.Either; ++import com.mojang.datafixers.util.Pair; ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.DataResult; ++import com.mojang.serialization.DynamicOps; ++import com.mojang.serialization.JsonOps; ++import com.mojang.serialization.MapCodec; ++import com.mojang.serialization.codecs.RecordCodecBuilder; ++import java.io.IOException; ++import java.util.Collections; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.UUID; ++import java.util.function.Consumer; ++import java.util.function.Function; ++import java.util.function.Predicate; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.text.BlockNBTComponent; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.EntityNBTComponent; ++import net.kyori.adventure.text.KeybindComponent; ++import net.kyori.adventure.text.NBTComponent; ++import net.kyori.adventure.text.NBTComponentBuilder; ++import net.kyori.adventure.text.ScoreComponent; ++import net.kyori.adventure.text.SelectorComponent; ++import net.kyori.adventure.text.StorageNBTComponent; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.TranslatableComponent; ++import net.kyori.adventure.text.TranslationArgument; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.event.DataComponentValue; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.Style; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import net.minecraft.core.UUIDUtil; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.network.RegistryFriendlyByteBuf; ++import net.minecraft.network.chat.ComponentSerialization; ++import net.minecraft.network.chat.contents.KeybindContents; ++import net.minecraft.network.chat.contents.ScoreContents; ++import net.minecraft.network.chat.contents.TranslatableContents; ++import net.minecraft.network.codec.ByteBufCodecs; ++import net.minecraft.network.codec.StreamCodec; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.util.ExtraCodecs; ++import net.minecraft.util.StringRepresentable; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.ItemStack; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.intellij.lang.annotations.Subst; ++ ++import static com.mojang.serialization.Codec.recursive; ++import static com.mojang.serialization.codecs.RecordCodecBuilder.mapCodec; ++import static java.util.function.Function.identity; ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.TranslationArgument.bool; ++import static net.kyori.adventure.text.TranslationArgument.component; ++import static net.kyori.adventure.text.TranslationArgument.numeric; ++ ++@DefaultQualifier(NonNull.class) ++public final class AdventureCodecs { ++ ++ public static final Codec<Component> COMPONENT_CODEC = recursive("adventure Component", AdventureCodecs::createCodec); ++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_COMPONENT_CODEC = ByteBufCodecs.fromCodecWithRegistriesTrusted(COMPONENT_CODEC); ++ ++ static final Codec<TextColor> TEXT_COLOR_CODEC = Codec.STRING.comapFlatMap(s -> { ++ if (s.startsWith("#")) { ++ @Nullable TextColor value = TextColor.fromHexString(s); ++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure TextColor"); ++ } else { ++ final @Nullable NamedTextColor value = NamedTextColor.NAMES.value(s); ++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure NamedTextColor"); ++ } ++ }, textColor -> { ++ if (textColor instanceof NamedTextColor named) { ++ return NamedTextColor.NAMES.keyOrThrow(named); ++ } else { ++ return textColor.asHexString(); ++ } ++ }); ++ ++ static final Codec<Key> KEY_CODEC = Codec.STRING.comapFlatMap(s -> { ++ return Key.parseable(s) ? DataResult.success(Key.key(s)) : DataResult.error(() -> "Cannot convert " + s + " to adventure Key"); ++ }, Key::asString); ++ ++ static final Codec<ClickEvent.Action> CLICK_EVENT_ACTION_CODEC = Codec.STRING.comapFlatMap(s -> { ++ final ClickEvent.@Nullable Action value = ClickEvent.Action.NAMES.value(s); ++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure ClickEvent$Action"); ++ }, ClickEvent.Action.NAMES::keyOrThrow); ++ static final Codec<ClickEvent> CLICK_EVENT_CODEC = RecordCodecBuilder.create((instance) -> { ++ return instance.group( ++ CLICK_EVENT_ACTION_CODEC.fieldOf("action").forGetter(ClickEvent::action), ++ Codec.STRING.fieldOf("value").forGetter(ClickEvent::value) ++ ).apply(instance, ClickEvent::clickEvent); ++ }); ++ ++ static Codec<HoverEvent.ShowEntity> showEntityCodec(final Codec<Component> componentCodec) { ++ return RecordCodecBuilder.create((instance) -> { ++ return instance.group( ++ KEY_CODEC.fieldOf("type").forGetter(HoverEvent.ShowEntity::type), ++ UUIDUtil.LENIENT_CODEC.fieldOf("id").forGetter(HoverEvent.ShowEntity::id), ++ componentCodec.lenientOptionalFieldOf("name").forGetter(he -> Optional.ofNullable(he.name())) ++ ).apply(instance, (key, uuid, component) -> { ++ return HoverEvent.ShowEntity.showEntity(key, uuid, component.orElse(null)); ++ }); ++ }); ++ } ++ ++ static Codec<HoverEvent.ShowItem> showItemCodec(final Codec<Component> componentCodec) { ++ return net.minecraft.network.chat.HoverEvent.ItemStackInfo.CODEC.xmap(isi -> { ++ @Subst("key") final String typeKey = isi.item.unwrapKey().orElseThrow().location().toString(); ++ return HoverEvent.ShowItem.showItem(Key.key(typeKey), isi.count, PaperAdventure.asAdventure(isi.getItemStack().getComponentsPatch())); ++ }, si -> { ++ final Item itemType = BuiltInRegistries.ITEM.get(PaperAdventure.asVanilla(si.item())); ++ final Map<Key, DataComponentValue> dataComponentsMap = si.dataComponents(); ++ final ItemStack stack = new ItemStack(BuiltInRegistries.ITEM.wrapAsHolder(itemType), si.count(), PaperAdventure.asVanilla(dataComponentsMap)); ++ return new net.minecraft.network.chat.HoverEvent.ItemStackInfo(stack); ++ }); ++ } ++ ++ static final HoverEventType<HoverEvent.ShowEntity> SHOW_ENTITY_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showEntityCodec, HoverEvent.Action.SHOW_ENTITY, "show_entity", AdventureCodecs::legacyDeserializeEntity); ++ static final HoverEventType<HoverEvent.ShowItem> SHOW_ITEM_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showItemCodec, HoverEvent.Action.SHOW_ITEM, "show_item", AdventureCodecs::legacyDeserializeItem); ++ static final HoverEventType<Component> SHOW_TEXT_HOVER_EVENT_TYPE = new HoverEventType<>(identity(), HoverEvent.Action.SHOW_TEXT, "show_text", (component, registryOps, codec) -> DataResult.success(component)); ++ static final Codec<HoverEventType<?>> HOVER_EVENT_TYPE_CODEC = StringRepresentable.fromValues(() -> new HoverEventType<?>[]{ SHOW_ENTITY_HOVER_EVENT_TYPE, SHOW_ITEM_HOVER_EVENT_TYPE, SHOW_TEXT_HOVER_EVENT_TYPE }); ++ ++ static DataResult<HoverEvent.ShowEntity> legacyDeserializeEntity(final Component component, final @Nullable RegistryOps<?> ops, final Codec<Component> componentCodec) { ++ try { ++ final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(component)); ++ final DynamicOps<JsonElement> dynamicOps = ops != null ? ops.withParent(JsonOps.INSTANCE) : JsonOps.INSTANCE; ++ final DataResult<Component> entityNameResult = componentCodec.parse(dynamicOps, JsonParser.parseString(tag.getString("name"))); ++ @Subst("key") final String keyString = tag.getString("type"); ++ final UUID entityUUID = UUID.fromString(tag.getString("id")); ++ return entityNameResult.map(name -> HoverEvent.ShowEntity.showEntity(Key.key(keyString), entityUUID, name)); ++ } catch (final Exception ex) { ++ return DataResult.error(() -> "Failed to parse tooltip: " + ex.getMessage()); ++ } ++ } ++ ++ static DataResult<HoverEvent.ShowItem> legacyDeserializeItem(final Component component, final @Nullable RegistryOps<?> ops, final Codec<Component> componentCodec) { ++ try { ++ final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(component)); ++ final DynamicOps<Tag> dynamicOps = ops != null ? ops.withParent(NbtOps.INSTANCE) : NbtOps.INSTANCE; ++ final DataResult<ItemStack> stackResult = ItemStack.CODEC.parse(dynamicOps, tag); ++ return stackResult.map(stack -> { ++ @Subst("key:value") final String location = stack.getItemHolder().unwrapKey().orElseThrow().location().toString(); ++ return HoverEvent.ShowItem.showItem(Key.key(location), stack.getCount(), PaperAdventure.asAdventure(stack.getComponentsPatch())); ++ }); ++ } catch (final CommandSyntaxException ex) { ++ return DataResult.error(() -> "Failed to parse item tag: " + ex.getMessage()); ++ } ++ } ++ ++ @FunctionalInterface ++ interface LegacyDeserializer<T> { ++ DataResult<T> apply(Component component, @Nullable RegistryOps<?> ops, Codec<Component> componentCodec); ++ } ++ ++ record HoverEventType<V>(Function<Codec<Component>, MapCodec<HoverEvent<V>>> codec, String id, Function<Codec<Component>, MapCodec<HoverEvent<V>>> legacyCodec) implements StringRepresentable { ++ HoverEventType(final Function<Codec<Component>, Codec<V>> contentCodec, final HoverEvent.Action<V> action, final String id, final LegacyDeserializer<V> legacyDeserializer) { ++ this(cc -> contentCodec.apply(cc).xmap(v -> HoverEvent.hoverEvent(action, v), HoverEvent::value).fieldOf("contents"), ++ id, ++ codec -> (new Codec<HoverEvent<V>>() { ++ public <D> DataResult<Pair<HoverEvent<V>, D>> decode(final DynamicOps<D> dynamicOps, final D object) { ++ return codec.decode(dynamicOps, object).flatMap(pair -> { ++ final DataResult<V> dataResult; ++ if (dynamicOps instanceof final RegistryOps<D> registryOps) { ++ dataResult = legacyDeserializer.apply(pair.getFirst(), registryOps, codec); ++ } else { ++ dataResult = legacyDeserializer.apply(pair.getFirst(), null, codec); ++ } ++ ++ return dataResult.map(value -> Pair.of(HoverEvent.hoverEvent(action, value), pair.getSecond())); ++ }); ++ } ++ ++ public <D> DataResult<D> encode(final HoverEvent<V> hoverEvent, final DynamicOps<D> dynamicOps, final D object) { ++ return DataResult.error(() -> "Can't encode in legacy format"); ++ } ++ }).fieldOf("value") ++ ); ++ } ++ @Override ++ public String getSerializedName() { ++ return this.id; ++ } ++ } ++ ++ private static final Function<HoverEvent<?>, HoverEventType<?>> GET_HOVER_EVENT_TYPE = he -> { ++ if (he.action() == HoverEvent.Action.SHOW_ENTITY) { ++ return SHOW_ENTITY_HOVER_EVENT_TYPE; ++ } else if (he.action() == HoverEvent.Action.SHOW_ITEM) { ++ return SHOW_ITEM_HOVER_EVENT_TYPE; ++ } else if (he.action() == HoverEvent.Action.SHOW_TEXT) { ++ return SHOW_TEXT_HOVER_EVENT_TYPE; ++ } else { ++ throw new IllegalStateException(); ++ } ++ }; ++ static final Codec<HoverEvent<?>> HOVER_EVENT_CODEC = Codec.withAlternative( ++ HOVER_EVENT_TYPE_CODEC.<HoverEvent<?>>dispatchMap("action", GET_HOVER_EVENT_TYPE, het -> het.codec.apply(COMPONENT_CODEC)).codec(), ++ HOVER_EVENT_TYPE_CODEC.<HoverEvent<?>>dispatchMap("action", GET_HOVER_EVENT_TYPE, het -> het.legacyCodec.apply(COMPONENT_CODEC)).codec() ++ ); ++ ++ public static final MapCodec<Style> STYLE_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ TEXT_COLOR_CODEC.optionalFieldOf("color").forGetter(nullableGetter(Style::color)), ++ Codec.BOOL.optionalFieldOf("bold").forGetter(decorationGetter(TextDecoration.BOLD)), ++ Codec.BOOL.optionalFieldOf("italic").forGetter(decorationGetter(TextDecoration.ITALIC)), ++ Codec.BOOL.optionalFieldOf("underlined").forGetter(decorationGetter(TextDecoration.UNDERLINED)), ++ Codec.BOOL.optionalFieldOf("strikethrough").forGetter(decorationGetter(TextDecoration.STRIKETHROUGH)), ++ Codec.BOOL.optionalFieldOf("obfuscated").forGetter(decorationGetter(TextDecoration.OBFUSCATED)), ++ CLICK_EVENT_CODEC.optionalFieldOf("clickEvent").forGetter(nullableGetter(Style::clickEvent)), ++ HOVER_EVENT_CODEC.optionalFieldOf("hoverEvent").forGetter(nullableGetter(Style::hoverEvent)), ++ Codec.STRING.optionalFieldOf("insertion").forGetter(nullableGetter(Style::insertion)), ++ KEY_CODEC.optionalFieldOf("font").forGetter(nullableGetter(Style::font)) ++ ).apply(instance, (textColor, bold, italic, underlined, strikethrough, obfuscated, clickEvent, hoverEvent, insertion, font) -> { ++ return Style.style(builder -> { ++ textColor.ifPresent(builder::color); ++ bold.ifPresent(styleBooleanConsumer(builder, TextDecoration.BOLD)); ++ italic.ifPresent(styleBooleanConsumer(builder, TextDecoration.ITALIC)); ++ underlined.ifPresent(styleBooleanConsumer(builder, TextDecoration.UNDERLINED)); ++ strikethrough.ifPresent(styleBooleanConsumer(builder, TextDecoration.STRIKETHROUGH)); ++ obfuscated.ifPresent(styleBooleanConsumer(builder, TextDecoration.OBFUSCATED)); ++ clickEvent.ifPresent(builder::clickEvent); ++ hoverEvent.ifPresent(builder::hoverEvent); ++ insertion.ifPresent(builder::insertion); ++ font.ifPresent(builder::font); ++ }); ++ }); ++ }); ++ static Consumer<Boolean> styleBooleanConsumer(final Style.Builder builder, final TextDecoration decoration) { ++ return b -> builder.decoration(decoration, b); ++ } ++ ++ static Function<Style, Optional<Boolean>> decorationGetter(final TextDecoration decoration) { ++ return style -> Optional.ofNullable(style.decoration(decoration) == TextDecoration.State.NOT_SET ? null : style.decoration(decoration) == TextDecoration.State.TRUE); ++ } ++ ++ static <R, T> Function<R, Optional<T>> nullableGetter(final Function<R, @Nullable T> getter) { ++ return style -> Optional.ofNullable(getter.apply(style)); ++ } ++ ++ static final MapCodec<TextComponent> TEXT_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group(Codec.STRING.fieldOf("text").forGetter(TextComponent::content)).apply(instance, Component::text); ++ }); ++ static final Codec<Object> PRIMITIVE_ARG_CODEC = ExtraCodecs.JAVA.validate(TranslatableContents::filterAllowedArguments); ++ static final Codec<TranslationArgument> ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, COMPONENT_CODEC).flatXmap((primitiveOrComponent) -> { ++ return primitiveOrComponent.map(o -> { ++ final TranslationArgument arg; ++ if (o instanceof String s) { ++ arg = component(text(s)); ++ } else if (o instanceof Boolean bool) { ++ arg = bool(bool); ++ } else if (o instanceof Number num) { ++ arg = numeric(num); ++ } else { ++ return DataResult.error(() -> o + " is not a valid translation argument primitive"); ++ } ++ return DataResult.success(arg); ++ }, component -> DataResult.success(component(component))); ++ }, translationArgument -> { ++ if (translationArgument.value() instanceof Number || translationArgument.value() instanceof Boolean) { ++ return DataResult.success(Either.left(translationArgument.value())); ++ } ++ final Component component = translationArgument.asComponent(); ++ final @Nullable String collapsed = tryCollapseToString(component); ++ if (collapsed != null) { ++ return DataResult.success(Either.left(collapsed)); // attempt to collapse all text components to strings ++ } ++ return DataResult.success(Either.right(component)); ++ }); ++ static final MapCodec<TranslatableComponent> TRANSLATABLE_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ Codec.STRING.fieldOf("translate").forGetter(TranslatableComponent::key), ++ Codec.STRING.lenientOptionalFieldOf("fallback").forGetter(nullableGetter(TranslatableComponent::fallback)), ++ ARG_CODEC.listOf().optionalFieldOf("with").forGetter(c -> c.arguments().isEmpty() ? Optional.empty() : Optional.of(c.arguments())) ++ ).apply(instance, (key, fallback, components) -> { ++ return Component.translatable(key, components.orElse(Collections.emptyList())).fallback(fallback.orElse(null)); ++ }); ++ }); ++ ++ static final MapCodec<KeybindComponent> KEYBIND_COMPONENT_MAP_CODEC = KeybindContents.CODEC.xmap(k -> Component.keybind(k.getName()), k -> new KeybindContents(k.keybind())); ++ static final MapCodec<ScoreComponent> SCORE_COMPONENT_INNER_MAP_CODEC = ScoreContents.INNER_CODEC.xmap(s -> Component.score(s.getName(), s.getObjective()), s -> new ScoreContents(s.name(), s.objective())); ++ static final MapCodec<ScoreComponent> SCORE_COMPONENT_MAP_CODEC = SCORE_COMPONENT_INNER_MAP_CODEC.fieldOf("score"); ++ static final MapCodec<SelectorComponent> SELECTOR_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ Codec.STRING.fieldOf("selector").forGetter(SelectorComponent::pattern), ++ COMPONENT_CODEC.optionalFieldOf("separator").forGetter(nullableGetter(SelectorComponent::separator)) ++ ).apply(instance, (selector, component) -> Component.selector(selector, component.orElse(null))); ++ }); ++ ++ interface NbtComponentDataSource { ++ NBTComponentBuilder<?, ?> builder(); ++ ++ DataSourceType<?> type(); ++ } ++ ++ record StorageDataSource(Key storage) implements NbtComponentDataSource { ++ @Override ++ public NBTComponentBuilder<?, ?> builder() { ++ return Component.storageNBT().storage(this.storage()); ++ } ++ ++ @Override ++ public DataSourceType<?> type() { ++ return STORAGE_DATA_SOURCE_TYPE; ++ } ++ } ++ ++ record BlockDataSource(String posPattern) implements NbtComponentDataSource { ++ @Override ++ public NBTComponentBuilder<?, ?> builder() { ++ return Component.blockNBT().pos(BlockNBTComponent.Pos.fromString(this.posPattern)); ++ } ++ ++ @Override ++ public DataSourceType<?> type() { ++ return BLOCK_DATA_SOURCE_TYPE; ++ } ++ } ++ ++ record EntityDataSource(String selectorPattern) implements NbtComponentDataSource { ++ @Override ++ public NBTComponentBuilder<?, ?> builder() { ++ return Component.entityNBT().selector(this.selectorPattern()); ++ } ++ ++ @Override ++ public DataSourceType<?> type() { ++ return ENTITY_DATA_SOURCE_TYPE; ++ } ++ } ++ ++ static final DataSourceType<StorageDataSource> STORAGE_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(KEY_CODEC.fieldOf("storage").forGetter(StorageDataSource::storage)).apply(instance, StorageDataSource::new)), "storage"); ++ static final DataSourceType<BlockDataSource> BLOCK_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(Codec.STRING.fieldOf("block").forGetter(BlockDataSource::posPattern)).apply(instance, BlockDataSource::new)), "block"); ++ static final DataSourceType<EntityDataSource> ENTITY_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(Codec.STRING.fieldOf("entity").forGetter(EntityDataSource::selectorPattern)).apply(instance, EntityDataSource::new)), "entity"); ++ ++ static final MapCodec<NbtComponentDataSource> NBT_COMPONENT_DATA_SOURCE_CODEC = ComponentSerialization.createLegacyComponentMatcher(new DataSourceType<?>[]{ENTITY_DATA_SOURCE_TYPE, BLOCK_DATA_SOURCE_TYPE, STORAGE_DATA_SOURCE_TYPE}, DataSourceType::codec, NbtComponentDataSource::type, "source"); ++ ++ record DataSourceType<D extends NbtComponentDataSource>(MapCodec<D> codec, String id) implements StringRepresentable { ++ @Override ++ public String getSerializedName() { ++ return this.id(); ++ } ++ } ++ ++ static final MapCodec<NBTComponent<?, ?>> NBT_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ Codec.STRING.fieldOf("nbt").forGetter(NBTComponent::nbtPath), ++ Codec.BOOL.lenientOptionalFieldOf("interpret", false).forGetter(NBTComponent::interpret), ++ COMPONENT_CODEC.lenientOptionalFieldOf("separator").forGetter(nullableGetter(NBTComponent::separator)), ++ NBT_COMPONENT_DATA_SOURCE_CODEC.forGetter(nbtComponent -> { ++ if (nbtComponent instanceof final EntityNBTComponent entityNBTComponent) { ++ return new EntityDataSource(entityNBTComponent.selector()); ++ } else if (nbtComponent instanceof final BlockNBTComponent blockNBTComponent) { ++ return new BlockDataSource(blockNBTComponent.pos().asString()); ++ } else if (nbtComponent instanceof final StorageNBTComponent storageNBTComponent) { ++ return new StorageDataSource(storageNBTComponent.storage()); ++ } else { ++ throw new IllegalArgumentException(nbtComponent + " isn't a valid nbt component"); ++ } ++ }) ++ ).apply(instance, (nbtPath, interpret, separator, dataSource) -> { ++ return dataSource.builder().nbtPath(nbtPath).interpret(interpret).separator(separator.orElse(null)).build(); ++ }); ++ }); ++ ++ @SuppressWarnings("NonExtendableApiUsage") ++ record ComponentType<C extends Component>(MapCodec<C> codec, Predicate<Component> test, String id) implements StringRepresentable { ++ @Override ++ public String getSerializedName() { ++ return this.id; ++ } ++ } ++ ++ static final ComponentType<TextComponent> PLAIN = new ComponentType<>(TEXT_COMPONENT_MAP_CODEC, TextComponent.class::isInstance, "text"); ++ static final ComponentType<TranslatableComponent> TRANSLATABLE = new ComponentType<>(TRANSLATABLE_COMPONENT_MAP_CODEC, TranslatableComponent.class::isInstance, "translatable"); ++ static final ComponentType<KeybindComponent> KEYBIND = new ComponentType<>(KEYBIND_COMPONENT_MAP_CODEC, KeybindComponent.class::isInstance, "keybind"); ++ static final ComponentType<ScoreComponent> SCORE = new ComponentType<>(SCORE_COMPONENT_MAP_CODEC, ScoreComponent.class::isInstance, "score"); ++ static final ComponentType<SelectorComponent> SELECTOR = new ComponentType<>(SELECTOR_COMPONENT_MAP_CODEC, SelectorComponent.class::isInstance, "selector"); ++ static final ComponentType<NBTComponent<?, ?>> NBT = new ComponentType<>(NBT_COMPONENT_MAP_CODEC, NBTComponent.class::isInstance, "nbt"); ++ ++ static Codec<Component> createCodec(final Codec<Component> selfCodec) { ++ final ComponentType<?>[] types = new ComponentType<?>[]{PLAIN, TRANSLATABLE, KEYBIND, SCORE, SELECTOR, NBT}; ++ final MapCodec<Component> legacyCodec = ComponentSerialization.createLegacyComponentMatcher(types, ComponentType::codec, component -> { ++ for (final ComponentType<?> type : types) { ++ if (type.test().test(component)) { ++ return type; ++ } ++ } ++ throw new IllegalStateException("Unexpected component type " + component); ++ }, "type"); ++ ++ final Codec<Component> directCodec = RecordCodecBuilder.create((instance) -> { ++ return instance.group( ++ legacyCodec.forGetter(identity()), ++ ExtraCodecs.nonEmptyList(selfCodec.listOf()).optionalFieldOf("extra", List.of()).forGetter(Component::children), ++ STYLE_MAP_CODEC.forGetter(Component::style) ++ ).apply(instance, (component, children, style) -> { ++ return component.style(style).children(children); ++ }); ++ }); ++ ++ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), directCodec).xmap((stringOrListOrComponent) -> { ++ return stringOrListOrComponent.map((stringOrList) -> stringOrList.map(Component::text, AdventureCodecs::createFromList), identity()); ++ }, (text) -> { ++ final @Nullable String string = tryCollapseToString(text); ++ return string != null ? Either.left(Either.left(string)) : Either.right(text); ++ }); ++ } ++ ++ public static @Nullable String tryCollapseToString(final Component component) { ++ if (component instanceof final TextComponent textComponent) { ++ if (component.children().isEmpty() && component.style().isEmpty()) { ++ return textComponent.content(); ++ } ++ } ++ return null; ++ } ++ ++ static Component createFromList(final List<? extends Component> components) { ++ Component component = components.get(0); ++ for (int i = 1; i < components.size(); i++) { ++ component = component.append(components.get(i)); ++ } ++ return component; ++ } ++ ++ private AdventureCodecs() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/AdventureComponent.java b/src/main/java/io/papermc/paper/adventure/AdventureComponent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c9d787f4e66f152b557229fdb1d9a3ac83a7d71f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/AdventureComponent.java +@@ -0,0 +1,88 @@ ++package io.papermc.paper.adventure; ++ ++import java.util.List; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import net.minecraft.network.chat.ComponentContents; ++import net.minecraft.network.chat.MutableComponent; ++import net.minecraft.network.chat.Style; ++import net.minecraft.network.chat.contents.PlainTextContents; ++import net.minecraft.util.FormattedCharSequence; ++import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ++import org.jetbrains.annotations.Nullable; ++ ++public final class AdventureComponent implements net.minecraft.network.chat.Component { ++ final Component adventure; ++ private net.minecraft.network.chat.@MonotonicNonNull Component vanilla; ++ ++ public AdventureComponent(final Component adventure) { ++ this.adventure = adventure; ++ } ++ ++ public net.minecraft.network.chat.Component deepConverted() { ++ net.minecraft.network.chat.Component vanilla = this.vanilla; ++ if (vanilla == null) { ++ vanilla = PaperAdventure.WRAPPER_AWARE_SERIALIZER.serialize(this.adventure); ++ this.vanilla = vanilla; ++ } ++ return vanilla; ++ } ++ ++ public net.minecraft.network.chat.@Nullable Component deepConvertedIfPresent() { ++ return this.vanilla; ++ } ++ ++ @Override ++ public Style getStyle() { ++ return this.deepConverted().getStyle(); ++ } ++ ++ @Override ++ public ComponentContents getContents() { ++ if (this.adventure instanceof TextComponent) { ++ return PlainTextContents.create(((TextComponent) this.adventure).content()); ++ } else { ++ return this.deepConverted().getContents(); ++ } ++ } ++ ++ @Override ++ public String getString() { ++ return PlainTextComponentSerializer.plainText().serialize(this.adventure); ++ } ++ ++ @Override ++ public List<net.minecraft.network.chat.Component> getSiblings() { ++ return this.deepConverted().getSiblings(); ++ } ++ ++ @Override ++ public MutableComponent plainCopy() { ++ return this.deepConverted().plainCopy(); ++ } ++ ++ @Override ++ public MutableComponent copy() { ++ return this.deepConverted().copy(); ++ } ++ ++ @Override ++ public FormattedCharSequence getVisualOrderText() { ++ return this.deepConverted().getVisualOrderText(); ++ } ++ ++ public Component adventure$component() { ++ return this.adventure; ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.deepConverted().hashCode(); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ return this.deepConverted().equals(obj); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java b/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..23bd6d2d8fed5a3491e856f8b875456dd29f8aaf +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java +@@ -0,0 +1,85 @@ ++package io.papermc.paper.adventure; ++ ++import com.google.common.collect.Collections2; ++import java.util.Set; ++import java.util.function.Function; ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.bossbar.BossBarImplementation; ++import net.kyori.adventure.bossbar.BossBarViewer; ++import net.kyori.adventure.text.Component; ++import net.minecraft.network.protocol.game.ClientboundBossEventPacket; ++import net.minecraft.server.level.ServerBossEvent; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.BossEvent; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.jetbrains.annotations.NotNull; ++ ++@SuppressWarnings("UnstableApiUsage") ++public final class BossBarImplementationImpl implements BossBar.Listener, BossBarImplementation { ++ private final BossBar bar; ++ private ServerBossEvent vanilla; ++ ++ public BossBarImplementationImpl(final BossBar bar) { ++ this.bar = bar; ++ } ++ ++ public void playerShow(final CraftPlayer player) { ++ if (this.vanilla == null) { ++ this.vanilla = new ServerBossEvent( ++ PaperAdventure.asVanilla(this.bar.name()), ++ PaperAdventure.asVanilla(this.bar.color()), ++ PaperAdventure.asVanilla(this.bar.overlay()) ++ ); ++ this.vanilla.adventure = this.bar; ++ this.bar.addListener(this); ++ } ++ this.vanilla.addPlayer(player.getHandle()); ++ } ++ ++ public void playerHide(final CraftPlayer player) { ++ if (this.vanilla != null) { ++ this.vanilla.removePlayer(player.getHandle()); ++ if (this.vanilla.getPlayers().isEmpty()) { ++ this.bar.removeListener(this); ++ this.vanilla = null; ++ } ++ } ++ } ++ ++ @Override ++ public void bossBarNameChanged(final @NonNull BossBar bar, final @NonNull Component oldName, final @NonNull Component newName) { ++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateNamePacket); ++ } ++ ++ @Override ++ public void bossBarProgressChanged(final @NonNull BossBar bar, final float oldProgress, final float newProgress) { ++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateProgressPacket); ++ } ++ ++ @Override ++ public void bossBarColorChanged(final @NonNull BossBar bar, final BossBar.@NonNull Color oldColor, final BossBar.@NonNull Color newColor) { ++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateStylePacket); ++ } ++ ++ @Override ++ public void bossBarOverlayChanged(final @NonNull BossBar bar, final BossBar.@NonNull Overlay oldOverlay, final BossBar.@NonNull Overlay newOverlay) { ++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateStylePacket); ++ } ++ ++ @Override ++ public void bossBarFlagsChanged(final @NonNull BossBar bar, final @NonNull Set<BossBar.Flag> flagsAdded, final @NonNull Set<BossBar.Flag> flagsRemoved) { ++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdatePropertiesPacket); ++ } ++ ++ @Override ++ public @NotNull Iterable<? extends BossBarViewer> viewers() { ++ return this.vanilla == null ? Set.of() : Collections2.transform(this.vanilla.getPlayers(), ServerPlayer::getBukkitEntity); ++ } ++ ++ private void maybeBroadcast(final Function<BossEvent, ClientboundBossEventPacket> fn) { ++ if (this.vanilla != null) { ++ this.vanilla.broadcast(fn); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69347ac0547cba3842040f89615e721b5d1ac3ca +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +@@ -0,0 +1,376 @@ ++package io.papermc.paper.adventure; ++ ++import io.papermc.paper.chat.ChatRenderer; ++import io.papermc.paper.event.player.AbstractChatEvent; ++import io.papermc.paper.event.player.AsyncChatEvent; ++import io.papermc.paper.event.player.ChatEvent; ++import java.lang.reflect.Field; ++import java.lang.reflect.Modifier; ++import java.util.BitSet; ++import java.util.Collection; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Objects; ++import java.util.Set; ++import java.util.concurrent.ExecutionException; ++import java.util.function.Function; ++import net.kyori.adventure.audience.Audience; ++import net.kyori.adventure.audience.ForwardingAudience; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; ++import net.minecraft.Optionull; ++import net.minecraft.Util; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.network.chat.ChatType; ++import net.minecraft.network.chat.OutgoingChatMessage; ++import net.minecraft.network.chat.PlayerChatMessage; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.command.ConsoleCommandSender; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.craftbukkit.util.LazyPlayerSet; ++import org.bukkit.craftbukkit.util.Waitable; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.AsyncPlayerChatEvent; ++import org.bukkit.event.player.PlayerChatEvent; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.intellij.lang.annotations.Subst; ++ ++import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection; ++ ++@DefaultQualifier(NonNull.class) ++public final class ChatProcessor { ++ static final ResourceKey<ChatType> PAPER_RAW = ResourceKey.create(Registries.CHAT_TYPE, new ResourceLocation(ResourceLocation.PAPER_NAMESPACE, "raw")); ++ static final String DEFAULT_LEGACY_FORMAT = "<%1$s> %2$s"; // copied from PlayerChatEvent/AsyncPlayerChatEvent ++ final MinecraftServer server; ++ final ServerPlayer player; ++ final PlayerChatMessage message; ++ final boolean async; ++ final String craftbukkit$originalMessage; ++ final Component paper$originalMessage; ++ final OutgoingChatMessage outgoing; ++ ++ static final int MESSAGE_CHANGED = 1; ++ static final int FORMAT_CHANGED = 2; ++ static final int SENDER_CHANGED = 3; // Not used ++ private final BitSet flags = new BitSet(3); ++ ++ public ChatProcessor(final MinecraftServer server, final ServerPlayer player, final PlayerChatMessage message, final boolean async) { ++ this.server = server; ++ this.player = player; ++ this.message = message; ++ this.async = async; ++ this.craftbukkit$originalMessage = message.unsignedContent() != null ? LegacyComponentSerializer.legacySection().serialize(PaperAdventure.asAdventure(message.unsignedContent())) : message.signedContent(); ++ this.paper$originalMessage = PaperAdventure.asAdventure(this.message.decoratedContent()); ++ this.outgoing = OutgoingChatMessage.create(this.message); ++ } ++ ++ @SuppressWarnings("deprecated") ++ public void process() { ++ final boolean listenersOnAsyncEvent = canYouHearMe(AsyncPlayerChatEvent.getHandlerList()); ++ final boolean listenersOnSyncEvent = canYouHearMe(PlayerChatEvent.getHandlerList()); ++ if (listenersOnAsyncEvent || listenersOnSyncEvent) { ++ final CraftPlayer player = this.player.getBukkitEntity(); ++ final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server)); ++ this.post(ae); ++ if (listenersOnSyncEvent) { ++ final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients()); ++ se.setCancelled(ae.isCancelled()); // propagate cancelled state ++ this.queueIfAsyncOrRunImmediately(new Waitable<Void>() { ++ @Override ++ protected Void evaluate() { ++ ChatProcessor.this.post(se); ++ return null; ++ } ++ }); ++ this.readLegacyModifications(se.getMessage(), se.getFormat(), se.getPlayer()); ++ this.processModern( ++ this.modernRenderer(se.getFormat()), ++ this.viewersFromLegacy(se.getRecipients()), ++ this.modernMessage(se.getMessage()), ++ se.getPlayer(), ++ se.isCancelled() ++ ); ++ } else { ++ this.readLegacyModifications(ae.getMessage(), ae.getFormat(), ae.getPlayer()); ++ this.processModern( ++ this.modernRenderer(ae.getFormat()), ++ this.viewersFromLegacy(ae.getRecipients()), ++ this.modernMessage(ae.getMessage()), ++ ae.getPlayer(), ++ ae.isCancelled() ++ ); ++ } ++ } else { ++ this.processModern( ++ defaultRenderer(), ++ new LazyChatAudienceSet(this.server), ++ this.paper$originalMessage, ++ this.player.getBukkitEntity(), ++ false ++ ); ++ } ++ } ++ ++ private ChatRenderer modernRenderer(final String format) { ++ if (this.flags.get(FORMAT_CHANGED)) { ++ return legacyRenderer(format); ++ } else { ++ return defaultRenderer(); ++ } ++ } ++ ++ private Component modernMessage(final String legacyMessage) { ++ if (this.flags.get(MESSAGE_CHANGED)) { ++ return legacySection().deserialize(legacyMessage); ++ } else { ++ return this.paper$originalMessage; ++ } ++ } ++ ++ private void readLegacyModifications(final String message, final String format, final Player playerSender) { ++ this.flags.set(MESSAGE_CHANGED, !message.equals(this.craftbukkit$originalMessage)); ++ this.flags.set(FORMAT_CHANGED, !format.equals(DEFAULT_LEGACY_FORMAT)); ++ this.flags.set(SENDER_CHANGED, playerSender != this.player.getBukkitEntity()); ++ } ++ ++ private void processModern(final ChatRenderer renderer, final Set<Audience> viewers, final Component message, final Player player, final boolean cancelled) { ++ final PlayerChatMessage.AdventureView signedMessage = this.message.adventureView(); ++ final AsyncChatEvent ae = new AsyncChatEvent(this.async, player, viewers, renderer, message, this.paper$originalMessage, signedMessage); ++ ae.setCancelled(cancelled); // propagate cancelled state ++ this.post(ae); ++ final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList()); ++ if (listenersOnSyncEvent) { ++ this.queueIfAsyncOrRunImmediately(new Waitable<Void>() { ++ @Override ++ protected Void evaluate() { ++ final ChatEvent se = new ChatEvent(player, ae.viewers(), ae.renderer(), ae.message(), ChatProcessor.this.paper$originalMessage/*, ae.usePreviewComponent()*/, signedMessage); ++ se.setCancelled(ae.isCancelled()); // propagate cancelled state ++ ChatProcessor.this.post(se); ++ ChatProcessor.this.readModernModifications(se, renderer); ++ ChatProcessor.this.complete(se); ++ return null; ++ } ++ }); ++ } else { ++ this.readModernModifications(ae, renderer); ++ this.complete(ae); ++ } ++ } ++ ++ private void readModernModifications(final AbstractChatEvent chatEvent, final ChatRenderer originalRenderer) { ++ this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.paper$originalMessage)); ++ if (originalRenderer != chatEvent.renderer()) { // don't set to false if it hasn't changed ++ this.flags.set(FORMAT_CHANGED, true); ++ } ++ } ++ ++ private void complete(final AbstractChatEvent event) { ++ if (event.isCancelled()) { ++ return; ++ } ++ ++ final CraftPlayer player = ((CraftPlayer) event.getPlayer()); ++ final Component displayName = displayName(player); ++ final Component message = event.message(); ++ final ChatRenderer renderer = event.renderer(); ++ ++ final Set<Audience> viewers = event.viewers(); ++ final ResourceKey<ChatType> chatTypeKey = renderer instanceof ChatRenderer.Default ? ChatType.CHAT : PAPER_RAW; ++ final ChatType.Bound chatType = ChatType.bind(chatTypeKey, this.player.level().registryAccess(), PaperAdventure.asVanilla(displayName(player))); ++ ++ OutgoingChat outgoingChat = viewers instanceof LazyChatAudienceSet lazyAudienceSet && lazyAudienceSet.isLazy() ? new ServerOutgoingChat() : new ViewersOutgoingChat(); ++ if (this.flags.get(FORMAT_CHANGED)) { ++ if (renderer instanceof ChatRenderer.ViewerUnaware unaware) { ++ outgoingChat.sendFormatChangedViewerUnaware(player, PaperAdventure.asVanilla(unaware.render(player, displayName, message)), viewers, chatType); ++ } else { ++ outgoingChat.sendFormatChangedViewerAware(player, displayName, message, renderer, viewers, chatType); ++ } ++ } else if (this.flags.get(MESSAGE_CHANGED)) { ++ if (!(renderer instanceof ChatRenderer.ViewerUnaware unaware)) { ++ throw new IllegalStateException("BUG: This should be a ViewerUnaware renderer at this point"); ++ } ++ final Component renderedComponent = chatTypeKey == ChatType.CHAT ? message : unaware.render(player, displayName, message); ++ outgoingChat.sendMessageChanged(player, PaperAdventure.asVanilla(renderedComponent), viewers, chatType); ++ } else { ++ outgoingChat.sendOriginal(player, viewers, chatType); ++ } ++ } ++ ++ interface OutgoingChat { ++ default void sendFormatChangedViewerUnaware(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) { ++ this.sendMessageChanged(player, renderedMessage, viewers, chatType); ++ } ++ ++ void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType); ++ ++ void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType); ++ ++ void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType); ++ } ++ ++ final class ServerOutgoingChat implements OutgoingChat { ++ @Override ++ public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) { ++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType, viewer -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, viewer))); ++ } ++ ++ @Override ++ public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) { ++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message.withUnsignedContent(renderedMessage), ChatProcessor.this.player, chatType); ++ } ++ ++ @Override ++ public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) { ++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType); ++ } ++ } ++ ++ final class ViewersOutgoingChat implements OutgoingChat { ++ @Override ++ public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) { ++ this.broadcastToViewers(viewers, chatType, v -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, v))); ++ } ++ ++ @Override ++ public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) { ++ this.broadcastToViewers(viewers, chatType, $ -> renderedMessage); ++ } ++ ++ @Override ++ public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) { ++ this.broadcastToViewers(viewers, chatType, null); ++ } ++ ++ private void broadcastToViewers(Collection<Audience> viewers, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) { ++ for (Audience viewer : viewers) { ++ if (acceptsNative(viewer)) { ++ this.sendNative(viewer, chatType, msgFunction); ++ } else { ++ final net.minecraft.network.chat.@Nullable Component unsigned = Optionull.map(msgFunction, f -> f.apply(viewer)); ++ final PlayerChatMessage msg = unsigned == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(unsigned); ++ viewer.sendMessage(msg.adventureView(), this.adventure(chatType)); ++ } ++ } ++ } ++ ++ private static final Map<String, net.kyori.adventure.chat.ChatType> BUILT_IN_CHAT_TYPES = Util.make(() -> { ++ final Map<String, net.kyori.adventure.chat.ChatType> map = new HashMap<>(); ++ for (final Field declaredField : net.kyori.adventure.chat.ChatType.class.getDeclaredFields()) { ++ if (Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().equals(ChatType.class)) { ++ try { ++ final net.kyori.adventure.chat.ChatType type = (net.kyori.adventure.chat.ChatType) declaredField.get(null); ++ map.put(type.key().asString(), type); ++ } catch (final ReflectiveOperationException ignore) { ++ } ++ } ++ } ++ return map; ++ }); ++ ++ private net.kyori.adventure.chat.ChatType.Bound adventure(ChatType.Bound chatType) { ++ @Subst("key:value") final String stringKey = Objects.requireNonNull( ++ chatType.chatType().unwrapKey().orElseThrow().location(), ++ () -> "No key for '%s' in CHAT_TYPE registry.".formatted(chatType) ++ ).toString(); ++ net.kyori.adventure.chat.@Nullable ChatType adventure = BUILT_IN_CHAT_TYPES.get(stringKey); ++ if (adventure == null) { ++ adventure = net.kyori.adventure.chat.ChatType.chatType(Key.key(stringKey)); ++ } ++ return adventure.bind( ++ PaperAdventure.asAdventure(chatType.name()), ++ chatType.targetName().map(PaperAdventure::asAdventure).orElse(null) ++ ); ++ } ++ ++ private static boolean acceptsNative(final Audience viewer) { ++ if (viewer instanceof Player || viewer instanceof ConsoleCommandSender) { ++ return true; ++ } ++ if (viewer instanceof ForwardingAudience.Single single) { ++ return acceptsNative(single.audience()); ++ } ++ return false; ++ } ++ ++ private void sendNative(final Audience viewer, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) { ++ if (viewer instanceof ConsoleCommandSender) { ++ this.sendToServer(chatType, msgFunction); ++ } else if (viewer instanceof CraftPlayer craftPlayer) { ++ craftPlayer.getHandle().sendChatMessage(ChatProcessor.this.outgoing, ChatProcessor.this.player.shouldFilterMessageTo(craftPlayer.getHandle()), chatType, Optionull.map(msgFunction, f -> f.apply(viewer))); ++ } else if (viewer instanceof ForwardingAudience.Single single) { ++ this.sendNative(single.audience(), chatType, msgFunction); ++ } else { ++ throw new IllegalStateException("Should only be a Player or Console or ForwardingAudience.Single pointing to one!"); ++ } ++ } ++ ++ private void sendToServer(final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) { ++ final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console)); ++ ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure"); ++ } ++ } ++ ++ private Set<Audience> viewersFromLegacy(final Set<Player> recipients) { ++ if (recipients instanceof LazyPlayerSet lazyPlayerSet && lazyPlayerSet.isLazy()) { ++ return new LazyChatAudienceSet(this.server); ++ } ++ final HashSet<Audience> viewers = new HashSet<>(recipients); ++ viewers.add(this.server.console); ++ return viewers; ++ } ++ ++ static String legacyDisplayName(final CraftPlayer player) { ++ return player.getDisplayName(); ++ } ++ ++ static Component displayName(final CraftPlayer player) { ++ return player.displayName(); ++ } ++ ++ private static ChatRenderer.Default defaultRenderer() { ++ return (ChatRenderer.Default) ChatRenderer.defaultRenderer(); ++ } ++ ++ private static ChatRenderer legacyRenderer(final String format) { ++ if (DEFAULT_LEGACY_FORMAT.equals(format)) { ++ return defaultRenderer(); ++ } ++ return ChatRenderer.viewerUnaware((player, sourceDisplayName, message) -> legacySection().deserialize(legacyFormat(format, player, legacySection().serialize(message)))); ++ } ++ ++ static String legacyFormat(final String format, Player player, String message) { ++ return String.format(format, legacyDisplayName((CraftPlayer) player), message); ++ } ++ ++ private void queueIfAsyncOrRunImmediately(final Waitable<Void> waitable) { ++ if (this.async) { ++ this.server.processQueue.add(waitable); ++ } else { ++ waitable.run(); ++ } ++ try { ++ waitable.get(); ++ } catch (final InterruptedException e) { ++ Thread.currentThread().interrupt(); // tag, you're it ++ } catch (final ExecutionException e) { ++ throw new RuntimeException("Exception processing chat", e.getCause()); ++ } ++ } ++ ++ private void post(final Event event) { ++ this.server.server.getPluginManager().callEvent(event); ++ } ++ ++ static boolean canYouHearMe(final HandlerList handlers) { ++ return handlers.getRegisteredListeners().length > 0; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/DisplayNames.java b/src/main/java/io/papermc/paper/adventure/DisplayNames.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d496a9a6ad229e42f1c44e31eafa6974b9faced5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/DisplayNames.java +@@ -0,0 +1,25 @@ ++package io.papermc.paper.adventure; ++ ++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.ChatColor; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++ ++public final class DisplayNames { ++ private DisplayNames() { ++ } ++ ++ public static String getLegacy(final CraftPlayer player) { ++ return getLegacy(player.getHandle()); ++ } ++ ++ @SuppressWarnings("deprecation") // Valid suppress due to supporting legacy display name formatting ++ public static String getLegacy(final ServerPlayer player) { ++ final String legacy = player.displayName; ++ if (legacy != null) { ++ // thank you for being worse than wet socks, Bukkit ++ return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName) + ChatColor.getLastColors(player.displayName); ++ } ++ return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java b/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0848ad9c7a6d386f0219b75df1ae4d08ba23aa59 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java +@@ -0,0 +1,55 @@ ++package io.papermc.paper.adventure; ++ ++import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent; ++import io.papermc.paper.event.player.AsyncChatDecorateEvent; ++import java.util.concurrent.CompletableFuture; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.network.chat.ChatDecorator; ++import net.minecraft.network.chat.Component; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class ImprovedChatDecorator implements ChatDecorator { ++ private final MinecraftServer server; ++ ++ public ImprovedChatDecorator(final MinecraftServer server) { ++ this.server = server; ++ } ++ ++ @Override ++ public CompletableFuture<Component> decorate(final @Nullable ServerPlayer sender, final Component message) { ++ return decorate(this.server, sender, null, message); ++ } ++ ++ @Override ++ public CompletableFuture<Component> decorate(final @Nullable ServerPlayer sender, final @Nullable CommandSourceStack commandSourceStack, final Component message) { ++ return decorate(this.server, sender, commandSourceStack, message); ++ } ++ ++ private static CompletableFuture<Component> decorate(final MinecraftServer server, final @Nullable ServerPlayer player, final @Nullable CommandSourceStack commandSourceStack, final Component originalMessage) { ++ return CompletableFuture.supplyAsync(() -> { ++ final net.kyori.adventure.text.Component initialResult = PaperAdventure.asAdventure(originalMessage); ++ ++ final @Nullable CraftPlayer craftPlayer = player == null ? null : player.getBukkitEntity(); ++ ++ final AsyncChatDecorateEvent event; ++ if (commandSourceStack != null) { ++ // TODO more command decorate context ++ event = new AsyncChatCommandDecorateEvent(craftPlayer, initialResult); ++ } else { ++ event = new AsyncChatDecorateEvent(craftPlayer, initialResult); ++ } ++ ++ if (event.callEvent()) { ++ return PaperAdventure.asVanilla(event.result()); ++ } ++ ++ return originalMessage; ++ }, server.chatExecutor); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2fd6c3e65354071af71c7d8ebb97b559b6e105ce +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.adventure; ++ ++import java.util.HashSet; ++import java.util.Set; ++import net.kyori.adventure.audience.Audience; ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.util.LazyHashSet; ++import org.bukkit.craftbukkit.util.LazyPlayerSet; ++import org.bukkit.entity.Player; ++ ++final class LazyChatAudienceSet extends LazyHashSet<Audience> { ++ private final MinecraftServer server; ++ ++ public LazyChatAudienceSet(final MinecraftServer server) { ++ this.server = server; ++ } ++ ++ @Override ++ protected Set<Audience> makeReference() { ++ final Set<Player> playerSet = LazyPlayerSet.makePlayerSet(this.server); ++ final HashSet<Audience> audiences = new HashSet<>(playerSet); ++ audiences.add(Bukkit.getConsoleSender()); ++ return audiences; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7397918cf747bc2352bf5bb112a71e7f6844e0e0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java +@@ -0,0 +1,478 @@ ++package io.papermc.paper.adventure; ++ ++import com.mojang.brigadier.StringReader; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.serialization.JavaOps; ++import io.netty.util.AttributeKey; ++import java.io.IOException; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.Optional; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.function.BiConsumer; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++import java.util.stream.StreamSupport; ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.inventory.Book; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.nbt.api.BinaryTagHolder; ++import net.kyori.adventure.sound.Sound; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.TranslatableComponent; ++import net.kyori.adventure.text.TranslationArgument; ++import net.kyori.adventure.text.event.DataComponentValue; ++import net.kyori.adventure.text.event.DataComponentValueConverterRegistry; ++import net.kyori.adventure.text.flattener.ComponentFlattener; ++import net.kyori.adventure.text.format.Style; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.serializer.ComponentSerializer; ++import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; ++import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import net.kyori.adventure.translation.GlobalTranslator; ++import net.kyori.adventure.translation.TranslationRegistry; ++import net.kyori.adventure.translation.Translator; ++import net.kyori.adventure.util.Codec; ++import net.minecraft.ChatFormatting; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.core.Holder; ++import net.minecraft.core.component.DataComponentPatch; ++import net.minecraft.core.component.DataComponentType; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.locale.Language; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.network.chat.ComponentUtils; ++import net.minecraft.network.protocol.Packet; ++import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket; ++import net.minecraft.network.protocol.game.ClientboundSoundPacket; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.network.Filterable; ++import net.minecraft.sounds.SoundEvent; ++import net.minecraft.sounds.SoundSource; ++import net.minecraft.world.BossEvent; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.component.WrittenBookContent; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.bukkit.craftbukkit.command.VanillaCommandWrapper; ++import org.bukkit.craftbukkit.entity.CraftEntity; ++import org.intellij.lang.annotations.Subst; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import static java.util.Objects.requireNonNull; ++ ++public final class PaperAdventure { ++ private static final Pattern LOCALIZATION_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?s"); ++ public static final ComponentFlattener FLATTENER = ComponentFlattener.basic().toBuilder() ++ .complexMapper(TranslatableComponent.class, (translatable, consumer) -> { ++ if (!Language.getInstance().has(translatable.key())) { ++ for (final Translator source : GlobalTranslator.translator().sources()) { ++ if (source instanceof TranslationRegistry registry && registry.contains(translatable.key())) { ++ consumer.accept(GlobalTranslator.render(translatable, Locale.US)); ++ return; ++ } ++ } ++ } ++ final @Nullable String fallback = translatable.fallback(); ++ final @NotNull String translated = Language.getInstance().getOrDefault(translatable.key(), fallback != null ? fallback : translatable.key()); ++ ++ final Matcher matcher = LOCALIZATION_PATTERN.matcher(translated); ++ final List<TranslationArgument> args = translatable.arguments(); ++ int argPosition = 0; ++ int lastIdx = 0; ++ while (matcher.find()) { ++ // append prior ++ if (lastIdx < matcher.start()) { ++ consumer.accept(Component.text(translated.substring(lastIdx, matcher.start()))); ++ } ++ lastIdx = matcher.end(); ++ ++ final @Nullable String argIdx = matcher.group(1); ++ // calculate argument position ++ if (argIdx != null) { ++ try { ++ final int idx = Integer.parseInt(argIdx) - 1; ++ if (idx < args.size()) { ++ consumer.accept(args.get(idx).asComponent()); ++ } ++ } catch (final NumberFormatException ex) { ++ // ignore, drop the format placeholder ++ } ++ } else { ++ final int idx = argPosition++; ++ if (idx < args.size()) { ++ consumer.accept(args.get(idx).asComponent()); ++ } ++ } ++ } ++ ++ // append tail ++ if (lastIdx < translated.length()) { ++ consumer.accept(Component.text(translated.substring(lastIdx))); ++ } ++ }) ++ .build(); ++ public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger ++ @Deprecated ++ public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build(); ++ public static final Codec<Tag, String, CommandSyntaxException, RuntimeException> NBT_CODEC = new Codec<>() { ++ @Override ++ public @NotNull Tag decode(final @NotNull String encoded) throws CommandSyntaxException { ++ return new TagParser(new StringReader(encoded)).readValue(); ++ } ++ ++ @Override ++ public @NotNull String encode(final @NotNull Tag decoded) { ++ return decoded.toString(); ++ } ++ }; ++ public static final ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer(); ++ ++ private PaperAdventure() { ++ } ++ ++ // Key ++ ++ public static ResourceLocation asVanilla(final Key key) { ++ return new ResourceLocation(key.namespace(), key.value()); ++ } ++ ++ public static ResourceLocation asVanillaNullable(final Key key) { ++ if (key == null) { ++ return null; ++ } ++ return asVanilla(key); ++ } ++ ++ // Component ++ ++ public static @NotNull Component asAdventure(@Nullable final net.minecraft.network.chat.Component component) { ++ return component == null ? Component.empty() : WRAPPER_AWARE_SERIALIZER.deserialize(component); ++ } ++ ++ public static ArrayList<Component> asAdventure(final List<? extends net.minecraft.network.chat.Component> vanillas) { ++ final ArrayList<Component> adventures = new ArrayList<>(vanillas.size()); ++ for (final net.minecraft.network.chat.Component vanilla : vanillas) { ++ adventures.add(asAdventure(vanilla)); ++ } ++ return adventures; ++ } ++ ++ public static ArrayList<Component> asAdventureFromJson(final List<String> jsonStrings) { ++ final ArrayList<Component> adventures = new ArrayList<>(jsonStrings.size()); ++ for (final String json : jsonStrings) { ++ adventures.add(GsonComponentSerializer.gson().deserialize(json)); ++ } ++ return adventures; ++ } ++ ++ public static List<String> asJson(final List<? extends Component> adventures) { ++ final List<String> jsons = new ArrayList<>(adventures.size()); ++ for (final Component component : adventures) { ++ jsons.add(GsonComponentSerializer.gson().serialize(component)); ++ } ++ return jsons; ++ } ++ ++ public static net.minecraft.network.chat.@NotNull Component asVanillaNullToEmpty(final @Nullable Component component) { ++ if (component == null) return net.minecraft.network.chat.CommonComponents.EMPTY; ++ return asVanilla(component); ++ } ++ ++ @Contract("null -> null; !null -> !null") ++ public static net.minecraft.network.chat.Component asVanilla(final @Nullable Component component) { ++ if (component == null) return null; ++ if (true) return new AdventureComponent(component); ++ return WRAPPER_AWARE_SERIALIZER.serialize(component); ++ } ++ ++ public static List<net.minecraft.network.chat.Component> asVanilla(final List<? extends Component> adventures) { ++ final List<net.minecraft.network.chat.Component> vanillas = new ArrayList<>(adventures.size()); ++ for (final Component adventure : adventures) { ++ vanillas.add(asVanilla(adventure)); ++ } ++ return vanillas; ++ } ++ ++ public static String asJsonString(final Component component, final Locale locale) { ++ return GsonComponentSerializer.gson().serialize(translated(component, locale)); ++ } ++ ++ public static boolean hasAnyTranslations() { ++ return StreamSupport.stream(GlobalTranslator.translator().sources().spliterator(), false) ++ .anyMatch(t -> t.hasAnyTranslations().toBooleanOrElse(true)); ++ } ++ ++ private static final Map<Locale, com.mojang.serialization.Codec<Component>> LOCALIZED_CODECS = new ConcurrentHashMap<>(); ++ ++ public static com.mojang.serialization.Codec<Component> localizedCodec(final @Nullable Locale l) { ++ if (l == null) { ++ return AdventureCodecs.COMPONENT_CODEC; ++ } ++ return LOCALIZED_CODECS.computeIfAbsent(l, locale -> AdventureCodecs.COMPONENT_CODEC.xmap( ++ component -> component, // decode ++ component -> translated(component, locale) // encode ++ )); ++ } ++ ++ public static String asPlain(final Component component, final Locale locale) { ++ return PlainTextComponentSerializer.plainText().serialize(translated(component, locale)); ++ } ++ ++ private static Component translated(final Component component, final Locale locale) { ++ //noinspection ConstantValue ++ return GlobalTranslator.render( ++ component, ++ // play it safe ++ locale != null ++ ? locale ++ : Locale.US ++ ); ++ } ++ ++ public static Component resolveWithContext(final @NotNull Component component, final @Nullable CommandSender context, final @Nullable org.bukkit.entity.Entity scoreboardSubject, final boolean bypassPermissions) throws IOException { ++ final CommandSourceStack css = context != null ? VanillaCommandWrapper.getListener(context) : null; ++ Boolean previous = null; ++ if (css != null && bypassPermissions) { ++ previous = css.bypassSelectorPermissions; ++ css.bypassSelectorPermissions = true; ++ } ++ try { ++ return asAdventure(ComponentUtils.updateForEntity(css, asVanilla(component), scoreboardSubject == null ? null : ((CraftEntity) scoreboardSubject).getHandle(), 0)); ++ } catch (final CommandSyntaxException e) { ++ throw new IOException(e); ++ } finally { ++ if (css != null && previous != null) { ++ css.bypassSelectorPermissions = previous; ++ } ++ } ++ } ++ ++ // BossBar ++ ++ public static BossEvent.BossBarColor asVanilla(final BossBar.Color color) { ++ return switch (color) { ++ case PINK -> BossEvent.BossBarColor.PINK; ++ case BLUE -> BossEvent.BossBarColor.BLUE; ++ case RED -> BossEvent.BossBarColor.RED; ++ case GREEN -> BossEvent.BossBarColor.GREEN; ++ case YELLOW -> BossEvent.BossBarColor.YELLOW; ++ case PURPLE -> BossEvent.BossBarColor.PURPLE; ++ case WHITE -> BossEvent.BossBarColor.WHITE; ++ }; ++ } ++ ++ public static BossBar.Color asAdventure(final BossEvent.BossBarColor color) { ++ return switch (color) { ++ case PINK -> BossBar.Color.PINK; ++ case BLUE -> BossBar.Color.BLUE; ++ case RED -> BossBar.Color.RED; ++ case GREEN -> BossBar.Color.GREEN; ++ case YELLOW -> BossBar.Color.YELLOW; ++ case PURPLE -> BossBar.Color.PURPLE; ++ case WHITE -> BossBar.Color.WHITE; ++ }; ++ } ++ ++ public static BossEvent.BossBarOverlay asVanilla(final BossBar.Overlay overlay) { ++ return switch (overlay) { ++ case PROGRESS -> BossEvent.BossBarOverlay.PROGRESS; ++ case NOTCHED_6 -> BossEvent.BossBarOverlay.NOTCHED_6; ++ case NOTCHED_10 -> BossEvent.BossBarOverlay.NOTCHED_10; ++ case NOTCHED_12 -> BossEvent.BossBarOverlay.NOTCHED_12; ++ case NOTCHED_20 -> BossEvent.BossBarOverlay.NOTCHED_20; ++ }; ++ } ++ ++ public static BossBar.Overlay asAdventure(final BossEvent.BossBarOverlay overlay) { ++ return switch (overlay) { ++ case PROGRESS -> BossBar.Overlay.PROGRESS; ++ case NOTCHED_6 -> BossBar.Overlay.NOTCHED_6; ++ case NOTCHED_10 -> BossBar.Overlay.NOTCHED_10; ++ case NOTCHED_12 -> BossBar.Overlay.NOTCHED_12; ++ case NOTCHED_20 -> BossBar.Overlay.NOTCHED_20; ++ }; ++ } ++ ++ public static void setFlag(final BossBar bar, final BossBar.Flag flag, final boolean value) { ++ if (value) { ++ bar.addFlag(flag); ++ } else { ++ bar.removeFlag(flag); ++ } ++ } ++ ++ // Book ++ ++ public static ItemStack asItemStack(final Book book, final Locale locale) { ++ final ItemStack item = new ItemStack(net.minecraft.world.item.Items.WRITTEN_BOOK, 1); ++ item.set(DataComponents.WRITTEN_BOOK_CONTENT, new WrittenBookContent( ++ Filterable.passThrough(validateField(asPlain(book.title(), locale), WrittenBookContent.TITLE_MAX_LENGTH, "title")), ++ asPlain(book.author(), locale), ++ 0, ++ book.pages().stream().map(c -> Filterable.passThrough(PaperAdventure.asVanilla(c))).toList(), // TODO should we validate legnth? ++ false ++ )); ++ return item; ++ } ++ ++ private static String validateField(final String content, final int length, final String name) { ++ final int actual = content.length(); ++ if (actual > length) { ++ throw new IllegalArgumentException("Field '" + name + "' has a maximum length of " + length + " but was passed '" + content + "', which was " + actual + " characters long."); ++ } ++ return content; ++ } ++ ++ // Sounds ++ ++ public static SoundSource asVanilla(final Sound.Source source) { ++ return switch (source) { ++ case MASTER -> SoundSource.MASTER; ++ case MUSIC -> SoundSource.MUSIC; ++ case RECORD -> SoundSource.RECORDS; ++ case WEATHER -> SoundSource.WEATHER; ++ case BLOCK -> SoundSource.BLOCKS; ++ case HOSTILE -> SoundSource.HOSTILE; ++ case NEUTRAL -> SoundSource.NEUTRAL; ++ case PLAYER -> SoundSource.PLAYERS; ++ case AMBIENT -> SoundSource.AMBIENT; ++ case VOICE -> SoundSource.VOICE; ++ }; ++ } ++ ++ public static @Nullable SoundSource asVanillaNullable(final Sound.@Nullable Source source) { ++ if (source == null) { ++ return null; ++ } ++ return asVanilla(source); ++ } ++ ++ public static Packet<?> asSoundPacket(final Sound sound, final double x, final double y, final double z, final long seed, @Nullable BiConsumer<Packet<?>, Float> packetConsumer) { ++ final ResourceLocation name = asVanilla(sound.name()); ++ final Optional<SoundEvent> soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(name); ++ final SoundSource source = asVanilla(sound.source()); ++ ++ final Holder<SoundEvent> soundEventHolder = soundEvent.map(BuiltInRegistries.SOUND_EVENT::wrapAsHolder).orElseGet(() -> Holder.direct(SoundEvent.createVariableRangeEvent(name))); ++ final Packet<?> packet = new ClientboundSoundPacket(soundEventHolder, source, x, y, z, sound.volume(), sound.pitch(), seed); ++ if (packetConsumer != null) { ++ packetConsumer.accept(packet, soundEventHolder.value().getRange(sound.volume())); ++ } ++ return packet; ++ } ++ ++ public static Packet<?> asSoundPacket(final Sound sound, final Entity emitter, final long seed, @Nullable BiConsumer<Packet<?>, Float> packetConsumer) { ++ final ResourceLocation name = asVanilla(sound.name()); ++ final Optional<SoundEvent> soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(name); ++ final SoundSource source = asVanilla(sound.source()); ++ ++ final Holder<SoundEvent> soundEventHolder = soundEvent.map(BuiltInRegistries.SOUND_EVENT::wrapAsHolder).orElseGet(() -> Holder.direct(SoundEvent.createVariableRangeEvent(name))); ++ final Packet<?> packet = new ClientboundSoundEntityPacket(soundEventHolder, source, emitter, sound.volume(), sound.pitch(), seed); ++ if (packetConsumer != null) { ++ packetConsumer.accept(packet, soundEventHolder.value().getRange(sound.volume())); ++ } ++ return packet; ++ } ++ ++ // NBT ++ ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ public static Map<Key, ? extends DataComponentValue> asAdventure( ++ final DataComponentPatch patch ++ ) { ++ if (patch.isEmpty()) { ++ return Collections.emptyMap(); ++ } ++ final Map<Key, DataComponentValue> map = new HashMap<>(); ++ for (final Map.Entry<DataComponentType<?>, Optional<?>> entry : patch.entrySet()) { ++ if (entry.getKey().isTransient()) continue; ++ @Subst("key:value") final String typeKey = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(entry.getKey())).toString(); ++ if (entry.getValue().isEmpty()) { ++ map.put(Key.key(typeKey), DataComponentValue.removed()); ++ } else { ++ map.put(Key.key(typeKey), new DataComponentValueImpl(entry.getKey().codec(), entry.getValue().get())); ++ } ++ } ++ return map; ++ } ++ ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ public static DataComponentPatch asVanilla(final Map<? extends Key, ? extends DataComponentValue> map) { ++ if (map.isEmpty()) { ++ return DataComponentPatch.EMPTY; ++ } ++ final DataComponentPatch.Builder builder = DataComponentPatch.builder(); ++ map.forEach((key, dataComponentValue) -> { ++ final DataComponentType<?> type = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.get(asVanilla(key))); ++ if (dataComponentValue instanceof DataComponentValue.Removed) { ++ builder.remove(type); ++ return; ++ } ++ final DataComponentValueImpl<?> converted = DataComponentValueConverterRegistry.convert(DataComponentValueImpl.class, key, dataComponentValue); ++ builder.set((DataComponentType) type, (Object) converted.value()); ++ }); ++ return builder.build(); ++ } ++ ++ public record DataComponentValueImpl<T>(com.mojang.serialization.Codec<T> codec, T value) implements DataComponentValue.TagSerializable { ++ ++ @Override ++ public @NotNull BinaryTagHolder asBinaryTag() { ++ return BinaryTagHolder.encode(this.codec.encodeStart(CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE), this.value).getOrThrow(IllegalArgumentException::new), NBT_CODEC); ++ } ++ } ++ ++ public static @Nullable BinaryTagHolder asBinaryTagHolder(final @Nullable CompoundTag tag) { ++ if (tag == null) { ++ return null; ++ } ++ return BinaryTagHolder.encode(tag, NBT_CODEC); ++ } ++ ++ // Colors ++ ++ public static @NotNull TextColor asAdventure(final ChatFormatting formatting) { ++ final Integer color = formatting.getColor(); ++ if (color == null) { ++ throw new IllegalArgumentException("Not a valid color"); ++ } ++ return TextColor.color(color); ++ } ++ ++ public static @Nullable ChatFormatting asVanilla(final TextColor color) { ++ return ChatFormatting.getByHexValue(color.value()); ++ } ++ ++ // Style ++ ++ public static net.minecraft.network.chat.Style asVanilla(final Style style) { ++ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE); ++ final Object encoded = AdventureCodecs.STYLE_MAP_CODEC.codec() ++ .encodeStart(ops, style).getOrThrow(IllegalStateException::new); ++ ++ return net.minecraft.network.chat.Style.Serializer.CODEC ++ .parse(ops, encoded).getOrThrow(IllegalStateException::new); ++ } ++ ++ public static Style asAdventure(final net.minecraft.network.chat.Style style) { ++ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE); ++ final Object encoded = net.minecraft.network.chat.Style.Serializer.CODEC ++ .encodeStart(ops, style).getOrThrow(IllegalStateException::new); ++ ++ return AdventureCodecs.STYLE_MAP_CODEC.codec() ++ .parse(ops, encoded).getOrThrow(IllegalStateException::new); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java b/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c786ddf0ef19757011452204fd11d24541c39d9e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.adventure; ++ ++import com.mojang.datafixers.util.Pair; ++import com.mojang.serialization.JavaOps; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.serializer.ComponentSerializer; ++import net.minecraft.network.chat.ComponentSerialization; ++import net.minecraft.resources.RegistryOps; ++import org.bukkit.craftbukkit.CraftRegistry; ++ ++final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> { ++ @Override ++ public Component deserialize(final net.minecraft.network.chat.Component input) { ++ if (input instanceof AdventureComponent) { ++ return ((AdventureComponent) input).adventure; ++ } ++ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE); ++ final Object obj = ComponentSerialization.CODEC.encodeStart(ops, input) ++ .getOrThrow(s -> new RuntimeException("Failed to encode Minecraft Component: " + input + "; " + s)); ++ final Pair<Component, Object> converted = AdventureCodecs.COMPONENT_CODEC.decode(ops, obj) ++ .getOrThrow(s -> new RuntimeException("Failed to decode to adventure Component: " + obj + "; " + s)); ++ return converted.getFirst(); ++ } ++ ++ @Override ++ public net.minecraft.network.chat.Component serialize(final Component component) { ++ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE); ++ final Object obj = AdventureCodecs.COMPONENT_CODEC.encodeStart(ops, component) ++ .getOrThrow(s -> new RuntimeException("Failed to encode adventure Component: " + component + "; " + s)); ++ final Pair<net.minecraft.network.chat.Component, Object> converted = ComponentSerialization.CODEC.decode(ops, obj) ++ .getOrThrow(s -> new RuntimeException("Failed to decode to Minecraft Component: " + obj + "; " + s)); ++ return converted.getFirst(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java b/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2ee72fe7cb56e70404b8c86f0c9578750a45af03 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java +@@ -0,0 +1,14 @@ ++package io.papermc.paper.adventure.providers; ++ ++import io.papermc.paper.adventure.BossBarImplementationImpl; ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.bossbar.BossBarImplementation; ++import org.jetbrains.annotations.NotNull; ++ ++@SuppressWarnings("UnstableApiUsage") // permitted provider ++public class BossBarImplementationProvider implements BossBarImplementation.Provider { ++ @Override ++ public @NotNull BossBarImplementation create(final @NotNull BossBar bar) { ++ return new BossBarImplementationImpl(bar); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..23432eea862c6df716d7726a32da3a0612a3fb77 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java +@@ -0,0 +1,96 @@ ++package io.papermc.paper.adventure.providers; ++ ++import java.util.HashMap; ++import java.util.Map; ++import java.util.UUID; ++import net.kyori.adventure.audience.Audience; ++import net.kyori.adventure.text.event.ClickCallback; ++import net.kyori.adventure.text.event.ClickEvent; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.Queue; ++import java.util.concurrent.ConcurrentLinkedQueue; ++ ++@SuppressWarnings("UnstableApiUsage") // permitted provider ++public class ClickCallbackProviderImpl implements ClickCallback.Provider { ++ ++ public static final CallbackManager CALLBACK_MANAGER = new CallbackManager(); ++ ++ @Override ++ public @NotNull ClickEvent create(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) { ++ return ClickEvent.runCommand("/paper:callback " + CALLBACK_MANAGER.addCallback(callback, options)); ++ } ++ ++ public static final class CallbackManager { ++ ++ private final Map<UUID, StoredCallback> callbacks = new HashMap<>(); ++ private final Queue<StoredCallback> queue = new ConcurrentLinkedQueue<>(); ++ ++ private CallbackManager() { ++ } ++ ++ public UUID addCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) { ++ final UUID id = UUID.randomUUID(); ++ this.queue.add(new StoredCallback(callback, options, id)); ++ return id; ++ } ++ ++ public void handleQueue(final int currentTick) { ++ // Evict expired entries ++ if (currentTick % 100 == 0) { ++ this.callbacks.values().removeIf(callback -> !callback.valid()); ++ } ++ ++ // Add entries from queue ++ StoredCallback callback; ++ while ((callback = this.queue.poll()) != null) { ++ this.callbacks.put(callback.id(), callback); ++ } ++ } ++ ++ public void runCallback(final @NotNull Audience audience, final UUID id) { ++ final StoredCallback callback = this.callbacks.get(id); ++ if (callback != null && callback.valid()) { //TODO Message if expired/invalid? ++ callback.takeUse(); ++ callback.callback.accept(audience); ++ } ++ } ++ } ++ ++ private static final class StoredCallback { ++ private final long startedAt = System.nanoTime(); ++ private final ClickCallback<Audience> callback; ++ private final long lifetime; ++ private final UUID id; ++ private int remainingUses; ++ ++ private StoredCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options, final UUID id) { ++ this.callback = callback; ++ this.lifetime = options.lifetime().toNanos(); ++ this.remainingUses = options.uses(); ++ this.id = id; ++ } ++ ++ public void takeUse() { ++ if (this.remainingUses != ClickCallback.UNLIMITED_USES) { ++ this.remainingUses--; ++ } ++ } ++ ++ public boolean hasRemainingUses() { ++ return this.remainingUses == ClickCallback.UNLIMITED_USES || this.remainingUses > 0; ++ } ++ ++ public boolean expired() { ++ return System.nanoTime() - this.startedAt >= this.lifetime; ++ } ++ ++ public boolean valid() { ++ return hasRemainingUses() && !expired(); ++ } ++ ++ public UUID id() { ++ return this.id; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8323f135d6bf2e1f12525e05094ffa3f2420e7e1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.adventure.providers; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider; ++import org.jetbrains.annotations.NotNull; ++import org.slf4j.LoggerFactory; ++ ++@SuppressWarnings("UnstableApiUsage") ++public class ComponentLoggerProviderImpl implements ComponentLoggerProvider { ++ @Override ++ public @NotNull ComponentLogger logger(@NotNull LoggerHelper helper, @NotNull String name) { ++ return helper.delegating(LoggerFactory.getLogger(name), this::serialize); ++ } ++ ++ private String serialize(final Component message) { ++ return PaperAdventure.asPlain(message, null); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/DataComponentValueConverterProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/DataComponentValueConverterProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6b37c0ebda4a0cfcf9c3b2c3483ffababe622555 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/DataComponentValueConverterProviderImpl.java +@@ -0,0 +1,82 @@ ++package io.papermc.paper.adventure.providers; ++ ++import com.google.gson.JsonElement; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.serialization.DynamicOps; ++import com.mojang.serialization.JsonOps; ++import io.papermc.paper.adventure.PaperAdventure; ++import java.util.List; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.nbt.api.BinaryTagHolder; ++import net.kyori.adventure.text.event.DataComponentValue; ++import net.kyori.adventure.text.event.DataComponentValueConverterRegistry; ++import net.kyori.adventure.text.serializer.gson.GsonDataComponentValue; ++import net.minecraft.core.component.DataComponentType; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.resources.RegistryOps; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import static net.kyori.adventure.text.serializer.gson.GsonDataComponentValue.gsonDataComponentValue; ++ ++@DefaultQualifier(NonNull.class) ++public class DataComponentValueConverterProviderImpl implements DataComponentValueConverterRegistry.Provider { ++ ++ static final Key ID = Key.key("adventure", "platform/paper"); ++ ++ @Override ++ public Key id() { ++ return ID; ++ } ++ ++ private static <T> RegistryOps<T> createOps(final DynamicOps<T> delegate) { ++ return CraftRegistry.getMinecraftRegistry().createSerializationContext(delegate); ++ } ++ ++ @SuppressWarnings({"unchecked", "rawtypes"}) ++ @Override ++ public Iterable<DataComponentValueConverterRegistry.Conversion<?, ?>> conversions() { ++ return List.of( ++ DataComponentValueConverterRegistry.Conversion.convert( ++ PaperAdventure.DataComponentValueImpl.class, ++ GsonDataComponentValue.class, ++ (key, dataComponentValue) -> gsonDataComponentValue((JsonElement) dataComponentValue.codec().encodeStart(createOps(JsonOps.INSTANCE), dataComponentValue.value()).getOrThrow()) ++ ), ++ DataComponentValueConverterRegistry.Conversion.convert( ++ GsonDataComponentValue.class, ++ PaperAdventure.DataComponentValueImpl.class, ++ (key, dataComponentValue) -> { ++ final @Nullable DataComponentType<?> type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(PaperAdventure.asVanilla(key)); ++ if (type == null) { ++ throw new IllegalArgumentException("Unknown data component type: " + key); ++ } ++ return new PaperAdventure.DataComponentValueImpl(type.codecOrThrow(), type.codecOrThrow().parse(createOps(JsonOps.INSTANCE), dataComponentValue.element()).getOrThrow(IllegalArgumentException::new)); ++ } ++ ), ++ DataComponentValueConverterRegistry.Conversion.convert( ++ PaperAdventure.DataComponentValueImpl.class, ++ DataComponentValue.TagSerializable.class, ++ (key, dataComponentValue) -> BinaryTagHolder.encode((Tag) dataComponentValue.codec().encodeStart(createOps(NbtOps.INSTANCE), dataComponentValue.value()).getOrThrow(), PaperAdventure.NBT_CODEC) ++ ), ++ DataComponentValueConverterRegistry.Conversion.convert( ++ DataComponentValue.TagSerializable.class, ++ PaperAdventure.DataComponentValueImpl.class, ++ (key, tagSerializable) -> { ++ final @Nullable DataComponentType<?> type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(PaperAdventure.asVanilla(key)); ++ if (type == null) { ++ throw new IllegalArgumentException("Unknown data component type: " + key); ++ } ++ try { ++ return new PaperAdventure.DataComponentValueImpl(type.codecOrThrow(), type.codecOrThrow().parse(createOps(NbtOps.INSTANCE), tagSerializable.asBinaryTag().get(PaperAdventure.NBT_CODEC)).getOrThrow(IllegalArgumentException::new)); ++ } catch (final CommandSyntaxException e) { ++ throw new IllegalArgumentException(e); ++ } ++ } ++ ) ++ ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/GsonComponentSerializerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/GsonComponentSerializerProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c620d5aa2b0208b769dbe9563f0e99edc9a91047 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/GsonComponentSerializerProviderImpl.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.adventure.providers; ++ ++import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.function.Consumer; ++ ++@SuppressWarnings("UnstableApiUsage") // permitted provider ++public class GsonComponentSerializerProviderImpl implements GsonComponentSerializer.Provider { ++ ++ @Override ++ public @NotNull GsonComponentSerializer gson() { ++ return GsonComponentSerializer.builder() ++ .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE) ++ .build(); ++ } ++ ++ @Override ++ public @NotNull GsonComponentSerializer gsonLegacy() { ++ return GsonComponentSerializer.builder() ++ .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE) ++ .downsampleColors() ++ .build(); ++ } ++ ++ @Override ++ public @NotNull Consumer<GsonComponentSerializer.Builder> builder() { ++ return builder -> builder.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/LegacyComponentSerializerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/LegacyComponentSerializerProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..03723dbe32b7eb95253e8ff6e72dbf8d2300a059 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/LegacyComponentSerializerProviderImpl.java +@@ -0,0 +1,36 @@ ++package io.papermc.paper.adventure.providers; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.function.Consumer; ++ ++@SuppressWarnings("UnstableApiUsage") // permitted provider ++public class LegacyComponentSerializerProviderImpl implements LegacyComponentSerializer.Provider { ++ ++ @Override ++ public @NotNull LegacyComponentSerializer legacyAmpersand() { ++ return LegacyComponentSerializer.builder() ++ .flattener(PaperAdventure.FLATTENER) ++ .character(LegacyComponentSerializer.AMPERSAND_CHAR) ++ .hexColors() ++ .useUnusualXRepeatedCharacterHexFormat() ++ .build(); ++ } ++ ++ @Override ++ public @NotNull LegacyComponentSerializer legacySection() { ++ return LegacyComponentSerializer.builder() ++ .flattener(PaperAdventure.FLATTENER) ++ .character(LegacyComponentSerializer.SECTION_CHAR) ++ .hexColors() ++ .useUnusualXRepeatedCharacterHexFormat() ++ .build(); ++ } ++ ++ @Override ++ public @NotNull Consumer<LegacyComponentSerializer.Builder> legacy() { ++ return builder -> builder.flattener(PaperAdventure.FLATTENER); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/MiniMessageProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/MiniMessageProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..25fd6992c869c841b1b1b3240f4d524948487614 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/MiniMessageProviderImpl.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.adventure.providers; ++ ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.function.Consumer; ++ ++@SuppressWarnings("UnstableApiUsage") // permitted provider ++public class MiniMessageProviderImpl implements MiniMessage.Provider { ++ ++ @Override ++ public @NotNull MiniMessage miniMessage() { ++ return MiniMessage.builder().build(); ++ } ++ ++ @Override ++ public @NotNull Consumer<MiniMessage.Builder> builder() { ++ return builder -> {}; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/NBTLegacyHoverEventSerializer.java b/src/main/java/io/papermc/paper/adventure/providers/NBTLegacyHoverEventSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..202b964e7e67717904cd3f00b6af6ad7f2a5c90e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/NBTLegacyHoverEventSerializer.java +@@ -0,0 +1,91 @@ ++package io.papermc.paper.adventure.providers; ++ ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import java.io.IOException; ++import java.util.UUID; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.nbt.api.BinaryTagHolder; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import net.kyori.adventure.util.Codec; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.Tag; ++import net.minecraft.nbt.TagParser; ++import org.intellij.lang.annotations.Subst; ++ ++final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer { ++ public static final NBTLegacyHoverEventSerializer INSTANCE = new NBTLegacyHoverEventSerializer(); ++ private static final Codec<CompoundTag, String, CommandSyntaxException, RuntimeException> SNBT_CODEC = Codec.codec(TagParser::parseTag, Tag::toString); ++ ++ static final String ITEM_TYPE = "id"; ++ static final String ITEM_COUNT = "Count"; ++ static final String ITEM_TAG = "tag"; ++ ++ static final String ENTITY_NAME = "name"; ++ static final String ENTITY_TYPE = "type"; ++ static final String ENTITY_ID = "id"; ++ ++ NBTLegacyHoverEventSerializer() { ++ } ++ ++ @Override ++ public HoverEvent.ShowItem deserializeShowItem(final Component input) throws IOException { ++ final String raw = PlainTextComponentSerializer.plainText().serialize(input); ++ try { ++ final CompoundTag contents = SNBT_CODEC.decode(raw); ++ final CompoundTag tag = contents.getCompound(ITEM_TAG); ++ @Subst("key") final String keyString = contents.getString(ITEM_TYPE); ++ return HoverEvent.ShowItem.showItem( ++ Key.key(keyString), ++ contents.contains(ITEM_COUNT) ? contents.getByte(ITEM_COUNT) : 1, ++ tag.isEmpty() ? null : BinaryTagHolder.encode(tag, SNBT_CODEC) ++ ); ++ } catch (final CommandSyntaxException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ @Override ++ public HoverEvent.ShowEntity deserializeShowEntity(final Component input, final Codec.Decoder<Component, String, ? extends RuntimeException> componentCodec) throws IOException { ++ final String raw = PlainTextComponentSerializer.plainText().serialize(input); ++ try { ++ final CompoundTag contents = SNBT_CODEC.decode(raw); ++ @Subst("key") final String keyString = contents.getString(ENTITY_TYPE); ++ return HoverEvent.ShowEntity.showEntity( ++ Key.key(keyString), ++ UUID.fromString(contents.getString(ENTITY_ID)), ++ componentCodec.decode(contents.getString(ENTITY_NAME)) ++ ); ++ } catch (final CommandSyntaxException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ @Override ++ public Component serializeShowItem(final HoverEvent.ShowItem input) throws IOException { ++ final CompoundTag tag = new CompoundTag(); ++ tag.putString(ITEM_TYPE, input.item().asString()); ++ tag.putByte(ITEM_COUNT, (byte) input.count()); ++ if (input.nbt() != null) { ++ try { ++ tag.put(ITEM_TAG, input.nbt().get(SNBT_CODEC)); ++ } catch (final CommandSyntaxException ex) { ++ throw new IOException(ex); ++ } ++ } ++ return Component.text(SNBT_CODEC.encode(tag)); ++ } ++ ++ @Override ++ public Component serializeShowEntity(final HoverEvent.ShowEntity input, final Codec.Encoder<Component, String, ? extends RuntimeException> componentCodec) { ++ final CompoundTag tag = new CompoundTag(); ++ tag.putString(ENTITY_ID, input.id().toString()); ++ tag.putString(ENTITY_TYPE, input.type().asString()); ++ if (input.name() != null) { ++ tag.putString(ENTITY_NAME, componentCodec.encode(input.name())); ++ } ++ return Component.text(SNBT_CODEC.encode(tag)); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/adventure/providers/PlainTextComponentSerializerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/PlainTextComponentSerializerProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0701d4f93a4d77a8177d2dd8d5076f9f781873d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/providers/PlainTextComponentSerializerProviderImpl.java +@@ -0,0 +1,23 @@ ++package io.papermc.paper.adventure.providers; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.function.Consumer; ++ ++@SuppressWarnings("UnstableApiUsage") // permitted provider ++public class PlainTextComponentSerializerProviderImpl implements PlainTextComponentSerializer.Provider { ++ ++ @Override ++ public @NotNull PlainTextComponentSerializer plainTextSimple() { ++ return PlainTextComponentSerializer.builder() ++ .flattener(PaperAdventure.FLATTENER) ++ .build(); ++ } ++ ++ @Override ++ public @NotNull Consumer<PlainTextComponentSerializer.Builder> plainText() { ++ return builder -> builder.flattener(PaperAdventure.FLATTENER); ++ } ++} +diff --git a/src/main/java/net/minecraft/ChatFormatting.java b/src/main/java/net/minecraft/ChatFormatting.java +index 08dcd817bfe1ba0121d4ce701825e4aee384db85..d5f63d06d921d731b4e64b38228377712fe9c75b 100644 +--- a/src/main/java/net/minecraft/ChatFormatting.java ++++ b/src/main/java/net/minecraft/ChatFormatting.java +@@ -112,6 +112,18 @@ public enum ChatFormatting implements StringRepresentable { + return name == null ? null : FORMATTING_BY_NAME.get(cleanName(name)); + } + ++ // Paper start - add method to get by hex value ++ @Nullable public static ChatFormatting getByHexValue(int i) { ++ for (ChatFormatting value : values()) { ++ if (value.getColor() != null && value.getColor() == i) { ++ return value; ++ } ++ } ++ ++ return null; ++ } ++ // Paper end - add method to get by hex value ++ + @Nullable + public static ChatFormatting getById(int colorIndex) { + if (colorIndex < 0) { +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index f30cdfa5fd294479e35680b2f758b3295f659b74..ec34e402104d7a696ea95e0b11ee70189b678ab9 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -65,6 +65,7 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS + private final CommandSigningContext signingContext; + private final TaskChainer chatMessageChainer; + public volatile CommandNode currentCommand; // CraftBukkit ++ public boolean bypassSelectorPermissions = false; // Paper - add bypass for selector permissions + + public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) { + this(output, pos, rot, world, level, name, displayName, server, entity, false, CommandResultCallback.EMPTY, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server)); +diff --git a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java +index 0047fd758ad43cc7b13355b60c410a9bcc37bec0..982b2bab27e3d55d0ba07060862c0c3183ad91b0 100644 +--- a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java ++++ b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java +@@ -53,17 +53,21 @@ public class MessageArgument implements SignedArgument<MessageArgument.Message> + private static void resolveSignedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) { + MinecraftServer minecraftServer = source.getServer(); + CompletableFuture<FilteredText> completableFuture = filterPlainText(source, message); +- Component component = minecraftServer.getChatDecorator().decorate(source.getPlayer(), message.decoratedContent()); +- source.getChatMessageChainer().append(completableFuture, filtered -> { +- PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(component).filter(filtered.mask()); ++ // Paper start - support asynchronous chat decoration ++ CompletableFuture<Component> componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent()); ++ source.getChatMessageChainer().append(CompletableFuture.allOf(completableFuture, componentFuture), filtered -> { ++ PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask()); ++ // Paper end - support asynchronous chat decoration + callback.accept(playerChatMessage2); + }); + } + + private static void resolveDisguisedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) { + ChatDecorator chatDecorator = source.getServer().getChatDecorator(); +- Component component = chatDecorator.decorate(source.getPlayer(), message.decoratedContent()); +- callback.accept(message.withUnsignedContent(component)); ++ // Paper start - support asynchronous chat decoration ++ CompletableFuture<Component> componentFuture = chatDecorator.decorate(source.getPlayer(), source, message.decoratedContent()); ++ source.getChatMessageChainer().append(componentFuture, (result) -> callback.accept(message.withUnsignedContent(result))); ++ // Paper end - support asynchronous chat decoration + } + + private static CompletableFuture<FilteredText> filterPlainText(CommandSourceStack source, PlayerChatMessage message) { +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 dd66979bcae33096d72001678e8e55569bea6f53..c8d39e6e1c570c9219f6066da273dc0130920519 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +@@ -93,7 +93,7 @@ public class EntitySelector { + } + + private void checkPermissions(CommandSourceStack source) throws CommandSyntaxException { +- if (this.usesSelector && !source.hasPermission(2, "minecraft.command.selector")) { // CraftBukkit ++ if (!source.bypassSelectorPermissions && (this.usesSelector && !source.hasPermission(2, "minecraft.command.selector"))) { // CraftBukkit // Paper - add bypass for selector perms + throw EntityArgument.ERROR_SELECTORS_NOT_ALLOWED.create(); + } + } +diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java +index 6d7545a925156aef6badcfa2bc40dbdad18c4ac0..0cd6a50837efce87ca052a0e1e24db2b75761196 100644 +--- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java ++++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java +@@ -72,6 +72,7 @@ public class FriendlyByteBuf extends ByteBuf { + + public static final int DEFAULT_NBT_QUOTA = 2097152; + private final ByteBuf source; ++ @Nullable public final java.util.Locale adventure$locale; // Paper - track player's locale for server-side translations + public static final short MAX_STRING_LENGTH = Short.MAX_VALUE; + public static final int MAX_COMPONENT_STRING_LENGTH = 262144; + private static final int PUBLIC_KEY_SIZE = 256; +@@ -80,6 +81,7 @@ public class FriendlyByteBuf extends ByteBuf { + private static final Gson GSON = new Gson(); + + public FriendlyByteBuf(ByteBuf parent) { ++ this.adventure$locale = PacketEncoder.ADVENTURE_LOCALE.get(); // Paper - track player's locale for server-side translations + this.source = parent; + } + +@@ -120,11 +122,16 @@ public class FriendlyByteBuf extends ByteBuf { + } + + public <T> void writeJsonWithCodec(Codec<T> codec, T value) { ++ // Paper start - Adventure; add max length parameter ++ this.writeJsonWithCodec(codec, value, MAX_STRING_LENGTH); ++ } ++ public <T> void writeJsonWithCodec(Codec<T> codec, T value, int maxLength) { ++ // Paper end - Adventure; add max length parameter + DataResult<JsonElement> dataresult = codec.encodeStart(JsonOps.INSTANCE, value); + + this.writeUtf(FriendlyByteBuf.GSON.toJson((JsonElement) dataresult.getOrThrow((s) -> { + return new EncoderException("Failed to encode: " + s + " " + String.valueOf(value)); +- }))); ++ })), maxLength); // Paper - Adventure; add max length parameter + } + + public static <T> IntFunction<T> limitValue(IntFunction<T> applier, int max) { +diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java +index a58f67973b4ed986065860263c7a42214640520d..046bfc212b640de174b300e7a05cc30bb3cac93e 100644 +--- a/src/main/java/net/minecraft/network/PacketEncoder.java ++++ b/src/main/java/net/minecraft/network/PacketEncoder.java +@@ -17,10 +17,12 @@ public class PacketEncoder<T extends PacketListener> extends MessageToByteEncode + this.protocolInfo = state; + } + ++ static final ThreadLocal<java.util.Locale> ADVENTURE_LOCALE = ThreadLocal.withInitial(() -> null); // Paper - adventure; set player's locale + protected void encode(ChannelHandlerContext channelHandlerContext, Packet<T> packet, ByteBuf byteBuf) throws Exception { + PacketType<? extends Packet<? super T>> packetType = packet.type(); + + try { ++ ADVENTURE_LOCALE.set(channelHandlerContext.channel().attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).get()); // Paper - adventure; set player's locale + this.protocolInfo.codec().encode(byteBuf, packet); + int i = byteBuf.readableBytes(); + if (LOGGER.isDebugEnabled()) { +diff --git a/src/main/java/net/minecraft/network/chat/ChatDecorator.java b/src/main/java/net/minecraft/network/chat/ChatDecorator.java +index e4624d696dcf0ddb6d42a80701dfc47ec6877540..6b8dc1eb490098cc14673c9ab0aa16fe3471325b 100644 +--- a/src/main/java/net/minecraft/network/chat/ChatDecorator.java ++++ b/src/main/java/net/minecraft/network/chat/ChatDecorator.java +@@ -2,10 +2,18 @@ package net.minecraft.network.chat; + + import javax.annotation.Nullable; + import net.minecraft.server.level.ServerPlayer; ++import java.util.concurrent.CompletableFuture; // Paper + + @FunctionalInterface + public interface ChatDecorator { +- ChatDecorator PLAIN = (sender, message) -> message; ++ ChatDecorator PLAIN = (sender, message) -> CompletableFuture.completedFuture(message); // Paper - adventure; support async chat decoration events + +- Component decorate(@Nullable ServerPlayer sender, Component message); ++ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - adventure; support chat decoration events (callers should use the overload with CommandSourceStack) ++ CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, Component message); // Paper - adventure; support async chat decoration events ++ ++ // Paper start - adventure; support async chat decoration events ++ default CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) { ++ throw new UnsupportedOperationException("Must override this implementation"); ++ } ++ // Paper end - adventure; support async chat decoration events + } +diff --git a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java +index ec99dc00d83e2369e66ba5e93bf482555aab7b06..22c6a324dedeb315eac2d3d3f55d2f3a9eebb0ad 100644 +--- a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java ++++ b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java +@@ -37,9 +37,31 @@ import net.minecraft.util.StringRepresentable; + + public class ComponentSerialization { + public static final Codec<Component> CODEC = Codec.recursive("Component", ComponentSerialization::createCodec); +- public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistries(CODEC); ++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_CODEC = createTranslationAware(() -> net.minecraft.nbt.NbtAccounter.create(net.minecraft.network.FriendlyByteBuf.DEFAULT_NBT_QUOTA)); // Paper - adventure + public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> OPTIONAL_STREAM_CODEC = STREAM_CODEC.apply(ByteBufCodecs::optional); +- public static final StreamCodec<RegistryFriendlyByteBuf, Component> TRUSTED_STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistriesTrusted(CODEC); ++ // Paper start - adventure; use locale from bytebuf for translation ++ public static final ThreadLocal<Boolean> DONT_RENDER_TRANSLATABLES = ThreadLocal.withInitial(() -> false); ++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> TRUSTED_STREAM_CODEC = createTranslationAware(net.minecraft.nbt.NbtAccounter::unlimitedHeap); ++ private static StreamCodec<RegistryFriendlyByteBuf, Component> createTranslationAware(final Supplier<net.minecraft.nbt.NbtAccounter> sizeTracker) { ++ return new StreamCodec<>() { ++ final StreamCodec<ByteBuf, net.minecraft.nbt.Tag> streamCodec = ByteBufCodecs.tagCodec(sizeTracker); ++ @Override ++ public Component decode(RegistryFriendlyByteBuf registryFriendlyByteBuf) { ++ net.minecraft.nbt.Tag tag = this.streamCodec.decode(registryFriendlyByteBuf); ++ RegistryOps<net.minecraft.nbt.Tag> registryOps = registryFriendlyByteBuf.registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE); ++ return CODEC.parse(registryOps, tag).getOrThrow(error -> new io.netty.handler.codec.DecoderException("Failed to decode: " + error + " " + tag)); ++ } ++ ++ @Override ++ public void encode(RegistryFriendlyByteBuf registryFriendlyByteBuf, Component object) { ++ RegistryOps<net.minecraft.nbt.Tag> registryOps = registryFriendlyByteBuf.registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE); ++ net.minecraft.nbt.Tag tag = (DONT_RENDER_TRANSLATABLES.get() ? CODEC : ComponentSerialization.localizedCodec(registryFriendlyByteBuf.adventure$locale)) ++ .encodeStart(registryOps, object).getOrThrow(error -> new io.netty.handler.codec.EncoderException("Failed to encode: " + error + " " + object)); ++ this.streamCodec.encode(registryFriendlyByteBuf, tag); ++ } ++ }; ++ } ++ // Paper end - adventure; use locale from bytebuf for translation + public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> TRUSTED_OPTIONAL_STREAM_CODEC = TRUSTED_STREAM_CODEC.apply( + ByteBufCodecs::optional + ); +@@ -100,7 +122,27 @@ public class ComponentSerialization { + return ExtraCodecs.orCompressed(mapCodec3, mapCodec2); + } + ++ // Paper start - adventure; create separate codec for each locale ++ private static final java.util.Map<java.util.Locale, Codec<Component>> LOCALIZED_CODECS = new java.util.concurrent.ConcurrentHashMap<>(); ++ ++ public static Codec<Component> localizedCodec(final [email protected] Locale locale) { ++ if (locale == null) { ++ return CODEC; ++ } ++ return LOCALIZED_CODECS.computeIfAbsent(locale, ++ loc -> Codec.recursive("Component", selfCodec -> createCodec(selfCodec, loc))); ++ } ++ ++ ++ // Paper end - adventure; create separate codec for each locale ++ + private static Codec<Component> createCodec(Codec<Component> selfCodec) { ++ // Paper start - adventure; create separate codec for each locale ++ return createCodec(selfCodec, null); ++ } ++ ++ private static Codec<Component> createCodec(Codec<Component> selfCodec, @javax.annotation.Nullable java.util.Locale locale) { ++ // Paper end - adventure; create separate codec for each locale + ComponentContents.Type<?>[] types = new ComponentContents.Type[]{ + PlainTextContents.TYPE, TranslatableContents.TYPE, KeybindContents.TYPE, ScoreContents.TYPE, SelectorContents.TYPE, NbtContents.TYPE + }; +@@ -113,6 +155,34 @@ public class ComponentSerialization { + ) + .apply(instance, MutableComponent::new) + ); ++ // Paper start - adventure; create separate codec for each locale ++ final Codec<Component> origCodec = codec; ++ codec = new Codec<>() { ++ @Override ++ public <T> DataResult<com.mojang.datafixers.util.Pair<Component, T>> decode(final DynamicOps<T> ops, final T input) { ++ return origCodec.decode(ops, input); ++ } ++ ++ @Override ++ public <T> DataResult<T> encode(final Component input, final DynamicOps<T> ops, final T prefix) { ++ final net.kyori.adventure.text.Component adventureComponent; ++ if (input instanceof io.papermc.paper.adventure.AdventureComponent adv) { ++ adventureComponent = adv.adventure$component(); ++ } else if (locale != null && input.getContents() instanceof TranslatableContents && io.papermc.paper.adventure.PaperAdventure.hasAnyTranslations()) { ++ adventureComponent = io.papermc.paper.adventure.PaperAdventure.asAdventure(input); ++ } else { ++ return origCodec.encode(input, ops, prefix); ++ } ++ return io.papermc.paper.adventure.PaperAdventure.localizedCodec(locale) ++ .encode(adventureComponent, ops, prefix); ++ } ++ ++ @Override ++ public String toString() { ++ return origCodec.toString() + "[AdventureComponentAware]"; ++ } ++ }; ++ // Paper end - adventure; create separate codec for each locale + return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), codec) + .xmap(either -> either.map(either2 -> either2.map(Component::literal, ComponentSerialization::createFromList), text -> (Component)text), text -> { + String string = text.tryCollapseToString(); +diff --git a/src/main/java/net/minecraft/network/chat/ComponentUtils.java b/src/main/java/net/minecraft/network/chat/ComponentUtils.java +index 0030c0c91e989fcdc5b7ce6490836a0e8dd3b5d5..3365aed2b67ae0e4dd0410f5190ba474f146139b 100644 +--- a/src/main/java/net/minecraft/network/chat/ComponentUtils.java ++++ b/src/main/java/net/minecraft/network/chat/ComponentUtils.java +@@ -41,6 +41,11 @@ public class ComponentUtils { + if (depth > 100) { + return text.copy(); + } else { ++ // Paper start - adventure; pass actual vanilla component ++ if (text instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) { ++ text = adventureComponent.deepConverted(); ++ } ++ // Paper end - adventure; pass actual vanilla component + MutableComponent mutableComponent = text.getContents().resolve(source, sender, depth + 1); + + for (Component component : text.getSiblings()) { +diff --git a/src/main/java/net/minecraft/network/chat/MessageSignature.java b/src/main/java/net/minecraft/network/chat/MessageSignature.java +index 739ef5fb6f4fa37382153ba6a308ca3b451e6b05..7c3154af5d7732037c0ee965f6f8b89424461abd 100644 +--- a/src/main/java/net/minecraft/network/chat/MessageSignature.java ++++ b/src/main/java/net/minecraft/network/chat/MessageSignature.java +@@ -13,6 +13,7 @@ import net.minecraft.util.SignatureUpdater; + import net.minecraft.util.SignatureValidator; + + public record MessageSignature(byte[] bytes) { ++ public net.kyori.adventure.chat.SignedMessage.Signature adventure() { return () -> this.bytes; } // Paper - adventure; support signed messages + public static final Codec<MessageSignature> CODEC = ExtraCodecs.BASE64_STRING.xmap(MessageSignature::new, MessageSignature::bytes); + public static final int BYTES = 256; + +diff --git a/src/main/java/net/minecraft/network/chat/MutableComponent.java b/src/main/java/net/minecraft/network/chat/MutableComponent.java +index 0a70fb7df0d4532edbc2468b13520c34ae1500e9..e34a8a66411b7571813117ce47d9dec08e567978 100644 +--- a/src/main/java/net/minecraft/network/chat/MutableComponent.java ++++ b/src/main/java/net/minecraft/network/chat/MutableComponent.java +@@ -94,6 +94,11 @@ public class MutableComponent implements Component { + + @Override + public boolean equals(Object object) { ++ // Paper start - make AdventureComponent equivalent ++ if (object instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) { ++ object = adventureComponent.deepConverted(); ++ } ++ // Paper end - make AdventureComponent equivalent + return this == object + || object instanceof MutableComponent mutableComponent + && this.contents.equals(mutableComponent.contents) +diff --git a/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java b/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java +index e2def0ca552343143e495736d533b3334686fd62..c87b708c368713a23a10ad97704575ee4df27891 100644 +--- a/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java ++++ b/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java +@@ -7,6 +7,12 @@ public interface OutgoingChatMessage { + + void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params); + ++ // Paper start ++ default void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) { ++ this.sendToPlayer(sender, filterMaskEnabled, params); ++ } ++ // Paper end ++ + static OutgoingChatMessage create(PlayerChatMessage message) { + return (OutgoingChatMessage)(message.isSystem() + ? new OutgoingChatMessage.Disguised(message.decoratedContent()) +@@ -16,7 +22,12 @@ public interface OutgoingChatMessage { + public static record Disguised(@Override Component content) implements OutgoingChatMessage { + @Override + public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) { +- sender.connection.sendDisguisedChatMessage(this.content, params); ++ // Paper start ++ this.sendToPlayer(sender, filterMaskEnabled, params, null); ++ } ++ public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) { ++ sender.connection.sendDisguisedChatMessage(unsigned != null ? unsigned : this.content, params); ++ // Paper end + } + } + +@@ -28,7 +39,13 @@ public interface OutgoingChatMessage { + + @Override + public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) { ++ // Paper start ++ this.sendToPlayer(sender, filterMaskEnabled, params, null); ++ } ++ public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) { ++ // Paper end + PlayerChatMessage playerChatMessage = this.message.filter(filterMaskEnabled); ++ playerChatMessage = unsigned != null ? playerChatMessage.withUnsignedContent(unsigned) : playerChatMessage; // Paper + if (!playerChatMessage.isFullyFiltered()) { + sender.connection.sendPlayerChatMessage(playerChatMessage, params); + } +diff --git a/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java b/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java +index d89049328641faa889b7c567123ab3a2c63b8df0..76b304560c2b631a18f20e656a65ac75af7b4e63 100644 +--- a/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java ++++ b/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java +@@ -17,6 +17,42 @@ import net.minecraft.util.SignatureValidator; + public record PlayerChatMessage( + SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask + ) { ++ // Paper start - adventure; support signed messages ++ public final class AdventureView implements net.kyori.adventure.chat.SignedMessage { ++ private AdventureView() { ++ } ++ @Override ++ public @org.jetbrains.annotations.NotNull Instant timestamp() { ++ return PlayerChatMessage.this.timeStamp(); ++ } ++ @Override ++ public long salt() { ++ return PlayerChatMessage.this.salt(); ++ } ++ @Override ++ public @org.jetbrains.annotations.Nullable Signature signature() { ++ return PlayerChatMessage.this.signature == null ? null : PlayerChatMessage.this.signature.adventure(); ++ } ++ @Override ++ public [email protected] Component unsignedContent() { ++ return PlayerChatMessage.this.unsignedContent() == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(PlayerChatMessage.this.unsignedContent()); ++ } ++ @Override ++ public @org.jetbrains.annotations.NotNull String message() { ++ return PlayerChatMessage.this.signedContent(); ++ } ++ @Override ++ public @org.jetbrains.annotations.NotNull net.kyori.adventure.identity.Identity identity() { ++ return net.kyori.adventure.identity.Identity.identity(PlayerChatMessage.this.sender()); ++ } ++ public PlayerChatMessage playerChatMessage() { ++ return PlayerChatMessage.this; ++ } ++ } ++ public AdventureView adventureView() { ++ return new AdventureView(); ++ } ++ // Paper end - adventure; support signed messages + public static final MapCodec<PlayerChatMessage> MAP_CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group( + SignedMessageLink.CODEC.fieldOf("link").forGetter(PlayerChatMessage::link), +@@ -47,7 +83,14 @@ public record PlayerChatMessage( + } + + public PlayerChatMessage withUnsignedContent(Component unsignedContent) { +- Component component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null; ++ // Paper start - adventure ++ final Component component; ++ if (unsignedContent instanceof io.papermc.paper.adventure.AdventureComponent advComponent) { ++ component = this.signedContent().equals(io.papermc.paper.adventure.AdventureCodecs.tryCollapseToString(advComponent.adventure$component())) ? null : unsignedContent; ++ } else { ++ component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null; ++ } ++ // Paper end - adventure + return new PlayerChatMessage(this.link, this.signature, this.signedBody, component, this.filterMask); + } + +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java +index c5ba5e30cd74ca2cfc3f952c7b992df239a53a34..fdb75db02603ef97b624219211a0db22cd447dea 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java +@@ -18,6 +18,11 @@ public record ClientboundSystemChatPacket(Component content, boolean overlay) im + this(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(content)), overlay); + } + // Spigot end ++ // Paper start ++ public ClientboundSystemChatPacket(net.kyori.adventure.text.Component content, boolean overlay) { ++ this(io.papermc.paper.adventure.PaperAdventure.asVanilla(content), overlay); ++ } ++ // Paper end + + @Override + public PacketType<ClientboundSystemChatPacket> type() { +diff --git a/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java b/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java +index bb97fdb9aa6167083442a928276ebe4225a586ef..5d1758086ed4fce5b36a5b31df44ccea42abc590 100644 +--- a/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java +@@ -18,11 +18,15 @@ public class ClientboundLoginDisconnectPacket implements Packet<ClientLoginPacke + } + + private ClientboundLoginDisconnectPacket(FriendlyByteBuf buf) { +- this.reason = Component.Serializer.fromJsonLenient(buf.readUtf(262144), RegistryAccess.EMPTY); ++ this.reason = Component.Serializer.fromJsonLenient(buf.readUtf(FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH), RegistryAccess.EMPTY); // Paper - diff on change + } + + private void write(FriendlyByteBuf buf) { +- buf.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY)); ++ // Paper start - Adventure ++ // buf.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY)); ++ // In the login phase, buf.adventure$locale field is always null ++ buf.writeJsonWithCodec(net.minecraft.network.chat.ComponentSerialization.localizedCodec(java.util.Locale.US), this.reason, FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH); ++ // Paper end - Adventure + } + + @Override +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index b1325e090f2c7aff31d27fc38ca7173efe31ed7c..0742aaf07f37e51d24295e7819ac6cec961c7626 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -201,6 +201,7 @@ import org.bukkit.craftbukkit.SpigotTimings; // Spigot + public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable { + + public static final Logger LOGGER = LogUtils.getLogger(); ++ public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper + public static final String VANILLA_BRAND = "vanilla"; + private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F; + private static final int TICK_STATS_SPAN = 100; +@@ -251,8 +252,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + private boolean preventProxyConnections; + private boolean pvp; + private boolean allowFlight; +- @Nullable +- private String motd; ++ private net.kyori.adventure.text.Component motd; // Paper - Adventure + private int playerIdleTimeout; + private final long[] tickTimesNanos; + private long aggregatedTickTimesNanos; +@@ -1397,7 +1397,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + private ServerStatus buildServerStatus() { + ServerStatus.Players serverping_serverpingplayersample = this.buildPlayerStatus(); + +- return new ServerStatus(Component.nullToEmpty(this.motd), Optional.of(serverping_serverpingplayersample), Optional.of(ServerStatus.Version.current()), Optional.ofNullable(this.statusIcon), this.enforceSecureProfile()); ++ return new ServerStatus(io.papermc.paper.adventure.PaperAdventure.asVanilla(this.motd), Optional.of(serverping_serverpingplayersample), Optional.of(ServerStatus.Version.current()), Optional.ofNullable(this.statusIcon), this.enforceSecureProfile()); // Paper - Adventure + } + + private ServerStatus.Players buildPlayerStatus() { +@@ -1429,6 +1429,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + SpigotTimings.schedulerTimer.startTiming(); // Spigot + this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit + SpigotTimings.schedulerTimer.stopTiming(); // Spigot ++ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper + this.profiler.push("commandFunctions"); + SpigotTimings.commandFunctionsTimer.startTiming(); // Spigot + this.getFunctions().tick(); +@@ -1805,10 +1806,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + @Override + public String getMotd() { +- return this.motd; ++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.motd); // Paper - Adventure + } + + public void setMotd(String motd) { ++ // Paper start - Adventure ++ this.motd = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserializeOr(motd, net.kyori.adventure.text.Component.empty()); ++ } ++ ++ public net.kyori.adventure.text.Component motd() { ++ return this.motd; ++ } ++ ++ public void motd(net.kyori.adventure.text.Component motd) { ++ // Paper end - Adventure + this.motd = motd; + } + +@@ -2570,23 +2581,24 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + + public void logChatMessage(Component message, ChatType.Bound params, @Nullable String prefix) { +- String s1 = params.decorate(message).getString(); ++ // Paper start ++ net.kyori.adventure.text.Component s1 = io.papermc.paper.adventure.PaperAdventure.asAdventure(params.decorate(message)); + + if (prefix != null) { +- MinecraftServer.LOGGER.info("[{}] {}", prefix, s1); ++ MinecraftServer.COMPONENT_LOGGER.info("[{}] {}", prefix, s1); + } else { +- MinecraftServer.LOGGER.info("{}", s1); ++ MinecraftServer.COMPONENT_LOGGER.info("{}", s1); ++ // Paper end + } + + } + +- // CraftBukkit start + public final java.util.concurrent.ExecutorService chatExecutor = java.util.concurrent.Executors.newCachedThreadPool( +- new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").build()); +- // CraftBukkit end ++ new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); // Paper + ++ public final ChatDecorator improvedChatDecorator = new io.papermc.paper.adventure.ImprovedChatDecorator(this); // Paper - adventure + public ChatDecorator getChatDecorator() { +- return ChatDecorator.PLAIN; ++ return this.improvedChatDecorator; // Paper - support async chat decoration events + } + + public boolean logIPs() { +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 5f8adf662e5ab9114e2891cfacb8db5d4d40a297..a8a7f1fcf235508d9437ec7c550c3a3853321b78 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -171,6 +171,7 @@ import net.minecraft.world.item.trading.MerchantOffers; + import net.minecraft.world.scores.Scoreboard; + import net.minecraft.world.scores.Team; + import net.minecraft.world.scores.criteria.ObjectiveCriteria; ++import io.papermc.paper.adventure.PaperAdventure; // Paper + import org.bukkit.Bukkit; + import org.bukkit.Location; + import org.bukkit.WeatherType; +@@ -236,6 +237,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + private boolean disconnected; + private int requestedViewDistance; + public String language = "en_us"; // CraftBukkit - default ++ public java.util.Locale adventure$locale = java.util.Locale.US; // Paper + @Nullable + private Vec3 startingToFallPosition; + @Nullable +@@ -269,6 +271,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + // CraftBukkit start + public CraftPlayer.TransferCookieConnection transferCookieConnection; + public String displayName; ++ public net.kyori.adventure.text.Component adventure$displayName; // Paper + public Component listName; + public org.bukkit.Location compassTarget; + public int newExp = 0; +@@ -355,6 +358,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + + // CraftBukkit start + this.displayName = this.getScoreboardName(); ++ this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper + this.bukkitPickUpLoot = true; + this.maxHealthCache = this.getMaxHealth(); + } +@@ -912,22 +916,17 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + + String deathmessage = defaultMessage.getString(); + this.keepLevel = keepInventory; // SPIGOT-2222: pre-set keepLevel +- org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, deathmessage, keepInventory); ++ org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, PaperAdventure.asAdventure(defaultMessage), keepInventory); // Paper - Adventure + + // SPIGOT-943 - only call if they have an inventory open + if (this.containerMenu != this.inventoryMenu) { + this.closeContainer(); + } + +- String deathMessage = event.getDeathMessage(); ++ net.kyori.adventure.text.Component deathMessage = event.deathMessage() != null ? event.deathMessage() : net.kyori.adventure.text.Component.empty(); // Paper - Adventure + +- if (deathMessage != null && deathMessage.length() > 0 && flag) { // TODO: allow plugins to override? +- Component ichatbasecomponent; +- if (deathMessage.equals(deathmessage)) { +- ichatbasecomponent = this.getCombatTracker().getDeathMessage(); +- } else { +- ichatbasecomponent = org.bukkit.craftbukkit.util.CraftChatMessage.fromStringOrNull(deathMessage); +- } ++ if (deathMessage != null && deathMessage != net.kyori.adventure.text.Component.empty() && flag) { // Paper - Adventure // TODO: allow plugins to override? ++ Component ichatbasecomponent = PaperAdventure.asVanilla(deathMessage); // Paper - Adventure + + this.connection.send(new ClientboundPlayerCombatKillPacket(this.getId(), ichatbasecomponent), PacketSendListener.exceptionallySend(() -> { + boolean flag1 = true; +@@ -2027,8 +2026,13 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + } + + public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params) { ++ // Paper start ++ this.sendChatMessage(message, filterMaskEnabled, params, null); ++ } ++ public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params, @Nullable Component unsigned) { ++ // Paper end + if (this.acceptsChatMessages()) { +- message.sendToPlayer(this, filterMaskEnabled, params); ++ message.sendToPlayer(this, filterMaskEnabled, params, unsigned); // Paper + } + + } +@@ -2055,6 +2059,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + } + // CraftBukkit end + this.language = clientOptions.language(); ++ this.adventure$locale = java.util.Objects.requireNonNullElse(net.kyori.adventure.translation.Translator.parseLocale(this.language), java.util.Locale.US); // Paper + this.requestedViewDistance = clientOptions.viewDistance(); + this.chatVisibility = clientOptions.chatVisibility(); + this.canChatColor = clientOptions.chatColors(); +diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +index 677ee8dfa0d2f5bcb23ee870a76bcee63e557813..94008b8a1f2dafdb9efa1fec6096d3eb632b9a5e 100644 +--- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -67,7 +67,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + private static final Component TIMEOUT_DISCONNECTION_MESSAGE = Component.translatable("disconnect.timeout"); + static final Component DISCONNECT_UNEXPECTED_QUERY = Component.translatable("multiplayer.disconnect.unexpected_query_response"); + protected final MinecraftServer server; +- protected final Connection connection; ++ public final Connection connection; // Paper + private final boolean transferred; + private long keepAliveTime; + private boolean keepAlivePending; +@@ -76,6 +76,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + private boolean closed = false; + private int latency; + private volatile boolean suspendFlushingOnServerThread = false; ++ public final java.util.Map<java.util.UUID, net.kyori.adventure.resource.ResourcePackCallback> packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks + + public ServerCommonPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { // CraftBukkit + this.server = minecraftserver; +@@ -189,6 +190,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + ServerCommonPacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack {} rejection", this.playerProfile().getName(), packet.id()); + this.disconnect((Component) Component.translatable("multiplayer.requiredTexturePrompt.disconnect")); + } ++ // Paper start - adventure pack callbacks ++ // call the callbacks before the previously-existing event so the event has final say ++ final net.kyori.adventure.resource.ResourcePackCallback callback; ++ if (packet.action().isTerminal()) { ++ callback = this.packCallbacks.remove(packet.id()); ++ } else { ++ callback = this.packCallbacks.get(packet.id()); ++ } ++ if (callback != null) { ++ callback.packEventReceived(packet.id(), net.kyori.adventure.resource.ResourcePackStatus.valueOf(packet.action().name()), this.getCraftPlayer()); ++ } ++ // Paper end + this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()])); // CraftBukkit + + } +@@ -275,6 +288,12 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + } + ++ // Paper start - adventure ++ public void disconnect(@Nullable final net.kyori.adventure.text.Component reason) { ++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason)); ++ } ++ // Paper end - adventure ++ + public void disconnect(Component reason) { + this.disconnect(new DisconnectionDetails(reason)); + } +@@ -305,9 +324,9 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + return; + } + +- String leaveMessage = ChatFormatting.YELLOW + this.player.getScoreboardName() + " left the game."; ++ net.kyori.adventure.text.Component leaveMessage = net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? this.player.getBukkitEntity().displayName() : net.kyori.adventure.text.Component.text(this.player.getScoreboardName())); // Paper - Adventure + +- PlayerKickEvent event = new PlayerKickEvent(this.player.getBukkitEntity(), CraftChatMessage.fromComponent(disconnectionInfo.reason()), leaveMessage); ++ PlayerKickEvent event = new PlayerKickEvent(this.player.getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionInfo.reason()), leaveMessage); // Paper - adventure + + if (this.cserver.getServer().isRunning()) { + this.cserver.getPluginManager().callEvent(event); +@@ -319,7 +338,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + this.player.kickLeaveMessage = event.getLeaveMessage(); // CraftBukkit - SPIGOT-3034: Forward leave message to PlayerQuitEvent + // Send the possibly modified leave message +- this.disconnect0(new DisconnectionDetails(CraftChatMessage.fromString(event.getReason(), true)[0], disconnectionInfo.report(), disconnectionInfo.bugReportLink())); ++ this.disconnect0(new DisconnectionDetails(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.reason()), disconnectionInfo.report(), disconnectionInfo.bugReportLink())); // Paper - Adventure + } + + private void disconnect0(DisconnectionDetails disconnectiondetails) { +diff --git a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +index e7c407039fef88ef01ba9b6be9ae5bcc3edc026f..5457358bc76889153036818fdfd70a043ec4e40f 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +@@ -120,6 +120,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + @Override + public void handleClientInformation(ServerboundClientInformationPacket packet) { + this.clientInformation = packet.information(); ++ this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper + } + + @Override +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 65b23aad3ea71d409253d3365fc1e37480b8e5ee..27cf5dceba5835f94f5397ec011f409e7b226ad5 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -45,6 +45,7 @@ import net.minecraft.nbt.CompoundTag; + import net.minecraft.network.Connection; + import net.minecraft.network.DisconnectionDetails; + import net.minecraft.network.TickablePacketListener; ++import net.minecraft.network.chat.ChatDecorator; + import net.minecraft.network.chat.ChatType; + import net.minecraft.network.chat.Component; + import net.minecraft.network.chat.LastSeenMessages; +@@ -195,6 +196,8 @@ import net.minecraft.world.phys.shapes.VoxelShape; + import org.slf4j.Logger; + + // CraftBukkit start ++import io.papermc.paper.adventure.ChatProcessor; // Paper ++import io.papermc.paper.adventure.PaperAdventure; // Paper + import com.mojang.datafixers.util.Pair; + import java.util.Arrays; + import java.util.concurrent.ExecutionException; +@@ -1728,9 +1731,11 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + */ + + this.player.disconnect(); +- String quitMessage = this.server.getPlayerList().remove(this.player); +- if ((quitMessage != null) && (quitMessage.length() > 0)) { +- this.server.getPlayerList().broadcastMessage(CraftChatMessage.fromString(quitMessage)); ++ // Paper start - Adventure ++ net.kyori.adventure.text.Component quitMessage = this.server.getPlayerList().remove(this.player); ++ if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { ++ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); ++ // Paper end + } + // CraftBukkit end + this.player.getTextFilter().leave(); +@@ -1791,10 +1796,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + CompletableFuture<FilteredText> completablefuture = this.filterTextPacket(playerchatmessage.signedContent()).thenApplyAsync(Function.identity(), this.server.chatExecutor); // CraftBukkit - async chat +- Component ichatbasecomponent = this.server.getChatDecorator().decorate(this.player, playerchatmessage.decoratedContent()); ++ CompletableFuture<Component> componentFuture = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage.decoratedContent()); // Paper - Adventure + +- this.chatMessageChain.append(completablefuture, (filteredtext) -> { +- PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(ichatbasecomponent).filter(filteredtext.mask()); ++ this.chatMessageChain.append(CompletableFuture.allOf(completablefuture, componentFuture), (filteredtext) -> { // Paper - Adventure ++ PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(componentFuture.join()).filter(completablefuture.join().mask()); // Paper - Adventure + + this.broadcastChatMessage(playerchatmessage1); + }); +@@ -2014,7 +2019,15 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.handleCommand(s); + } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { + // Do nothing, this is coming from a plugin +- } else { ++ // Paper start ++ } else if (true) { ++ if (!async && !org.bukkit.Bukkit.isPrimaryThread()) { ++ org.spigotmc.AsyncCatcher.catchOp("Asynchronous player chat is not allowed here"); ++ } ++ final ChatProcessor cp = new ChatProcessor(this.server, this.player, original, async); ++ cp.process(); ++ // Paper end ++ } else if (false) { // Paper + Player player = this.getCraftPlayer(); + AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, s, new LazyPlayerSet(this.server)); + String originalFormat = event.getFormat(), originalMessage = event.getMessage(); +@@ -3011,6 +3024,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + public void handleClientInformation(ServerboundClientInformationPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); + this.player.updateOptions(packet.information()); ++ this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper + } + + @Override +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 8e715261b99bcc19b6f4b9787987c59aa19ba70e..851e4bdf1739736fd193786a6541ffb341525dcb 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -333,7 +333,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { + final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); + if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { +- event.disallow(asyncEvent.getResult(), asyncEvent.getKickMessage()); ++ event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure + } + Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<PlayerPreLoginEvent.Result>() { + @Override +@@ -345,12 +345,12 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + + ServerLoginPacketListenerImpl.this.server.processQueue.add(waitable); + if (waitable.get() != PlayerPreLoginEvent.Result.ALLOWED) { +- this.disconnect(event.getKickMessage()); ++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure + return; + } + } else { + if (asyncEvent.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { +- this.disconnect(asyncEvent.getKickMessage()); ++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(asyncEvent.kickMessage())); // Paper - Adventure + return; + } + } +diff --git a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +index 2c555c5ad05992b19f67b883c3c0ec7018180747..71540687b4212702cdaaad5fd4815fb3eb97ddd6 100644 +--- a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +@@ -58,7 +58,7 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene + CraftIconCache icon = server.server.getServerIcon(); + + ServerListPingEvent() { +- super(ServerStatusPacketListenerImpl.this.connection.hostname, ((InetSocketAddress) ServerStatusPacketListenerImpl.this.connection.getRemoteAddress()).getAddress(), server.getMotd(), server.getPlayerList().getMaxPlayers()); ++ super(ServerStatusPacketListenerImpl.this.connection.hostname, ((InetSocketAddress) ServerStatusPacketListenerImpl.this.connection.getRemoteAddress()).getAddress(), server.server.motd(), server.getPlayerList().getMaxPlayers()); // Paper - Adventure + } + + @Override +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 6abe921099ff00ecfaf0f423ef27d708420f6f48..9113c183f1e977f71eea95a891618883b075ce5a 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -271,7 +271,7 @@ public abstract class PlayerList { + } + // CraftBukkit start + ichatmutablecomponent.withStyle(ChatFormatting.YELLOW); +- String joinMessage = CraftChatMessage.fromComponent(ichatmutablecomponent); ++ Component joinMessage = ichatmutablecomponent; // Paper - Adventure + + playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); + ServerStatus serverping = this.server.getStatus(); +@@ -292,19 +292,18 @@ public abstract class PlayerList { + // Ensure that player inventory is populated with its viewer + player.containerMenu.transferTo(player.containerMenu, bukkitPlayer); + +- PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, joinMessage); ++ PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + this.cserver.getPluginManager().callEvent(playerJoinEvent); + + if (!player.connection.isAcceptingMessages()) { + return; + } + +- joinMessage = playerJoinEvent.getJoinMessage(); ++ final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); + +- if (joinMessage != null && joinMessage.length() > 0) { +- for (Component line : org.bukkit.craftbukkit.util.CraftChatMessage.fromString(joinMessage)) { +- this.server.getPlayerList().broadcastSystemMessage(line, false); +- } ++ if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure ++ joinMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(jm); // Paper - Adventure ++ this.server.getPlayerList().broadcastSystemMessage(joinMessage, false); // Paper - Adventure + } + // CraftBukkit end + +@@ -492,7 +491,7 @@ public abstract class PlayerList { + + } + +- public String remove(ServerPlayer entityplayer) { // CraftBukkit - return string ++ public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component + ServerLevel worldserver = entityplayer.serverLevel(); + + entityplayer.awardStat(Stats.LEAVE_GAME); +@@ -503,7 +502,7 @@ public abstract class PlayerList { + entityplayer.closeContainer(); + } + +- PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(entityplayer.getBukkitEntity(), entityplayer.kickLeaveMessage != null ? entityplayer.kickLeaveMessage : "\u00A7e" + entityplayer.getScoreboardName() + " left the game"); ++ PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(entityplayer.getBukkitEntity(), net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName()))); // Paper - Adventure + this.cserver.getPluginManager().callEvent(playerQuitEvent); + entityplayer.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage()); + +@@ -556,7 +555,7 @@ public abstract class PlayerList { + this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity()); + // CraftBukkit end + +- return playerQuitEvent.getQuitMessage(); // CraftBukkit ++ return playerQuitEvent.quitMessage(); // Paper - Adventure + } + + // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer +@@ -603,11 +602,11 @@ public abstract class PlayerList { + } + + // return chatmessage; +- event.disallow(PlayerLoginEvent.Result.KICK_BANNED, CraftChatMessage.fromComponent(ichatmutablecomponent)); ++ event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + } else if (!this.isWhiteListed(gameprofile)) { + ichatmutablecomponent = Component.translatable("multiplayer.disconnect.not_whitelisted"); +- event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, org.spigotmc.SpigotConfig.whitelistMessage); // Spigot +- } else if (this.ipBans.isBanned(socketaddress)) { ++ event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage)); // Spigot // Paper - Adventure ++ } else if (this.getIpBans().isBanned(socketaddress) && !this.getIpBans().get(socketaddress).hasExpired()) { + IpBanListEntry ipbanentry = this.ipBans.get(socketaddress); + + ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason()); +@@ -616,17 +615,17 @@ public abstract class PlayerList { + } + + // return chatmessage; +- event.disallow(PlayerLoginEvent.Result.KICK_BANNED, CraftChatMessage.fromComponent(ichatmutablecomponent)); ++ event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.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)) { +- event.disallow(PlayerLoginEvent.Result.KICK_FULL, org.spigotmc.SpigotConfig.serverFullMessage); // Spigot ++ event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } + + this.cserver.getPluginManager().callEvent(event); + if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { +- loginlistener.disconnect(event.getKickMessage()); ++ loginlistener.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure + return null; + } + return entity; +@@ -1122,7 +1121,7 @@ public abstract class PlayerList { + public void removeAll() { + // CraftBukkit start - disconnect safely + for (ServerPlayer player : this.players) { +- player.connection.disconnect(CraftChatMessage.fromStringOrEmpty(this.server.server.getShutdownMessage())); // CraftBukkit - add custom shutdown message ++ player.connection.disconnect(this.server.server.shutdownMessage()); // CraftBukkit - add custom shutdown message // Paper - Adventure + } + // CraftBukkit end + +@@ -1163,24 +1162,43 @@ public abstract class PlayerList { + } + + public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params) { ++ // Paper start ++ this.broadcastChatMessage(message, sender, params, null); ++ } ++ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) { ++ // Paper end + Objects.requireNonNull(sender); +- this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params); ++ this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params, unsignedFunction); // Paper + } + + private void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params) { ++ // Paper start ++ this.broadcastChatMessage(message, shouldSendFiltered, sender, params, null); ++ } ++ public void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) { ++ // Paper end + boolean flag = this.verifyChatTrusted(message); + +- this.server.logChatMessage(message.decoratedContent(), params, flag ? null : "Not Secure"); ++ this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), params, flag ? null : "Not Secure"); // Paper + OutgoingChatMessage outgoingchatmessage = OutgoingChatMessage.create(message); + boolean flag1 = false; + + boolean flag2; ++ Packet<?> disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingchatmessage.content(), params) : null; // Paper - don't send player chat packets from vanished players + + for (Iterator iterator = this.players.iterator(); iterator.hasNext(); flag1 |= flag2 && message.isFullyFiltered()) { + ServerPlayer entityplayer1 = (ServerPlayer) iterator.next(); + + flag2 = shouldSendFiltered.test(entityplayer1); +- entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params); ++ // Paper start - don't send player chat packets from vanished players ++ if (sender != null && !entityplayer1.getBukkitEntity().canSee(sender.getBukkitEntity())) { ++ entityplayer1.connection.send(unsignedFunction != null ++ ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(entityplayer1.getBukkitEntity()), params) ++ : disguised); ++ continue; ++ } ++ // Paper end ++ entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params, unsignedFunction == null ? null : unsignedFunction.apply(entityplayer1.getBukkitEntity())); // Paper + } + + if (flag1 && sender != null) { +@@ -1189,7 +1207,7 @@ public abstract class PlayerList { + + } + +- private boolean verifyChatTrusted(PlayerChatMessage message) { ++ public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public + return message.hasSignature() && !message.hasExpiredServer(Instant.now()); + } + +diff --git a/src/main/java/net/minecraft/world/BossEvent.java b/src/main/java/net/minecraft/world/BossEvent.java +index ed54c81a3269360acce674aa4e1d54ccb2461841..c9c849534c3998cfcab7ddcb12a71ccb1fdb3e1a 100644 +--- a/src/main/java/net/minecraft/world/BossEvent.java ++++ b/src/main/java/net/minecraft/world/BossEvent.java +@@ -13,6 +13,7 @@ public abstract class BossEvent { + protected boolean darkenScreen; + protected boolean playBossMusic; + protected boolean createWorldFog; ++ public net.kyori.adventure.bossbar.BossBar adventure; // Paper + + public BossEvent(UUID uuid, Component name, BossEvent.BossBarColor color, BossEvent.BossBarOverlay style) { + this.id = uuid; +@@ -27,61 +28,75 @@ public abstract class BossEvent { + } + + public Component getName() { ++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.name()); // Paper + return this.name; + } + + public void setName(Component name) { ++ if (this.adventure != null) this.adventure.name(io.papermc.paper.adventure.PaperAdventure.asAdventure(name)); // Paper + this.name = name; + } + + public float getProgress() { ++ if (this.adventure != null) return this.adventure.progress(); // Paper + return this.progress; + } + + public void setProgress(float percent) { ++ if (this.adventure != null) this.adventure.progress(percent); // Paper + this.progress = percent; + } + + public BossEvent.BossBarColor getColor() { ++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.color()); // Paper + return this.color; + } + + public void setColor(BossEvent.BossBarColor color) { ++ if (this.adventure != null) this.adventure.color(io.papermc.paper.adventure.PaperAdventure.asAdventure(color)); // Paper + this.color = color; + } + + public BossEvent.BossBarOverlay getOverlay() { ++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.overlay()); // Paper + return this.overlay; + } + + public void setOverlay(BossEvent.BossBarOverlay style) { ++ if (this.adventure != null) this.adventure.overlay(io.papermc.paper.adventure.PaperAdventure.asAdventure(style)); // Paper + this.overlay = style; + } + + public boolean shouldDarkenScreen() { ++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.DARKEN_SCREEN); // Paper + return this.darkenScreen; + } + + public BossEvent setDarkenScreen(boolean darkenSky) { ++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.DARKEN_SCREEN, darkenSky); // Paper + this.darkenScreen = darkenSky; + return this; + } + + public boolean shouldPlayBossMusic() { ++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC); // Paper + return this.playBossMusic; + } + + public BossEvent setPlayBossMusic(boolean dragonMusic) { ++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC, dragonMusic); // Paper + this.playBossMusic = dragonMusic; + return this; + } + + public BossEvent setCreateWorldFog(boolean thickenFog) { ++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG, thickenFog); // Paper + this.createWorldFog = thickenFog; + return this; + } + + public boolean shouldCreateWorldFog() { ++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG); // Paper + return this.createWorldFog; + } + +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index b6cb1de08e955dfc99a0cb736b2ed456c99fde5c..abf502b8395bb0a8a32c1fdcd62532790deb1c6e 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -183,7 +183,15 @@ public final class ItemStack implements DataComponentHolder { + CraftItemStack.setItemMeta(itemstack, CraftItemStack.getItemMeta(itemstack)); + // Spigot end + ITEM_STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.getItemHolder()); // CraftBukkit - decompile error ++ // Paper start - adventure; conditionally render translatable components ++ boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get(); ++ try { ++ net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true); + DataComponentPatch.STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.components.asPatch()); ++ } finally { ++ net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(prev); ++ } ++ // Paper end - adventure; conditionally render translatable components + } + } + }; +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 8ec376f453ac1f4c9423483f5ae1625b295858c7..e535fb3b5194b8412c0c26c0799340916c7542eb 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 +@@ -210,22 +210,22 @@ public class SignBlockEntity extends BlockEntity implements CommandSource { // C + + // CraftBukkit start + Player player = ((ServerPlayer) entityhuman).getBukkitEntity(); +- String[] lines = new String[4]; ++ List<net.kyori.adventure.text.Component> lines = new java.util.ArrayList<>(); // Paper - adventure + + for (int i = 0; i < list.size(); ++i) { +- lines[i] = CraftChatMessage.fromComponent(signtext.getMessage(i, entityhuman.isTextFilteringEnabled())); ++ lines.add(io.papermc.paper.adventure.PaperAdventure.asAdventure(signtext.getMessage(i, entityhuman.isTextFilteringEnabled()))); // Paper - Adventure + } + +- SignChangeEvent event = new SignChangeEvent(CraftBlock.at(this.level, this.worldPosition), player, lines.clone(), (front) ? Side.FRONT : Side.BACK); ++ SignChangeEvent event = new SignChangeEvent(CraftBlock.at(this.level, this.worldPosition), player, new java.util.ArrayList<>(lines), (front) ? Side.FRONT : Side.BACK); // Paper - Adventure + entityhuman.level().getCraftServer().getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return originalText; + } + +- Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.getLines()); ++ Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.lines()); // Paper - Adventure + for (int i = 0; i < components.length; i++) { +- if (!Objects.equals(lines[i], event.getLine(i))) { ++ if (!Objects.equals(lines.get(i), event.line(i))) { // Paper - Adventure + signtext = signtext.setMessage(i, components[i]); + } + } +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index 6acee03278c8005a06d9cd2577761f2f5355a7ec..5e469bd4d9ca428abdd9d758993164635dc86f27 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -45,6 +45,7 @@ import net.minecraft.world.level.saveddata.SavedData; + import org.slf4j.Logger; + + // CraftBukkit start ++import io.papermc.paper.adventure.PaperAdventure; // Paper + import java.util.UUID; + import org.bukkit.Bukkit; + import org.bukkit.craftbukkit.CraftServer; +@@ -615,7 +616,7 @@ public class MapItemSavedData extends SavedData { + + for (org.bukkit.map.MapCursor cursor : render.cursors) { + if (cursor.isVisible()) { +- icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), CraftChatMessage.fromStringOrOptional(cursor.getCaption()))); ++ icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption())))); + } + } + collection = icons; +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 9babfd8e6c847ea26863be6243f17fc252dc9e1d..0e3ff653211b0210f8679b475d5f62eecbdfd946 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -636,8 +636,10 @@ public final class CraftServer implements Server { + } + + @Override ++ @Deprecated // Paper start + public int broadcastMessage(String message) { + return this.broadcast(message, BROADCAST_CHANNEL_USERS); ++ // Paper end + } + + @Override +@@ -1613,7 +1615,15 @@ public final class CraftServer implements Server { + return this.configuration.getInt("settings.spawn-radius", -1); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component shutdownMessage() { ++ String msg = getShutdownMessage(); ++ return msg != null ? net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(msg) : null; ++ } ++ // Paper end + @Override ++ @Deprecated // Paper + public String getShutdownMessage() { + return this.configuration.getString("settings.shutdown-message"); + } +@@ -1787,7 +1797,20 @@ public final class CraftServer implements Server { + } + + @Override ++ @Deprecated // Paper + public int broadcast(String message, String permission) { ++ // Paper start - Adventure ++ return this.broadcast(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(message), permission); ++ } ++ ++ @Override ++ public int broadcast(net.kyori.adventure.text.Component message) { ++ return this.broadcast(message, BROADCAST_CHANNEL_USERS); ++ } ++ ++ @Override ++ public int broadcast(net.kyori.adventure.text.Component message, String permission) { ++ // Paper end + Set<CommandSender> recipients = new HashSet<>(); + for (Permissible permissible : this.getPluginManager().getPermissionSubscriptions(permission)) { + if (permissible instanceof CommandSender && permissible.hasPermission(permission)) { +@@ -1795,14 +1818,14 @@ public final class CraftServer implements Server { + } + } + +- BroadcastMessageEvent broadcastMessageEvent = new BroadcastMessageEvent(!Bukkit.isPrimaryThread(), message, recipients); ++ BroadcastMessageEvent broadcastMessageEvent = new BroadcastMessageEvent(!Bukkit.isPrimaryThread(), message, recipients); // Paper - Adventure + this.getPluginManager().callEvent(broadcastMessageEvent); + + if (broadcastMessageEvent.isCancelled()) { + return 0; + } + +- message = broadcastMessageEvent.getMessage(); ++ message = broadcastMessageEvent.message(); // Paper - Adventure + + for (CommandSender recipient : recipients) { + recipient.sendMessage(message); +@@ -2064,6 +2087,14 @@ public final class CraftServer implements Server { + return CraftInventoryCreator.INSTANCE.createInventory(owner, type); + } + ++ // Paper start ++ @Override ++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ Preconditions.checkArgument(type.isCreatable(), "Cannot open an inventory of type ", type); ++ return CraftInventoryCreator.INSTANCE.createInventory(owner, type, title); ++ } ++ // Paper end ++ + @Override + public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) { + Preconditions.checkArgument(type != null, "InventoryType cannot be null"); +@@ -2078,13 +2109,28 @@ public final class CraftServer implements Server { + return CraftInventoryCreator.INSTANCE.createInventory(owner, size); + } + ++ // Paper start ++ @Override ++ public Inventory createInventory(InventoryHolder owner, int size, net.kyori.adventure.text.Component title) throws IllegalArgumentException { ++ Preconditions.checkArgument(9 <= size && size <= 54 && size % 9 == 0, "Size for custom inventory must be a multiple of 9 between 9 and 54 slots (got " + size + ")"); ++ return CraftInventoryCreator.INSTANCE.createInventory(owner, size, title); ++ } ++ // Paper end ++ + @Override + public Inventory createInventory(InventoryHolder owner, int size, String title) throws IllegalArgumentException { + Preconditions.checkArgument(9 <= size && size <= 54 && size % 9 == 0, "Size for custom inventory must be a multiple of 9 between 9 and 54 slots (got %s)", size); + return CraftInventoryCreator.INSTANCE.createInventory(owner, size, title); + } + ++ // Paper start ++ @Override ++ public Merchant createMerchant(net.kyori.adventure.text.Component title) { ++ return new org.bukkit.craftbukkit.inventory.CraftMerchantCustom(title == null ? InventoryType.MERCHANT.defaultTitle() : title); ++ } ++ // Paper end + @Override ++ @Deprecated // Paper + public Merchant createMerchant(String title) { + return new CraftMerchantCustom(title == null ? InventoryType.MERCHANT.getDefaultTitle() : title); + } +@@ -2149,6 +2195,17 @@ public final class CraftServer implements Server { + return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog) + } + ++ // Paper start - Adventure ++ @Override ++ public net.kyori.adventure.text.Component motd() { ++ return this.console.motd(); ++ } ++ @Override ++ public void motd(final net.kyori.adventure.text.Component motd) { ++ this.console.motd(motd); ++ } ++ // Paper end ++ + @Override + public String getMotd() { + return this.console.getMotd(); +@@ -2598,4 +2655,57 @@ public final class CraftServer implements Server { + public double[] getTPS() { + return new double[]{0, 0, 0}; // TODO + } ++ ++ // Paper start - adventure sounds ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound) { ++ if (sound.seed().isEmpty()) org.spigotmc.AsyncCatcher.catchOp("play sound; cannot generate seed with world random"); // Paper ++ final long seed = sound.seed().orElseGet(this.console.overworld().getRandom()::nextLong); ++ for (ServerPlayer player : this.playerList.getPlayers()) { ++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null)); ++ } ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound, final double x, final double y, final double z) { ++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper ++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, x, y, z, sound.seed().orElseGet(this.console.overworld().getRandom()::nextLong), this.playSound0(x, y, z, this.console.getAllLevels())); ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound, final net.kyori.adventure.sound.Sound.Emitter emitter) { ++ if (sound.seed().isEmpty()) org.spigotmc.AsyncCatcher.catchOp("play sound; cannot generate seed with world random"); // Paper ++ final long seed = sound.seed().orElseGet(this.console.overworld().getRandom()::nextLong); ++ if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) { ++ for (ServerPlayer player : this.playerList.getPlayers()) { ++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null)); ++ } ++ } else if (emitter instanceof org.bukkit.craftbukkit.entity.CraftEntity craftEntity) { ++ org.spigotmc.AsyncCatcher.catchOp("play sound; cannot use entity emitter"); // Paper ++ final net.minecraft.world.entity.Entity entity = craftEntity.getHandle(); ++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, entity, seed, this.playSound0(entity.getX(), entity.getY(), entity.getZ(), List.of((ServerLevel) entity.level()))); ++ } else { ++ throw new IllegalArgumentException("Sound emitter must be an Entity or self(), but was: " + emitter); ++ } ++ } ++ ++ private java.util.function.BiConsumer<net.minecraft.network.protocol.Packet<?>, Float> playSound0(final double x, final double y, final double z, final Iterable<ServerLevel> levels) { ++ return (packet, distance) -> { ++ for (final ServerLevel level : levels) { ++ level.getServer().getPlayerList().broadcast(null, x, y, z, distance, level.dimension(), packet); ++ } ++ }; ++ } ++ // Paper end ++ ++ // Paper start ++ private Iterable<? extends net.kyori.adventure.audience.Audience> adventure$audiences; ++ @Override ++ public Iterable<? extends net.kyori.adventure.audience.Audience> audiences() { ++ if (this.adventure$audiences == null) { ++ this.adventure$audiences = com.google.common.collect.Iterables.concat(java.util.Collections.singleton(this.getConsoleSender()), this.getOnlinePlayers()); ++ } ++ return this.adventure$audiences; ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index a59eebb89d11788b999d1e5cb4fd2f4e55e023ab..1e8d93d81b7a391bbd1e9926ff35a68d4c563f0f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -162,6 +162,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + private final BlockMetadataStore blockMetadata = new BlockMetadataStore(this); + private final Object2IntOpenHashMap<SpawnCategory> spawnCategoryLimit = new Object2IntOpenHashMap<>(); + private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftWorld.DATA_TYPE_REGISTRY); ++ private net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers + + private static final Random rand = new Random(); + +@@ -1709,6 +1710,42 @@ public class CraftWorld extends CraftRegionAccessor implements World { + entityTracker.broadcastAndSend(packet); + } + } ++ // Paper start - Adventure ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound) { ++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper ++ final long seed = sound.seed().orElseGet(this.world.getRandom()::nextLong); ++ for (ServerPlayer player : this.getHandle().players()) { ++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null)); ++ } ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound, final double x, final double y, final double z) { ++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper ++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, x, y, z, sound.seed().orElseGet(this.world.getRandom()::nextLong), this.playSound0(x, y, z)); ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound, final net.kyori.adventure.sound.Sound.Emitter emitter) { ++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper ++ final long seed = sound.seed().orElseGet(this.getHandle().getRandom()::nextLong); ++ if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) { ++ for (ServerPlayer player : this.getHandle().players()) { ++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null)); ++ } ++ } else if (emitter instanceof CraftEntity craftEntity) { ++ final net.minecraft.world.entity.Entity entity = craftEntity.getHandle(); ++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, entity, seed, this.playSound0(entity.getX(), entity.getY(), entity.getZ())); ++ } else { ++ throw new IllegalArgumentException("Sound emitter must be an Entity or self(), but was: " + emitter); ++ } ++ } ++ ++ private java.util.function.BiConsumer<net.minecraft.network.protocol.Packet<?>, Float> playSound0(final double x, final double y, final double z) { ++ return (packet, distance) -> this.world.getServer().getPlayerList().broadcast(null, x, y, z, distance, this.world.dimension(), packet); ++ } ++ // Paper end + + private static Map<String, GameRules.Key<?>> gamerules; + public static synchronized Map<String, GameRules.Key<?>> getGameRulesNMS() { +@@ -2150,5 +2187,18 @@ public class CraftWorld extends CraftRegionAccessor implements World { + public void setSendViewDistance(final int viewDistance) { + throw new UnsupportedOperationException("Not implemented yet"); + } ++ ++ // Paper start - implement pointers ++ @Override ++ public net.kyori.adventure.pointer.Pointers pointers() { ++ if (this.adventure$pointers == null) { ++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder() ++ .withDynamic(net.kyori.adventure.identity.Identity.NAME, this::getName) ++ .withDynamic(net.kyori.adventure.identity.Identity.UUID, this::getUID) ++ .build(); ++ } ++ ++ return this.adventure$pointers; ++ } + // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 9ec50bbb262b25fea157ae48e8395f5cd38f8906..985b77911d03bc60a0210b796e901f31e2676268 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -20,6 +20,12 @@ public class Main { + public static boolean useConsole = true; + + public static void main(String[] args) { ++ // Paper start ++ final String warnWhenLegacyFormattingDetected = String.join(".", "net", "kyori", "adventure", "text", "warnWhenLegacyFormattingDetected"); ++ if (false && System.getProperty(warnWhenLegacyFormattingDetected) == null) { ++ System.setProperty(warnWhenLegacyFormattingDetected, String.valueOf(true)); ++ } ++ // Paper end + // Todo: Installation script + OptionParser parser = new OptionParser() { + { +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java +index 9ef8b0327e377817faaf58b82a8fdfa5e801eac8..2dfbe061a064b0c79b96f644a1c3639bb900eca4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java +@@ -72,6 +72,19 @@ public class CraftBeacon extends CraftBlockEntityState<BeaconBlockEntity> implem + this.getSnapshot().secondaryPower = (effect != null) ? CraftPotionEffectType.bukkitToMinecraftHolder(effect) : null; + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component customName() { ++ final BeaconBlockEntity be = this.getSnapshot(); ++ return be.name != null ? io.papermc.paper.adventure.PaperAdventure.asAdventure(be.name) : null; ++ } ++ ++ @Override ++ public void customName(final net.kyori.adventure.text.Component customName) { ++ this.getSnapshot().setCustomName(customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null); ++ } ++ // Paper end ++ + @Override + public String getCustomName() { + BeaconBlockEntity beacon = this.getSnapshot(); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java +index f9b89a7c6ac9f7fdbd29567a5b6550398dbc7345..f5b0bec4c1164fe7ef6da1f19a6ce9bb3d6864d0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java +@@ -45,4 +45,16 @@ public class CraftCommandBlock extends CraftBlockEntityState<CommandBlockEntity> + public CraftCommandBlock copy(Location location) { + return new CraftCommandBlock(this, location); + } ++ ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component name() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(getSnapshot().getCommandBlock().getName()); ++ } ++ ++ @Override ++ public void name(net.kyori.adventure.text.Component name) { ++ getSnapshot().getCommandBlock().setCustomName(name == null ? net.minecraft.network.chat.Component.literal("@") : io.papermc.paper.adventure.PaperAdventure.asVanilla(name)); ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java +index 007b6d66dd837ca6352c0fba5c2399139f6b5425..513402b61e6b8388b7bc163d98e54ffae0e18254 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java +@@ -32,6 +32,19 @@ public abstract class CraftContainer<T extends BaseContainerBlockEntity> extends + this.getSnapshot().lockKey = (key == null) ? LockCode.NO_LOCK : new LockCode(key); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component customName() { ++ final T be = this.getSnapshot(); ++ return be.hasCustomName() ? io.papermc.paper.adventure.PaperAdventure.asAdventure(be.getCustomName()) : null; ++ } ++ ++ @Override ++ public void customName(final net.kyori.adventure.text.Component customName) { ++ this.getSnapshot().name = (customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null); ++ } ++ // Paper end ++ + @Override + public String getCustomName() { + T container = this.getSnapshot(); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java b/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java +index 690dd79b82108322a290c00de63b1f038f617c84..a01691f98a378a818b8bf12176c7270e15c316d8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java +@@ -16,6 +16,19 @@ public class CraftEnchantingTable extends CraftBlockEntityState<EnchantingTableB + super(state, location); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component customName() { ++ final EnchantingTableBlockEntity be = this.getSnapshot(); ++ return be.hasCustomName() ? io.papermc.paper.adventure.PaperAdventure.asAdventure(be.getCustomName()) : null; ++ } ++ ++ @Override ++ public void customName(final net.kyori.adventure.text.Component customName) { ++ this.getSnapshot().setCustomName(customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null); ++ } ++ // Paper end ++ + @Override + public String getCustomName() { + EnchantingTableBlockEntity enchant = this.getSnapshot(); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java b/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java +index 0641d17d8095e7700c651e90472ae53d6f7dbbb2..a12702cdf36c75572e661b5b5758270f5058c181 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java +@@ -36,6 +36,23 @@ public class CraftSign<T extends SignBlockEntity> extends CraftBlockEntityState< + this.back = new CraftSignSide(this.getSnapshot().getBackText()); + } + ++ // Paper start ++ @Override ++ public java.util.@NotNull List<net.kyori.adventure.text.Component> lines() { ++ return this.front.lines(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.@NotNull Component line(int index) { ++ return this.front.line(index); ++ } ++ ++ @Override ++ public void line(int index, net.kyori.adventure.text.@NotNull Component line) { ++ this.front.line(index, line); ++ } ++ // Paper end ++ + @Override + public String[] getLines() { + return this.front.getLines(); +@@ -161,6 +178,20 @@ public class CraftSign<T extends SignBlockEntity> extends CraftBlockEntityState< + ((CraftPlayer) player).getHandle().openTextEdit(handle, Side.FRONT == side); + } + ++ // Paper start ++ public static Component[] sanitizeLines(java.util.List<? extends net.kyori.adventure.text.Component> lines) { ++ Component[] components = new Component[4]; ++ for (int i = 0; i < 4; i++) { ++ if (i < lines.size() && lines.get(i) != null) { ++ components[i] = io.papermc.paper.adventure.PaperAdventure.asVanilla(lines.get(i)); ++ } else { ++ components[i] = net.minecraft.network.chat.Component.literal(""); ++ } ++ } ++ return components; ++ } ++ // Paper end ++ + public static Component[] sanitizeLines(String[] lines) { + Component[] components = new Component[4]; + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java b/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java +index d4724c812f8b7322ad59dc0695d01ceb47772dc4..4747d77fd5fd12116ef40d897a08c7baca60a399 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java +@@ -12,37 +12,70 @@ import org.jetbrains.annotations.Nullable; + public class CraftSignSide implements SignSide { + + // Lazily initialized only if requested: +- private String[] originalLines = null; +- private String[] lines = null; ++ // Paper start ++ private java.util.ArrayList<net.kyori.adventure.text.Component> originalLines = null; // ArrayList for RandomAccess ++ private java.util.ArrayList<net.kyori.adventure.text.Component> lines = null; // ArrayList for RandomAccess ++ // Paper end + private SignText signText; + + public CraftSignSide(SignText signText) { + this.signText = signText; + } + ++ // Paper start ++ @Override ++ public java.util.@NotNull List<net.kyori.adventure.text.Component> lines() { ++ this.loadLines(); ++ return this.lines; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.@NotNull Component line(final int index) throws IndexOutOfBoundsException { ++ this.loadLines(); ++ return this.lines.get(index); ++ } ++ ++ @Override ++ public void line(final int index, final net.kyori.adventure.text.@NotNull Component line) throws IndexOutOfBoundsException { ++ com.google.common.base.Preconditions.checkArgument(line != null, "Line cannot be null"); ++ this.loadLines(); ++ this.lines.set(index, line); ++ } ++ ++ private void loadLines() { ++ if (this.lines != null) { ++ return; ++ } ++ // Lazy initialization: ++ this.lines = io.papermc.paper.adventure.PaperAdventure.asAdventure(com.google.common.collect.Lists.newArrayList(this.signText.getMessages(false))); ++ this.originalLines = new java.util.ArrayList<>(this.lines); ++ } ++ // Paper end ++ + @NotNull + @Override + public String[] getLines() { +- if (this.lines == null) { +- // Lazy initialization: +- Component[] messages = this.signText.getMessages(false); +- this.lines = new String[messages.length]; +- System.arraycopy(CraftSign.revertComponents(messages), 0, this.lines, 0, this.lines.length); +- this.originalLines = new String[this.lines.length]; +- System.arraycopy(this.lines, 0, this.originalLines, 0, this.originalLines.length); +- } +- return this.lines; ++ // Paper start ++ this.loadLines(); ++ return this.lines.stream().map(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()::serialize).toArray(String[]::new); // Paper ++ // Paper end + } + + @NotNull + @Override + public String getLine(int index) throws IndexOutOfBoundsException { +- return this.getLines()[index]; ++ // Paper start ++ this.loadLines(); ++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.lines.get(index)); ++ // Paper end + } + + @Override + public void setLine(int index, @NotNull String line) throws IndexOutOfBoundsException { +- this.getLines()[index] = line; ++ // Paper start ++ this.loadLines(); ++ this.lines.set(index, line != null ? net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(line) : net.kyori.adventure.text.Component.empty()); ++ // Paper end + } + + @Override +@@ -68,13 +101,16 @@ public class CraftSignSide implements SignSide { + + public SignText applyLegacyStringToSignSide() { + if (this.lines != null) { +- for (int i = 0; i < this.lines.length; i++) { +- String line = (this.lines[i] == null) ? "" : this.lines[i]; +- if (line.equals(this.originalLines[i])) { ++ // Paper start ++ for (int i = 0; i < this.lines.size(); ++i) { ++ net.kyori.adventure.text.Component component = this.lines.get(i); ++ net.kyori.adventure.text.Component origComp = this.originalLines.get(i); ++ if (component.equals(origComp)) { + continue; // The line contents are still the same, skip. + } +- this.signText = this.signText.setMessage(i, CraftChatMessage.fromString(line)[0]); ++ this.signText = this.signText.setMessage(i, io.papermc.paper.adventure.PaperAdventure.asVanilla(component)); + } ++ // Paper end + } + + return this.signText; +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java +index f2e8b63d787754c0a92441dcc9eb39dffdc1e280..9feae61fe02cbc624581ef0bd4c60af636407367 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java +@@ -61,6 +61,18 @@ public class CraftBlockCommandSender extends ServerCommandSender implements Bloc + return this.block.getTextName(); + } + ++ // Paper start ++ @Override ++ public void sendMessage(net.kyori.adventure.identity.Identity identity, net.kyori.adventure.text.Component message, net.kyori.adventure.audience.MessageType type) { ++ block.source.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component name() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.block.getDisplayName()); ++ } ++ // Paper end ++ + @Override + public boolean isOp() { + return CraftBlockCommandSender.SHARED_PERM.isOp(); +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +index f3cb4102ab223f379f60dac317df7da1fab812a8..324e6d1a4fadd3e557e4ba05f04e6a5891cc54df 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +@@ -46,6 +46,13 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + return "CONSOLE"; + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component name() { ++ return net.kyori.adventure.text.Component.text(this.getName()); ++ } ++ // Paper end ++ + @Override + public boolean isOp() { + return true; +@@ -80,4 +87,11 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + public boolean isConversing() { + return this.conversationTracker.isConversing(); + } ++ ++ // 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)); ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java +index e3c7fa50fad3077a297d2412de9d26d53371808c..5b7d230103f421fb939072e1526854f715430e51 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java +@@ -39,6 +39,13 @@ public class CraftRemoteConsoleCommandSender extends ServerCommandSender impleme + return "Rcon"; + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component name() { ++ return net.kyori.adventure.text.Component.text(this.getName()); ++ } ++ // Paper end ++ + @Override + public boolean isOp() { + return true; +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java +index 53d6950ad270ba901de5226b9daecb683248ad05..3e7d14564f11a3ed0b0766444e9d681804597e9a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java +@@ -67,6 +67,13 @@ public class ProxiedNativeCommandSender implements ProxiedCommandSender { + return this.getCallee().getName(); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component name() { ++ return this.getCallee().name(); ++ } ++ // Paper end ++ + @Override + public boolean isPermissionSet(String name) { + return this.getCaller().isPermissionSet(name); +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java +index 1e82312c24cb752d63b165926861fc178cd7849b..7f22950ae61436e91a59cd29a345809c42bbe739 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java +@@ -13,6 +13,7 @@ import org.bukkit.plugin.Plugin; + + public abstract class ServerCommandSender implements CommandSender { + private final PermissibleBase perm; ++ private net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers + + protected ServerCommandSender() { + this.perm = new PermissibleBase(this); +@@ -130,4 +131,18 @@ public abstract class ServerCommandSender implements CommandSender { + return this.spigot; + } + // Spigot end ++ ++ // Paper start - implement pointers ++ @Override ++ public net.kyori.adventure.pointer.Pointers pointers() { ++ if (this.adventure$pointers == null) { ++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder() ++ .withDynamic(net.kyori.adventure.identity.Identity.DISPLAY_NAME, this::name) ++ .withStatic(net.kyori.adventure.permission.PermissionChecker.POINTER, this::permissionValue) ++ .build(); ++ } ++ ++ return this.adventure$pointers; ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java +index ed6944418382dbd02e5ddbf50c6ac4bb26415807..97a7dc31fe7a2defed2202ccc518c66b7ff4b0da 100644 +--- a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java ++++ b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java +@@ -166,6 +166,12 @@ public class CraftEnchantment extends Enchantment implements Handleable<net.mine + CraftEnchantment ench = (CraftEnchantment) other; + return !net.minecraft.world.item.enchantment.Enchantment.areCompatible(this.handle, ench.handle); + } ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component displayName(int level) { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(getHandle().getFullname(level)); ++ } ++ // Paper end + + @Override + public String getTranslationKey() { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index d09102fe44dffa61dff883488f47715effaa8211..269326e7689eba91bcfd3475006e8cbf8f5694ef 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -69,6 +69,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + private final EntityType entityType; + private EntityDamageEvent lastDamageEvent; + private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftEntity.DATA_TYPE_REGISTRY); ++ protected net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers + + public CraftEntity(final CraftServer server, final Entity entity) { + this.server = server; +@@ -525,6 +526,32 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return this.getHandle().getVehicle().getBukkitEntity(); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component customName() { ++ final Component name = this.getHandle().getCustomName(); ++ return name != null ? io.papermc.paper.adventure.PaperAdventure.asAdventure(name) : null; ++ } ++ ++ @Override ++ public void customName(final net.kyori.adventure.text.Component customName) { ++ this.getHandle().setCustomName(customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null); ++ } ++ ++ @Override ++ public net.kyori.adventure.pointer.Pointers pointers() { ++ if (this.adventure$pointers == null) { ++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder() ++ .withDynamic(net.kyori.adventure.identity.Identity.DISPLAY_NAME, this::name) ++ .withDynamic(net.kyori.adventure.identity.Identity.UUID, this::getUniqueId) ++ .withStatic(net.kyori.adventure.permission.PermissionChecker.POINTER, this::permissionValue) ++ .build(); ++ } ++ ++ return this.adventure$pointers; ++ } ++ // Paper end ++ + @Override + public void setCustomName(String name) { + // sane limit for name length +@@ -621,6 +648,17 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + public String getName() { + return CraftChatMessage.fromComponent(this.getHandle().getName()); + } ++ // Paper start ++ @Override ++ public [email protected] Component name() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getName()); ++ } ++ ++ @Override ++ public [email protected] Component teamDisplayName() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getDisplayName()); ++ } ++ // Paper end + + @Override + public boolean isPermissionSet(String name) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +index 8ba24b389a16a7f4ec4dcac87f2acfd560d65c34..1010ac55d91559bcb6ebadeea447f32c6aa89226 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +@@ -321,9 +321,12 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { + container = CraftEventFactory.callInventoryOpenEvent(player, container); + if (container == null) return; + +- String title = container.getBukkitView().getTitle(); ++ //String title = container.getBukkitView().getTitle(); // Paper - comment ++ net.kyori.adventure.text.Component adventure$title = container.getBukkitView().title(); // Paper ++ if (adventure$title == null) adventure$title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(container.getBukkitView().getTitle()); // Paper + +- player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0])); ++ //player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0])); // Paper - comment ++ player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, io.papermc.paper.adventure.PaperAdventure.asVanilla(adventure$title))); // Paper + player.containerMenu = container; + player.initMenu(container); + } +@@ -392,8 +395,12 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { + + // Now open the window + MenuType<?> windowType = CraftContainer.getNotchInventoryType(inventory.getTopInventory()); +- String title = inventory.getTitle(); +- player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0])); ++ ++ //String title = inventory.getTitle(); // Paper - comment ++ net.kyori.adventure.text.Component adventure$title = inventory.title(); // Paper ++ if (adventure$title == null) adventure$title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(inventory.getTitle()); // Paper ++ //player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0])); // Paper - comment ++ player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, io.papermc.paper.adventure.PaperAdventure.asVanilla(adventure$title))); // Paper + player.containerMenu = container; + player.initMenu(container); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java +index 55945b83a5426b352bad9507cc9e94afb1278032..9ea1537408ff2d790747b6e5a681d9171a4233ae 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java +@@ -59,6 +59,13 @@ public class CraftMinecartCommand extends CraftMinecart implements CommandMineca + return CraftChatMessage.fromComponent(this.getHandle().getCommandBlock().getName()); + } + ++ // Paper start ++ @Override ++ public [email protected] Component name() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getCommandBlock().getName()); ++ } ++ // Paper end ++ + @Override + public boolean isOp() { + return true; +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index c025542b1073ce7e6e30a43744276fd1fa1ca484..64aed98084aeb3f29db301adf3c8c49ee9236a0b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -388,14 +388,40 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public String getDisplayName() { ++ if(true) return io.papermc.paper.adventure.DisplayNames.getLegacy(this); // Paper + return this.getHandle().displayName; + } + + @Override + public void setDisplayName(final String name) { ++ this.getHandle().adventure$displayName = name != null ? net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(name) : net.kyori.adventure.text.Component.text(this.getName()); // Paper + this.getHandle().displayName = name == null ? this.getName() : name; + } + ++ // Paper start ++ @Override ++ public void playerListName(net.kyori.adventure.text.Component name) { ++ getHandle().listName = name == null ? null : io.papermc.paper.adventure.PaperAdventure.asVanilla(name); ++ if (getHandle().connection == null) return; // Updates are possible before the player has fully joined ++ for (ServerPlayer player : server.getHandle().players) { ++ if (player.getBukkitEntity().canSee(this)) { ++ player.connection.send(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, getHandle())); ++ } ++ } ++ } ++ @Override ++ public net.kyori.adventure.text.Component playerListName() { ++ return getHandle().listName == null ? net.kyori.adventure.text.Component.text(getName()) : io.papermc.paper.adventure.PaperAdventure.asAdventure(getHandle().listName); ++ } ++ @Override ++ public net.kyori.adventure.text.Component playerListHeader() { ++ return playerListHeader; ++ } ++ @Override ++ public net.kyori.adventure.text.Component playerListFooter() { ++ return playerListFooter; ++ } ++ // Paper end + @Override + public String getPlayerListName() { + return this.getHandle().listName == null ? this.getName() : CraftChatMessage.fromComponent(this.getHandle().listName); +@@ -407,6 +433,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + name = this.getName(); + } + this.getHandle().listName = name.equals(this.getName()) ? null : CraftChatMessage.fromStringOrNull(name); ++ if (this.getHandle().connection == null) return; // Paper - Updates are possible before the player has fully joined + for (ServerPlayer player : (List<ServerPlayer>) this.server.getHandle().players) { + if (player.getBukkitEntity().canSee(this)) { + player.connection.send(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, this.getHandle())); +@@ -414,42 +441,42 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + } + +- private Component playerListHeader; +- private Component playerListFooter; ++ private net.kyori.adventure.text.Component playerListHeader; // Paper - Adventure ++ private net.kyori.adventure.text.Component playerListFooter; // Paper - Adventure + + @Override + public String getPlayerListHeader() { +- return (this.playerListHeader == null) ? null : CraftChatMessage.fromComponent(this.playerListHeader); ++ return (this.playerListHeader == null) ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.playerListHeader); + } + + @Override + public String getPlayerListFooter() { +- return (this.playerListFooter == null) ? null : CraftChatMessage.fromComponent(this.playerListFooter); ++ return (this.playerListFooter == null) ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.playerListFooter); // Paper - Adventure + } + + @Override + public void setPlayerListHeader(String header) { +- this.playerListHeader = CraftChatMessage.fromStringOrNull(header, true); ++ this.playerListHeader = header == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(header); // Paper - Adventure + this.updatePlayerListHeaderFooter(); + } + + @Override + public void setPlayerListFooter(String footer) { +- this.playerListFooter = CraftChatMessage.fromStringOrNull(footer, true); ++ this.playerListFooter = footer == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(footer); // Paper - Adventure + this.updatePlayerListHeaderFooter(); + } + + @Override + public void setPlayerListHeaderFooter(String header, String footer) { +- this.playerListHeader = CraftChatMessage.fromStringOrNull(header, true); +- this.playerListFooter = CraftChatMessage.fromStringOrNull(footer, true); ++ this.playerListHeader = header == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(header); // Paper - Adventure ++ this.playerListFooter = footer == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(footer); // Paper - Adventure + this.updatePlayerListHeaderFooter(); + } + + private void updatePlayerListHeaderFooter() { + if (this.getHandle().connection == null) return; + +- ClientboundTabListPacket packet = new ClientboundTabListPacket((this.playerListHeader == null) ? Component.empty() : this.playerListHeader, (this.playerListFooter == null) ? Component.empty() : this.playerListFooter); ++ ClientboundTabListPacket packet = new ClientboundTabListPacket(io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListHeader), io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListFooter)); // Paper - adventure + this.getHandle().connection.send(packet); + } + +@@ -481,6 +508,23 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + this.getHandle().connection.disconnect(CraftChatMessage.fromStringOrEmpty(message)); + } + ++ // Paper start ++ private static final net.kyori.adventure.text.Component DEFAULT_KICK_COMPONENT = net.kyori.adventure.text.Component.translatable("multiplayer.disconnect.kicked"); ++ @Override ++ public void kick() { ++ this.kick(DEFAULT_KICK_COMPONENT); ++ } ++ ++ @Override ++ public void kick(final net.kyori.adventure.text.Component message) { ++ org.spigotmc.AsyncCatcher.catchOp("player kick"); ++ final ServerGamePacketListenerImpl connection = this.getHandle().connection; ++ if (connection != null) { ++ connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message); ++ } ++ } ++ // Paper end ++ + @Override + public void setCompassTarget(Location loc) { + Preconditions.checkArgument(loc != null, "Location cannot be null"); +@@ -777,6 +821,24 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + this.getHandle().connection.send(packet); + } + ++ // Paper start ++ @Override ++ public void sendSignChange(Location loc, @Nullable List<? extends net.kyori.adventure.text.Component> lines, DyeColor dyeColor, boolean hasGlowingText) { ++ if (getHandle().connection == null) { ++ return; ++ } ++ if (lines == null) { ++ lines = new java.util.ArrayList<>(4); ++ } ++ Preconditions.checkArgument(loc != null, "Location cannot be null"); ++ Preconditions.checkArgument(dyeColor != null, "DyeColor cannot be null"); ++ if (lines.size() < 4) { ++ throw new IllegalArgumentException("Must have at least 4 lines"); ++ } ++ Component[] components = CraftSign.sanitizeLines(lines); ++ this.sendSignChange0(components, loc, dyeColor, hasGlowingText); ++ } ++ // Paper end + @Override + public void sendSignChange(Location loc, String[] lines) { + this.sendSignChange(loc, lines, DyeColor.BLACK); +@@ -800,6 +862,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + if (this.getHandle().connection == null) return; + + Component[] components = CraftSign.sanitizeLines(lines); ++ // Paper start - adventure ++ this.sendSignChange0(components, loc, dyeColor, hasGlowingText); ++ } ++ ++ private void sendSignChange0(Component[] components, Location loc, DyeColor dyeColor, boolean hasGlowingText) { ++ // Paper end + SignBlockEntity sign = new SignBlockEntity(CraftLocation.toBlockPosition(loc), Blocks.OAK_SIGN.defaultBlockState()); + SignText text = sign.getFrontText(); + text = text.setColor(net.minecraft.world.item.DyeColor.byId(dyeColor.getWoolData())); +@@ -1815,7 +1883,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setResourcePack(String url) { +- this.setResourcePack(url, null); ++ this.setResourcePack(url, (byte[]) null); + } + + @Override +@@ -1830,7 +1898,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setResourcePack(String url, byte[] hash, boolean force) { +- this.setResourcePack(url, hash, null, force); ++ this.setResourcePack(url, hash, (String) null, force); + } + + @Override +@@ -1867,6 +1935,59 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + this.handlePushResourcePack(new ClientboundResourcePackPushPacket(id, url, hashStr, force, CraftChatMessage.fromStringOrOptional(prompt, true)), false); + } + ++ // Paper start - adventure ++ @Override ++ public void setResourcePack(final UUID uuid, final String url, final byte[] hashBytes, final net.kyori.adventure.text.Component prompt, final boolean force) { ++ Preconditions.checkArgument(uuid != null, "Resource pack UUID cannot be null"); ++ Preconditions.checkArgument(url != null, "Resource pack URL cannot be null"); ++ final String hash; ++ if (hashBytes != null) { ++ Preconditions.checkArgument(hashBytes.length == 20, "Resource pack hash should be 20 bytes long but was " + hashBytes.length); ++ hash = BaseEncoding.base16().lowerCase().encode(hashBytes); ++ } else { ++ hash = ""; ++ } ++ this.getHandle().connection.send(new ClientboundResourcePackPopPacket(Optional.empty())); ++ this.getHandle().connection.send(new ClientboundResourcePackPushPacket(uuid, url, hash, force, Optional.ofNullable(prompt).map(io.papermc.paper.adventure.PaperAdventure::asVanilla))); ++ } ++ ++ @SuppressWarnings({"unchecked", "rawtypes"}) ++ void sendBundle(final List<? extends net.minecraft.network.protocol.Packet<? extends net.minecraft.network.protocol.common.ClientCommonPacketListener>> packet) { ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket((List) packet)); ++ } ++ ++ @Override ++ public void sendResourcePacks(final net.kyori.adventure.resource.ResourcePackRequest request) { ++ if (this.getHandle().connection == null) return; ++ final List<ClientboundResourcePackPushPacket> packs = new java.util.ArrayList<>(request.packs().size()); ++ if (request.replace()) { ++ this.clearResourcePacks(); ++ } ++ final Component prompt = io.papermc.paper.adventure.PaperAdventure.asVanilla(request.prompt()); ++ for (final java.util.Iterator<net.kyori.adventure.resource.ResourcePackInfo> iter = request.packs().iterator(); iter.hasNext();) { ++ final net.kyori.adventure.resource.ResourcePackInfo pack = iter.next(); ++ packs.add(new ClientboundResourcePackPushPacket(pack.id(), pack.uri().toASCIIString(), pack.hash(), request.required(), iter.hasNext() ? Optional.empty() : Optional.ofNullable(prompt))); ++ if (request.callback() != net.kyori.adventure.resource.ResourcePackCallback.noOp()) { ++ this.getHandle().connection.packCallbacks.put(pack.id(), request.callback()); // just override if there is a previously existing callback ++ } ++ } ++ this.sendBundle(packs); ++ super.sendResourcePacks(request); ++ } ++ ++ @Override ++ public void removeResourcePacks(final UUID id, final UUID ... others) { ++ if (this.getHandle().connection == null) return; ++ this.sendBundle(net.kyori.adventure.util.MonkeyBars.nonEmptyArrayToList(pack -> new ClientboundResourcePackPopPacket(Optional.of(pack)), id, others)); ++ } ++ ++ @Override ++ public void clearResourcePacks() { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new ClientboundResourcePackPopPacket(Optional.empty())); ++ } ++ // Paper end - adventure ++ + @Override + public void removeResourcePack(UUID id) { + Preconditions.checkArgument(id != null, "Resource pack id cannot be null"); +@@ -2287,6 +2408,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return (this.getHandle().requestedViewDistance() == 0) ? Bukkit.getViewDistance() : this.getHandle().requestedViewDistance(); + } + ++ // Paper start ++ @Override ++ public java.util.Locale locale() { ++ return getHandle().adventure$locale; ++ } ++ // Paper end + @Override + public int getPing() { + return this.getHandle().connection.latency(); +@@ -2337,6 +2464,248 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.getHandle().allowsListing(); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component displayName() { ++ return this.getHandle().adventure$displayName; ++ } ++ ++ @Override ++ public void displayName(final net.kyori.adventure.text.Component displayName) { ++ this.getHandle().adventure$displayName = displayName != null ? displayName : net.kyori.adventure.text.Component.text(this.getName()); ++ this.getHandle().displayName = null; ++ } ++ ++ @Override ++ public void deleteMessage(net.kyori.adventure.chat.SignedMessage.Signature signature) { ++ if (getHandle().connection == null) return; ++ net.minecraft.network.chat.MessageSignature sig = new net.minecraft.network.chat.MessageSignature(signature.bytes()); ++ ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundDeleteChatPacket(new net.minecraft.network.chat.MessageSignature.Packed(sig))); ++ } ++ ++ private net.minecraft.network.chat.ChatType.Bound toHandle(net.kyori.adventure.chat.ChatType.Bound boundChatType) { ++ net.minecraft.core.Registry<net.minecraft.network.chat.ChatType> chatTypeRegistry = this.getHandle().level().registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.CHAT_TYPE); ++ ++ return new net.minecraft.network.chat.ChatType.Bound( ++ chatTypeRegistry.getHolderOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.CHAT_TYPE, io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.type().key()))), ++ io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.name()), ++ Optional.ofNullable(io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.target())) ++ ); ++ } ++ ++ @Override ++ public void sendMessage(net.kyori.adventure.text.Component message, net.kyori.adventure.chat.ChatType.Bound boundChatType) { ++ if (getHandle().connection == null) return; ++ ++ net.minecraft.network.chat.Component component = io.papermc.paper.adventure.PaperAdventure.asVanilla(message); ++ this.getHandle().sendChatMessage(new net.minecraft.network.chat.OutgoingChatMessage.Disguised(component), this.getHandle().isTextFilteringEnabled(), this.toHandle(boundChatType)); ++ } ++ ++ @Override ++ public void sendMessage(net.kyori.adventure.chat.SignedMessage signedMessage, net.kyori.adventure.chat.ChatType.Bound boundChatType) { ++ if (getHandle().connection == null) return; ++ ++ if (signedMessage instanceof PlayerChatMessage.AdventureView view) { ++ this.getHandle().sendChatMessage(net.minecraft.network.chat.OutgoingChatMessage.create(view.playerChatMessage()), this.getHandle().isTextFilteringEnabled(), this.toHandle(boundChatType)); ++ return; ++ } ++ net.kyori.adventure.text.Component message = signedMessage.unsignedContent() == null ? net.kyori.adventure.text.Component.text(signedMessage.message()) : signedMessage.unsignedContent(); ++ if (signedMessage.isSystem()) { ++ this.sendMessage(message, boundChatType); ++ } else { ++ super.sendMessage(signedMessage, boundChatType); ++ } ++// net.minecraft.network.chat.PlayerChatMessage playerChatMessage = new net.minecraft.network.chat.PlayerChatMessage( ++// null, // TODO: ++// new net.minecraft.network.chat.MessageSignature(signedMessage.signature().bytes()), ++// null, // TODO ++// io.papermc.paper.adventure.PaperAdventure.asVanilla(signedMessage.unsignedContent()), ++// net.minecraft.network.chat.FilterMask.PASS_THROUGH ++// ); ++// ++// this.getHandle().sendChatMessage(net.minecraft.network.chat.OutgoingChatMessage.create(playerChatMessage), this.getHandle().isTextFilteringEnabled(), this.toHandle(boundChatType)); ++ } ++ ++ @Deprecated(forRemoval = true) ++ @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) { ++ if (getHandle().connection == null) return; ++ final net.minecraft.core.Registry<net.minecraft.network.chat.ChatType> chatTypeRegistry = this.getHandle().level().registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.CHAT_TYPE); ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundSystemChatPacket(message, false)); ++ } ++ ++ @Override ++ public void sendActionBar(final net.kyori.adventure.text.Component message) { ++ final net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket packet = new net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(message)); ++ this.getHandle().connection.send(packet); ++ } ++ ++ @Override ++ public void sendPlayerListHeader(final net.kyori.adventure.text.Component header) { ++ this.playerListHeader = header; ++ this.adventure$sendPlayerListHeaderAndFooter(); ++ } ++ ++ @Override ++ public void sendPlayerListFooter(final net.kyori.adventure.text.Component footer) { ++ this.playerListFooter = footer; ++ this.adventure$sendPlayerListHeaderAndFooter(); ++ } ++ ++ @Override ++ public void sendPlayerListHeaderAndFooter(final net.kyori.adventure.text.Component header, final net.kyori.adventure.text.Component footer) { ++ this.playerListHeader = header; ++ this.playerListFooter = footer; ++ this.adventure$sendPlayerListHeaderAndFooter(); ++ } ++ ++ private void adventure$sendPlayerListHeaderAndFooter() { ++ final ServerGamePacketListenerImpl connection = this.getHandle().connection; ++ if (connection == null) return; ++ final ClientboundTabListPacket packet = new ClientboundTabListPacket( ++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListHeader), ++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListFooter) ++ ); ++ connection.send(packet); ++ } ++ ++ @Override ++ public void showTitle(final net.kyori.adventure.title.Title title) { ++ final ServerGamePacketListenerImpl connection = this.getHandle().connection; ++ final net.kyori.adventure.title.Title.Times times = title.times(); ++ if (times != null) { ++ connection.send(new ClientboundSetTitlesAnimationPacket(ticks(times.fadeIn()), ticks(times.stay()), ticks(times.fadeOut()))); ++ } ++ final ClientboundSetSubtitleTextPacket sp = new ClientboundSetSubtitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(title.subtitle())); ++ connection.send(sp); ++ final ClientboundSetTitleTextPacket tp = new ClientboundSetTitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(title.title())); ++ connection.send(tp); ++ } ++ ++ @Override ++ public <T> void sendTitlePart(final net.kyori.adventure.title.TitlePart<T> part, T value) { ++ java.util.Objects.requireNonNull(part, "part"); ++ java.util.Objects.requireNonNull(value, "value"); ++ if (part == net.kyori.adventure.title.TitlePart.TITLE) { ++ final ClientboundSetTitleTextPacket tp = new ClientboundSetTitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla((net.kyori.adventure.text.Component)value)); ++ this.getHandle().connection.send(tp); ++ } else if (part == net.kyori.adventure.title.TitlePart.SUBTITLE) { ++ final ClientboundSetSubtitleTextPacket sp = new ClientboundSetSubtitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla((net.kyori.adventure.text.Component)value)); ++ this.getHandle().connection.send(sp); ++ } else if (part == net.kyori.adventure.title.TitlePart.TIMES) { ++ final net.kyori.adventure.title.Title.Times times = (net.kyori.adventure.title.Title.Times) value; ++ this.getHandle().connection.send(new ClientboundSetTitlesAnimationPacket(ticks(times.fadeIn()), ticks(times.stay()), ticks(times.fadeOut()))); ++ } else { ++ throw new IllegalArgumentException("Unknown TitlePart"); ++ } ++ } ++ ++ private static int ticks(final java.time.Duration duration) { ++ if (duration == null) { ++ return -1; ++ } ++ return (int) (duration.toMillis() / 50L); ++ } ++ ++ @Override ++ public void clearTitle() { ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundClearTitlesPacket(false)); ++ } ++ ++ // resetTitle implemented above ++ ++ private @Nullable Set<net.kyori.adventure.bossbar.BossBar> activeBossBars; ++ ++ @Override ++ public @NotNull Iterable<? extends net.kyori.adventure.bossbar.BossBar> activeBossBars() { ++ if (this.activeBossBars != null) { ++ return java.util.Collections.unmodifiableSet(this.activeBossBars); ++ } ++ return Set.of(); ++ } ++ ++ @Override ++ public void showBossBar(final net.kyori.adventure.bossbar.BossBar bar) { ++ net.kyori.adventure.bossbar.BossBarImplementation.get(bar, io.papermc.paper.adventure.BossBarImplementationImpl.class).playerShow(this); ++ if (this.activeBossBars == null) { ++ this.activeBossBars = new HashSet<>(); ++ } ++ this.activeBossBars.add(bar); ++ } ++ ++ @Override ++ public void hideBossBar(final net.kyori.adventure.bossbar.BossBar bar) { ++ net.kyori.adventure.bossbar.BossBarImplementation.get(bar, io.papermc.paper.adventure.BossBarImplementationImpl.class).playerHide(this); ++ if (this.activeBossBars != null) { ++ this.activeBossBars.remove(bar); ++ if (this.activeBossBars.isEmpty()) { ++ this.activeBossBars = null; ++ } ++ } ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound) { ++ final Vec3 pos = this.getHandle().position(); ++ this.playSound(sound, pos.x, pos.y, pos.z); ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound, final double x, final double y, final double z) { ++ this.getHandle().connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, x, y, z, sound.seed().orElseGet(this.getHandle().getRandom()::nextLong), null)); ++ } ++ ++ @Override ++ public void playSound(final net.kyori.adventure.sound.Sound sound, final net.kyori.adventure.sound.Sound.Emitter emitter) { ++ final Entity entity; ++ if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) { ++ entity = this.getHandle(); ++ } else if (emitter instanceof CraftEntity craftEntity) { ++ entity = craftEntity.getHandle(); ++ } else { ++ throw new IllegalArgumentException("Sound emitter must be an Entity or self(), but was: " + emitter); ++ } ++ this.getHandle().connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, entity, sound.seed().orElseGet(this.getHandle().getRandom()::nextLong), null)); ++ } ++ ++ @Override ++ public void stopSound(final net.kyori.adventure.sound.SoundStop stop) { ++ this.getHandle().connection.send(new ClientboundStopSoundPacket( ++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullable(stop.sound()), ++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullable(stop.source()) ++ )); ++ } ++ ++ @Override ++ public void openBook(final net.kyori.adventure.inventory.Book book) { ++ final java.util.Locale locale = this.getHandle().adventure$locale; ++ final net.minecraft.world.item.ItemStack item = io.papermc.paper.adventure.PaperAdventure.asItemStack(book, locale); ++ final ServerPlayer player = this.getHandle(); ++ final ServerGamePacketListenerImpl connection = player.connection; ++ final net.minecraft.world.entity.player.Inventory inventory = player.getInventory(); ++ final int slot = inventory.items.size() + inventory.selected; ++ final int stateId = getHandle().containerMenu.getStateId(); ++ connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(0, stateId, slot, item)); ++ connection.send(new net.minecraft.network.protocol.game.ClientboundOpenBookPacket(net.minecraft.world.InteractionHand.MAIN_HAND)); ++ connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(0, stateId, slot, inventory.getSelected())); ++ } ++ ++ @Override ++ public net.kyori.adventure.pointer.Pointers pointers() { ++ if (this.adventure$pointers == null) { ++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder() ++ .withDynamic(net.kyori.adventure.identity.Identity.DISPLAY_NAME, this::displayName) ++ .withDynamic(net.kyori.adventure.identity.Identity.NAME, this::getName) ++ .withDynamic(net.kyori.adventure.identity.Identity.UUID, this::getUniqueId) ++ .withStatic(net.kyori.adventure.permission.PermissionChecker.POINTER, this::permissionValue) ++ .withDynamic(net.kyori.adventure.identity.Identity.LOCALE, this::locale) ++ .build(); ++ } ++ ++ return this.adventure$pointers; ++ } ++ // Paper end + // Spigot start + private final Player.Spigot spigot = new Player.Spigot() + { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java +index 5725b0281ac53a2354b233223259d6784353bc6e..9ef939b76d06874b856e0c850addb364146f5a00 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java +@@ -32,6 +32,17 @@ public class CraftTextDisplay extends CraftDisplay implements TextDisplay { + public void setText(String text) { + this.getHandle().setText(CraftChatMessage.fromString(text, true)[0]); + } ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component text() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getText()); ++ } ++ ++ @Override ++ public void text(net.kyori.adventure.text.Component text) { ++ this.getHandle().setText(text == null ? net.minecraft.network.chat.Component.empty() : io.papermc.paper.adventure.PaperAdventure.asVanilla(text)); ++ } ++ // Paper end + + @Override + public int getLineWidth() { +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 02b52e182ce89d20a873076ca3180ff6f68219d3..968c5588659990d1e29252413881cf6dd867b984 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -907,7 +907,7 @@ public class CraftEventFactory { + return event; + } + +- public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, DamageSource damageSource, List<org.bukkit.inventory.ItemStack> drops, String deathMessage, boolean keepInventory) { ++ public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, DamageSource damageSource, List<org.bukkit.inventory.ItemStack> drops, net.kyori.adventure.text.Component deathMessage, boolean keepInventory) { // Paper - Adventure + CraftPlayer entity = victim.getBukkitEntity(); + CraftDamageSource bukkitDamageSource = new CraftDamageSource(damageSource); + PlayerDeathEvent event = new PlayerDeathEvent(entity, bukkitDamageSource, drops, victim.getExpReward(damageSource.getEntity()), 0, deathMessage); +@@ -935,7 +935,7 @@ public class CraftEventFactory { + * Server methods + */ + public static ServerListPingEvent callServerListPingEvent(SocketAddress address, String motd, int numPlayers, int maxPlayers) { +- ServerListPingEvent event = new ServerListPingEvent("", ((InetSocketAddress) address).getAddress(), motd, numPlayers, maxPlayers); ++ ServerListPingEvent event = new ServerListPingEvent("", ((InetSocketAddress) address).getAddress(), Bukkit.getServer().motd(), numPlayers, maxPlayers); + Bukkit.getServer().getPluginManager().callEvent(event); + return event; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +index d47b248535882bb58ae6c1b6ef756464a1989918..39be45585835eabc8d8bcae0158c094c3dcb1aa3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +@@ -73,6 +73,13 @@ public class CraftContainer extends AbstractContainerMenu { + return inventory.getType(); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component title() { ++ return inventory instanceof CraftInventoryCustom ? ((CraftInventoryCustom.MinecraftInventory) ((CraftInventory) inventory).getInventory()).title() : net.kyori.adventure.text.Component.text(inventory.getType().getDefaultTitle()); ++ } ++ // Paper end ++ + @Override + public String getTitle() { + return this.title; +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java +index c9cc23757a9fcc58d30b2915d4c5cfbc7d1c767a..fc0e1212022d1aa3506699b60ef338196eb54eba 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java +@@ -19,6 +19,12 @@ public class CraftInventoryCustom extends CraftInventory { + super(new MinecraftInventory(owner, type)); + } + ++ // Paper start ++ public CraftInventoryCustom(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ super(new MinecraftInventory(owner, type, title)); ++ } ++ // Paper end ++ + public CraftInventoryCustom(InventoryHolder owner, InventoryType type, String title) { + super(new MinecraftInventory(owner, type, title)); + } +@@ -27,6 +33,12 @@ public class CraftInventoryCustom extends CraftInventory { + super(new MinecraftInventory(owner, size)); + } + ++ // Paper start ++ public CraftInventoryCustom(InventoryHolder owner, int size, net.kyori.adventure.text.Component title) { ++ super(new MinecraftInventory(owner, size, title)); ++ } ++ // Paper end ++ + public CraftInventoryCustom(InventoryHolder owner, int size, String title) { + super(new MinecraftInventory(owner, size, title)); + } +@@ -36,9 +48,17 @@ public class CraftInventoryCustom extends CraftInventory { + private int maxStack = MAX_STACK; + private final List<HumanEntity> viewers; + private final String title; ++ private final net.kyori.adventure.text.Component adventure$title; // Paper + private InventoryType type; + private final InventoryHolder owner; + ++ // Paper start ++ public MinecraftInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ this(owner, type.getDefaultSize(), title); ++ this.type = type; ++ } ++ // Paper end ++ + public MinecraftInventory(InventoryHolder owner, InventoryType type) { + this(owner, type.getDefaultSize(), type.getDefaultTitle()); + this.type = type; +@@ -57,11 +77,24 @@ public class CraftInventoryCustom extends CraftInventory { + Preconditions.checkArgument(title != null, "title cannot be null"); + this.items = NonNullList.withSize(size, ItemStack.EMPTY); + this.title = title; ++ this.adventure$title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title); + this.viewers = new ArrayList<HumanEntity>(); + this.owner = owner; + this.type = InventoryType.CHEST; + } + ++ // Paper start ++ public MinecraftInventory(final InventoryHolder owner, final int size, final net.kyori.adventure.text.Component title) { ++ Preconditions.checkArgument(title != null, "Title cannot be null"); ++ this.items = NonNullList.withSize(size, ItemStack.EMPTY); ++ this.title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(title); ++ this.adventure$title = title; ++ this.viewers = new ArrayList<HumanEntity>(); ++ this.owner = owner; ++ this.type = InventoryType.CHEST; ++ } ++ // Paper end ++ + @Override + public int getContainerSize() { + return this.items.size(); +@@ -183,6 +216,12 @@ public class CraftInventoryCustom extends CraftInventory { + return null; + } + ++ // Paper start ++ public net.kyori.adventure.text.Component title() { ++ return this.adventure$title; ++ } ++ // Paper end ++ + public String getTitle() { + return this.title; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java +index 4dd9a80af9901287ab6740b072f2b89678c3d0cb..b2586684295b295a3196a2a9cf724cec975b5a40 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java +@@ -73,6 +73,13 @@ public class CraftInventoryView extends InventoryView { + return CraftItemStack.asCraftMirror(this.container.getSlot(slot).getItem()); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component title() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.container.getTitle()); ++ } ++ // Paper end ++ + @Override + public String getTitle() { + return this.title; +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java +index a24a397f7e2bc2293aee290b3fc39087c79388ac..78c96b43d611f70814b80a05dfded794858e2eda 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java +@@ -510,4 +510,21 @@ public final class CraftItemFactory implements ItemFactory { + Optional<HolderSet.Named<Enchantment>> optional = (allowTreasures) ? Optional.empty() : registry.registryOrThrow(Registries.ENCHANTMENT).getTag(EnchantmentTags.IN_ENCHANTING_TABLE); + return CraftItemStack.asCraftMirror(EnchantmentHelper.enchantItem(source, craft.handle, level, registry, optional)); + } ++ ++ // Paper start - Adventure ++ @Override ++ public net.kyori.adventure.text.event.HoverEvent<net.kyori.adventure.text.event.HoverEvent.ShowItem> asHoverEvent(final ItemStack item, final java.util.function.UnaryOperator<net.kyori.adventure.text.event.HoverEvent.ShowItem> op) { ++ return net.kyori.adventure.text.event.HoverEvent.showItem(op.apply( ++ net.kyori.adventure.text.event.HoverEvent.ShowItem.showItem( ++ item.getType().getKey(), ++ item.getAmount(), ++ io.papermc.paper.adventure.PaperAdventure.asAdventure(CraftItemStack.unwrap(item).getComponentsPatch())) // unwrap is fine here because the components patch will be safely copied ++ )); ++ } ++ ++ @Override ++ public [email protected] Component displayName(@org.jetbrains.annotations.NotNull ItemStack itemStack) { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(CraftItemStack.asNMSCopy(itemStack).getDisplayName()); ++ } ++ // Paper end - Adventure + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java +index 9e05a8515c5f6f340182e91150fcad8bbf80a22b..adf22ce4f0bcd3bd57dc2030c6c92d3df96566e3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java +@@ -13,10 +13,17 @@ import org.bukkit.craftbukkit.util.CraftChatMessage; + + public class CraftMerchantCustom extends CraftMerchant { + ++ @Deprecated // Paper - Adventure + public CraftMerchantCustom(String title) { + super(new MinecraftMerchant(title)); + this.getMerchant().craftMerchant = this; + } ++ // Paper start ++ public CraftMerchantCustom(net.kyori.adventure.text.Component title) { ++ super(new MinecraftMerchant(title)); ++ getMerchant().craftMerchant = this; ++ } ++ // Paper end + + @Override + public String toString() { +@@ -35,10 +42,17 @@ public class CraftMerchantCustom extends CraftMerchant { + private Player tradingPlayer; + protected CraftMerchant craftMerchant; + ++ @Deprecated // Paper - Adventure + public MinecraftMerchant(String title) { + Preconditions.checkArgument(title != null, "Title cannot be null"); + this.title = CraftChatMessage.fromString(title)[0]; + } ++ // Paper start ++ public MinecraftMerchant(net.kyori.adventure.text.Component title) { ++ Preconditions.checkArgument(title != null, "Title cannot be null"); ++ this.title = io.papermc.paper.adventure.PaperAdventure.asVanilla(title); ++ } ++ // Paper end + + @Override + public CraftMerchant getCraftMerchant() { +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java +index 61ad00c38cfef8a1de61b4597ec1042428feccf4..4da38ebb7fdbdb0f8fa422ebcd2e3eec2b2be846 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java +@@ -2,8 +2,9 @@ package org.bukkit.craftbukkit.inventory; + + import com.google.common.base.Preconditions; + import com.google.common.collect.ImmutableList; +-import com.google.common.collect.ImmutableMap.Builder; + import com.google.common.collect.Lists; ++ ++import com.google.common.collect.ImmutableMap; // Paper + import java.util.ArrayList; + import java.util.Arrays; + import java.util.List; +@@ -170,6 +171,130 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta, WritableBo + public void setGeneration(Generation generation) { + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component title() { ++ return null; ++ } ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta title(net.kyori.adventure.text.Component title) { ++ return this; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component author() { ++ return null; ++ } ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta author(net.kyori.adventure.text.Component author) { ++ return this; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component page(final int page) { ++ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number"); ++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(this.pages.get(page - 1)); ++ } ++ ++ @Override ++ public void page(final int page, net.kyori.adventure.text.Component data) { ++ if (!this.isValidPage(page)) { ++ throw new IllegalArgumentException("Invalid page number " + page + "/" + this.pages.size()); ++ } ++ if (data == null) { ++ data = net.kyori.adventure.text.Component.empty(); ++ } ++ this.pages.set(page - 1, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(data)); ++ } ++ ++ @Override ++ public List<net.kyori.adventure.text.Component> pages() { ++ if (this.pages == null) return ImmutableList.of(); ++ return this.pages.stream().map(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()::deserialize).collect(ImmutableList.toImmutableList()); ++ } ++ ++ @Override ++ public BookMeta pages(List<net.kyori.adventure.text.Component> pages) { ++ if (this.pages != null) this.pages.clear(); ++ for (net.kyori.adventure.text.Component page : pages) { ++ this.addPages(page); ++ } ++ return this; ++ } ++ ++ @Override ++ public BookMeta pages(net.kyori.adventure.text.Component... pages) { ++ if (this.pages != null) this.pages.clear(); ++ this.addPages(pages); ++ return this; ++ } ++ ++ @Override ++ public void addPages(net.kyori.adventure.text.Component... pages) { ++ if (this.pages == null) this.pages = new ArrayList<>(); ++ for (net.kyori.adventure.text.Component page : pages) { ++ if (this.pages.size() >= MAX_PAGES) { ++ return; ++ } ++ ++ if (page == null) { ++ page = net.kyori.adventure.text.Component.empty(); ++ } ++ ++ this.pages.add(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(page)); ++ } ++ } ++ ++ private CraftMetaBook(List<net.kyori.adventure.text.Component> pages) { ++ super((org.bukkit.craftbukkit.inventory.CraftMetaItem) org.bukkit.Bukkit.getItemFactory().getItemMeta(org.bukkit.Material.WRITABLE_BOOK)); ++ this.pages = pages.subList(0, Math.min(MAX_PAGES, pages.size())).stream().map(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()::serialize).collect(java.util.stream.Collectors.toList()); ++ } ++ ++ static class CraftMetaBookBuilder implements BookMetaBuilder { ++ protected final List<net.kyori.adventure.text.Component> pages = new java.util.ArrayList<>(); ++ ++ @Override ++ public BookMetaBuilder title(net.kyori.adventure.text.Component title) { ++ return this; ++ } ++ ++ @Override ++ public BookMetaBuilder author(net.kyori.adventure.text.Component author) { ++ return this; ++ } ++ ++ @Override ++ public BookMetaBuilder addPage(net.kyori.adventure.text.Component page) { ++ this.pages.add(page); ++ return this; ++ } ++ ++ @Override ++ public BookMetaBuilder pages(net.kyori.adventure.text.Component... pages) { ++ java.util.Collections.addAll(this.pages, pages); ++ return this; ++ } ++ ++ @Override ++ public BookMetaBuilder pages(java.util.Collection<net.kyori.adventure.text.Component> pages) { ++ this.pages.addAll(pages); ++ return this; ++ } ++ ++ @Override ++ public BookMeta build() { ++ return new CraftMetaBook(this.pages); ++ } ++ } ++ ++ @Override ++ public BookMetaBuilder toBuilder() { ++ return new CraftMetaBookBuilder(); ++ } ++ ++ // Paper end + @Override + public String getPage(final int page) { + Preconditions.checkArgument(this.isValidPage(page), "Invalid page number (%s)", page); +@@ -286,7 +411,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta, WritableBo + } + + @Override +- Builder<String, Object> serialize(Builder<String, Object> builder) { ++ ImmutableMap.Builder<String, Object> serialize(ImmutableMap.Builder<String, Object> builder) { + super.serialize(builder); + + if (this.pages != null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java +index 2e6f0a0f4bbe4ae3c7c85e679f6187e89d1298ff..c7360e2b2d6e50abc371c21b09cdadd63892f439 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java +@@ -2,7 +2,7 @@ package org.bukkit.craftbukkit.inventory; + + import com.google.common.base.Preconditions; + import com.google.common.collect.ImmutableList; +-import com.google.common.collect.ImmutableMap.Builder; ++import com.google.common.collect.ImmutableMap; // Paper + import java.util.ArrayList; + import java.util.Arrays; + import java.util.List; +@@ -346,7 +346,7 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta { + } + + @Override +- Builder<String, Object> serialize(Builder<String, Object> builder) { ++ ImmutableMap.Builder<String, Object> serialize(ImmutableMap.Builder<String, Object> builder) { + super.serialize(builder); + + if (this.hasTitle()) { +@@ -459,4 +459,113 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta { + return this.spigot; + } + // Spigot end ++ ++ // Paper start - adventure ++ public static final net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.builder() ++ .character(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.SECTION_CHAR) ++ .build(); ++ private CraftMetaBookSigned(net.kyori.adventure.text.Component title, net.kyori.adventure.text.Component author, java.util.List<net.kyori.adventure.text.Component> pages) { ++ super((org.bukkit.craftbukkit.inventory.CraftMetaItem) org.bukkit.Bukkit.getItemFactory().getItemMeta(Material.WRITABLE_BOOK)); ++ this.title = title == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(title); ++ this.author = author == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(author); ++ this.pages = io.papermc.paper.adventure.PaperAdventure.asVanilla(pages.subList(0, Math.min(MAX_PAGES, pages.size()))); ++ } ++ ++ static final class CraftMetaBookSignedBuilder extends CraftMetaBook.CraftMetaBookBuilder { ++ private net.kyori.adventure.text.Component title; ++ private net.kyori.adventure.text.Component author; ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta.BookMetaBuilder title(final net.kyori.adventure.text.Component title) { ++ this.title = title; ++ return this; ++ } ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta.BookMetaBuilder author(final net.kyori.adventure.text.Component author) { ++ this.author = author; ++ return this; ++ } ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta build() { ++ return new CraftMetaBookSigned(this.title, this.author, this.pages); ++ } ++ } ++ ++ @Override ++ public BookMetaBuilder toBuilder() { ++ return new CraftMetaBookSignedBuilder(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component title() { ++ return this.title == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.deserialize(this.title); ++ } ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta title(net.kyori.adventure.text.Component title) { ++ this.setTitle(title == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(title)); ++ return this; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component author() { ++ return this.author == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.deserialize(this.author); ++ } ++ ++ @Override ++ public org.bukkit.inventory.meta.BookMeta author(net.kyori.adventure.text.Component author) { ++ this.setAuthor(author == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(author)); ++ return this; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component page(final int page) { ++ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number"); ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.pages.get(page - 1)); ++ } ++ ++ @Override ++ public void page(final int page, net.kyori.adventure.text.Component data) { ++ if (!this.isValidPage(page)) { ++ throw new IllegalArgumentException("Invalid page number " + page + "/" + this.pages.size()); ++ } ++ this.pages.set(page - 1, io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(data)); ++ } ++ ++ @Override ++ public List<net.kyori.adventure.text.Component> pages() { ++ if (this.pages == null) return ImmutableList.of(); ++ return this.pages.stream().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).collect(ImmutableList.toImmutableList()); ++ } ++ ++ @Override ++ public BookMeta pages(List<net.kyori.adventure.text.Component> pages) { ++ if (this.pages != null) this.pages.clear(); ++ for (net.kyori.adventure.text.Component page : pages) { ++ this.addPages(page); ++ } ++ return this; ++ } ++ ++ @Override ++ public BookMeta pages(net.kyori.adventure.text.Component... pages) { ++ if (this.pages != null) this.pages.clear(); ++ this.addPages(pages); ++ return this; ++ } ++ ++ @Override ++ public void addPages(net.kyori.adventure.text.Component... pages) { ++ if (this.pages == null) this.pages = new ArrayList<>(); ++ for (net.kyori.adventure.text.Component page : pages) { ++ if (this.pages.size() >= MAX_PAGES) { ++ return; ++ } ++ ++ this.pages.add(io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(page)); ++ } ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +index 23b6575326446441a3fa3cb5788d7068e49705af..fd147ea7fb5d143c11e7e2bf09f31bcb6e513f76 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +@@ -906,6 +906,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + return !(this.hasDisplayName() || this.hasItemName() || this.hasLocalizedName() || this.hasEnchants() || (this.lore != null) || this.hasCustomModelData() || this.hasBlockData() || this.hasRepairCost() || !this.unhandledTags.build().isEmpty() || !this.persistentDataContainer.isEmpty() || this.hideFlag != 0 || this.isHideTooltip() || this.isUnbreakable() || this.hasEnchantmentGlintOverride() || this.isFireResistant() || this.hasMaxStackSize() || this.hasRarity() || this.hasFood() || this.hasTool() || this.hasDamage() || this.hasMaxDamage() || this.hasAttributeModifiers() || this.customTag != null); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component displayName() { ++ return displayName == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(displayName); ++ } ++ ++ @Override ++ public void displayName(final net.kyori.adventure.text.Component displayName) { ++ this.displayName = displayName == null ? null : io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName); ++ } ++ // Paper end ++ + @Override + public String getDisplayName() { + return CraftChatMessage.fromComponent(this.displayName); +@@ -936,6 +948,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + return this.itemName != null; + } + ++ // Paper start - Adventure ++ @Override ++ public net.kyori.adventure.text.Component itemName() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.itemName); ++ } ++ ++ @Override ++ public void itemName(final net.kyori.adventure.text.Component name) { ++ this.itemName = io.papermc.paper.adventure.PaperAdventure.asVanilla(name); ++ } ++ // Paper end - Adventure ++ + @Override + public String getLocalizedName() { + return this.getDisplayName(); +@@ -955,6 +979,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + return this.lore != null && !this.lore.isEmpty(); + } + ++ // Paper start ++ @Override ++ public List<net.kyori.adventure.text.Component> lore() { ++ return this.lore != null ? io.papermc.paper.adventure.PaperAdventure.asAdventure(this.lore) : null; ++ } ++ ++ @Override ++ public void lore(final List<? extends net.kyori.adventure.text.Component> lore) { ++ this.lore = lore != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(lore) : null; ++ } ++ // Paper end ++ + @Override + public boolean hasRepairCost() { + return this.repairCost > 0; +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java +index 8952e5526cfe90ad16a7af28b0a8d9c65b159f90..cd3e35867075e65f46051fb88d8a2460a8bb4b53 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java +@@ -60,6 +60,14 @@ public class CraftTrimMaterial implements TrimMaterial, Handleable<net.minecraft + @NotNull + @Override + public String getTranslationKey() { ++ if (!(this.handle.description().getContents() instanceof TranslatableContents)) throw new UnsupportedOperationException("Description isn't translatable!"); // Paper + return ((TranslatableContents) this.handle.description().getContents()).getKey(); + } ++ ++ // Paper start - adventure ++ @Override ++ public net.kyori.adventure.text.Component description() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.handle.description()); ++ } ++ // Paper end - adventure + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java +index 0a6df5fdaf6724bb1cf777d527f032e2ae74fbbd..364f8d7a7106259401154d91b1b79869d014a469 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java +@@ -60,6 +60,14 @@ public class CraftTrimPattern implements TrimPattern, Handleable<net.minecraft.w + @NotNull + @Override + public String getTranslationKey() { ++ if (!(this.handle.description().getContents() instanceof TranslatableContents)) throw new UnsupportedOperationException("Description isn't translatable!"); // Paper + return ((TranslatableContents) this.handle.description().getContents()).getKey(); + } ++ ++ // Paper start - adventure ++ @Override ++ public net.kyori.adventure.text.Component description() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.handle.description()); ++ } ++ // Paper end - adventure + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java +index ed4415f6dd588c08c922efd5beebb3b124beb9d6..78a7ac47f20e84ccd67ff44d0bc7a2f2faa0d476 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java +@@ -12,6 +12,13 @@ public class CraftCustomInventoryConverter implements CraftInventoryCreator.Inve + return new CraftInventoryCustom(holder, type); + } + ++ // Paper start ++ @Override ++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ return new CraftInventoryCustom(owner, type, title); ++ } ++ // Paper end ++ + @Override + public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) { + return new CraftInventoryCustom(owner, type, title); +@@ -21,6 +28,12 @@ public class CraftCustomInventoryConverter implements CraftInventoryCreator.Inve + return new CraftInventoryCustom(owner, size); + } + ++ // Paper start ++ public Inventory createInventory(InventoryHolder owner, int size, net.kyori.adventure.text.Component title) { ++ return new CraftInventoryCustom(owner, size, title); ++ } ++ // Paper end ++ + public Inventory createInventory(InventoryHolder owner, int size, String title) { + return new CraftInventoryCustom(owner, size, title); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java +index ec8ef47ed7cc627fef2c71b2b281119245e88b97..53cbc743b1e722d029021f9d63ffbf7d0fddd04e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java +@@ -45,6 +45,12 @@ public final class CraftInventoryCreator { + return this.converterMap.get(type).createInventory(holder, type); + } + ++ // Paper start ++ public Inventory createInventory(InventoryHolder holder, InventoryType type, net.kyori.adventure.text.Component title) { ++ return converterMap.get(type).createInventory(holder, type, title); ++ } ++ // Paper end ++ + public Inventory createInventory(InventoryHolder holder, InventoryType type, String title) { + return this.converterMap.get(type).createInventory(holder, type, title); + } +@@ -53,6 +59,12 @@ public final class CraftInventoryCreator { + return this.DEFAULT_CONVERTER.createInventory(holder, size); + } + ++ // Paper start ++ public Inventory createInventory(InventoryHolder holder, int size, net.kyori.adventure.text.Component title) { ++ return DEFAULT_CONVERTER.createInventory(holder, size, title); ++ } ++ // Paper end ++ + public Inventory createInventory(InventoryHolder holder, int size, String title) { + return this.DEFAULT_CONVERTER.createInventory(holder, size, title); + } +@@ -61,6 +73,10 @@ public final class CraftInventoryCreator { + + Inventory createInventory(InventoryHolder holder, InventoryType type); + ++ // Paper start ++ Inventory createInventory(InventoryHolder holder, InventoryType type, net.kyori.adventure.text.Component title); ++ // Paper end ++ + Inventory createInventory(InventoryHolder holder, InventoryType type, String title); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java +index 11d23b3a9a0c99ede04f2cd64d8022b2b0b737df..7bc082d08a3d577481046818f0d58133413fc723 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java +@@ -31,6 +31,18 @@ public abstract class CraftTileInventoryConverter implements CraftInventoryCreat + return this.getInventory(this.getTileEntity()); + } + ++ // Paper start ++ @Override ++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ Container te = getTileEntity(); ++ if (te instanceof RandomizableContainerBlockEntity) { ++ ((RandomizableContainerBlockEntity) te).name = io.papermc.paper.adventure.PaperAdventure.asVanilla(title); ++ } ++ ++ return getInventory(te); ++ } ++ // Paper end ++ + @Override + public Inventory createInventory(InventoryHolder holder, InventoryType type, String title) { + Container te = this.getTileEntity(); +@@ -53,6 +65,15 @@ public abstract class CraftTileInventoryConverter implements CraftInventoryCreat + return furnace; + } + ++ // Paper start ++ @Override ++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ Container tileEntity = getTileEntity(); ++ ((AbstractFurnaceBlockEntity) tileEntity).setCustomName(io.papermc.paper.adventure.PaperAdventure.asVanilla(title)); ++ return getInventory(tileEntity); ++ } ++ // Paper end ++ + @Override + public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) { + Container tileEntity = this.getTileEntity(); +@@ -73,6 +94,18 @@ public abstract class CraftTileInventoryConverter implements CraftInventoryCreat + return new BrewingStandBlockEntity(BlockPos.ZERO, Blocks.BREWING_STAND.defaultBlockState()); + } + ++ // Paper start ++ @Override ++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) { ++ // BrewingStand does not extend TileEntityLootable ++ Container tileEntity = getTileEntity(); ++ if (tileEntity instanceof BrewingStandBlockEntity) { ++ ((BrewingStandBlockEntity) tileEntity).name = io.papermc.paper.adventure.PaperAdventure.asVanilla(title); ++ } ++ return getInventory(tileEntity); ++ } ++ // Paper end ++ + @Override + public Inventory createInventory(InventoryHolder holder, InventoryType type, String title) { + // BrewingStand does not extend TileEntityLootable +diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +index 9efd33cc208af7068be6cf4040dd398f2506c709..0cbbd915631904fe8c6effefb92895422b33eff6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +@@ -43,7 +43,7 @@ public class CraftMapRenderer extends MapRenderer { + } + + MapDecoration decoration = this.worldMap.decorations.get(key); +- cursors.addCursor(new MapCursor(decoration.x(), decoration.y(), (byte) (decoration.rot() & 15), CraftMapCursor.CraftType.minecraftHolderToBukkit(decoration.type()), true, CraftChatMessage.fromComponent(decoration.name().orElse(null)))); ++ cursors.addCursor(new MapCursor(decoration.x(), decoration.y(), (byte) (decoration.rot() & 15), CraftMapCursor.CraftType.minecraftHolderToBukkit(decoration.type()), true, decoration.name().isEmpty() ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(decoration.name().get()))); // Paper + } + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java +index 5c987c7d9e43bb481800935cbc918a43a3656524..da1e4496d78a2c1b258ff8bb316414cb8a662ba2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java +@@ -31,6 +31,21 @@ final class CraftObjective extends CraftScoreboardComponent implements Objective + return this.objective.getName(); + } + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component displayName() throws IllegalStateException { ++ CraftScoreboard scoreboard = checkState(); ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(objective.getDisplayName()); ++ } ++ @Override ++ public void displayName(net.kyori.adventure.text.Component displayName) throws IllegalStateException, IllegalArgumentException { ++ if (displayName == null) { ++ displayName = net.kyori.adventure.text.Component.empty(); ++ } ++ CraftScoreboard scoreboard = checkState(); ++ objective.setDisplayName(io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName)); ++ } ++ // Paper end + @Override + public String getDisplayName() { + this.checkState(); +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java +index d9f8a0c894a2a70b3dc2cef4feb87363627ec73a..5681630159bb52628e6cc391db324bbafe333414 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java +@@ -29,6 +29,33 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + public CraftObjective registerNewObjective(String name, String criteria) { + return this.registerNewObjective(name, criteria, name); + } ++ // Paper start - Adventure ++ @Override ++ public CraftObjective registerNewObjective(String name, String criteria, net.kyori.adventure.text.Component displayName) { ++ return this.registerNewObjective(name, CraftCriteria.getFromBukkit(criteria), displayName, RenderType.INTEGER); ++ } ++ @Override ++ public CraftObjective registerNewObjective(String name, String criteria, net.kyori.adventure.text.Component displayName, RenderType renderType) { ++ return this.registerNewObjective(name, CraftCriteria.getFromBukkit(criteria), displayName, renderType); ++ } ++ @Override ++ public CraftObjective registerNewObjective(String name, Criteria criteria, net.kyori.adventure.text.Component displayName) throws IllegalArgumentException { ++ return this.registerNewObjective(name, criteria, displayName, RenderType.INTEGER); ++ } ++ @Override ++ public CraftObjective registerNewObjective(String name, Criteria criteria, net.kyori.adventure.text.Component displayName, RenderType renderType) throws IllegalArgumentException { ++ if (displayName == null) { ++ displayName = net.kyori.adventure.text.Component.empty(); ++ } ++ Preconditions.checkArgument(name != null, "Objective name cannot be null"); ++ Preconditions.checkArgument(criteria != null, "Criteria cannot be null"); ++ Preconditions.checkArgument(renderType != null, "RenderType cannot be null"); ++ Preconditions.checkArgument(name.length() <= Short.MAX_VALUE, "The name '%s' is longer than the limit of 32767 characters (%s)", name, name.length()); ++ Preconditions.checkArgument(this.board.getObjective(name) == null, "An objective of name '%s' already exists", name); ++ net.minecraft.world.scores.Objective objective = this.board.addObjective(name, ((CraftCriteria) criteria).criteria, io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName), CraftScoreboardTranslations.fromBukkitRender(renderType), true, null); ++ return new CraftObjective(this, objective); ++ } ++ // Paper end - Adventure + + @Override + public CraftObjective registerNewObjective(String name, String criteria, String displayName) { +@@ -47,15 +74,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + + @Override + public CraftObjective registerNewObjective(String name, Criteria criteria, String displayName, RenderType renderType) { +- Preconditions.checkArgument(name != null, "Objective name cannot be null"); +- Preconditions.checkArgument(criteria != null, "Criteria cannot be null"); +- Preconditions.checkArgument(displayName != null, "Display name cannot be null"); +- Preconditions.checkArgument(renderType != null, "RenderType cannot be null"); +- Preconditions.checkArgument(name.length() <= Short.MAX_VALUE, "The name '%s' is longer than the limit of 32767 characters (%s)", name, name.length()); +- Preconditions.checkArgument(this.board.getObjective(name) == null, "An objective of name '%s' already exists", name); +- +- net.minecraft.world.scores.Objective objective = this.board.addObjective(name, ((CraftCriteria) criteria).criteria, CraftChatMessage.fromStringOrNull(displayName), CraftScoreboardTranslations.fromBukkitRender(renderType), true, null); +- return new CraftObjective(this, objective); ++ return this.registerNewObjective(name, criteria, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(displayName), renderType); // Paper - Adventure + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java +index 4d586e1375ed8782939c9d480479e0dd981f8cbc..7900adb0b158bc17dd792dd082c338547bc1aa0a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java +@@ -26,6 +26,63 @@ final class CraftTeam extends CraftScoreboardComponent implements Team { + + return this.team.getName(); + } ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.Component displayName() throws IllegalStateException { ++ CraftScoreboard scoreboard = checkState(); ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(team.getDisplayName()); ++ } ++ @Override ++ public void displayName(net.kyori.adventure.text.Component displayName) throws IllegalStateException, IllegalArgumentException { ++ if (displayName == null) displayName = net.kyori.adventure.text.Component.empty(); ++ CraftScoreboard scoreboard = checkState(); ++ team.setDisplayName(io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName)); ++ } ++ @Override ++ public net.kyori.adventure.text.Component prefix() throws IllegalStateException { ++ CraftScoreboard scoreboard = checkState(); ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(team.getPlayerPrefix()); ++ } ++ @Override ++ public void prefix(net.kyori.adventure.text.Component prefix) throws IllegalStateException, IllegalArgumentException { ++ if (prefix == null) prefix = net.kyori.adventure.text.Component.empty(); ++ CraftScoreboard scoreboard = checkState(); ++ team.setPlayerPrefix(io.papermc.paper.adventure.PaperAdventure.asVanilla(prefix)); ++ } ++ @Override ++ public net.kyori.adventure.text.Component suffix() throws IllegalStateException { ++ CraftScoreboard scoreboard = checkState(); ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(team.getPlayerSuffix()); ++ } ++ @Override ++ public void suffix(net.kyori.adventure.text.Component suffix) throws IllegalStateException, IllegalArgumentException { ++ if (suffix == null) suffix = net.kyori.adventure.text.Component.empty(); ++ CraftScoreboard scoreboard = checkState(); ++ team.setPlayerSuffix(io.papermc.paper.adventure.PaperAdventure.asVanilla(suffix)); ++ } ++ @Override ++ public boolean hasColor() { ++ CraftScoreboard scoreboard = checkState(); ++ return this.team.getColor().getColor() != null; ++ } ++ @Override ++ public net.kyori.adventure.text.format.TextColor color() throws IllegalStateException { ++ CraftScoreboard scoreboard = checkState(); ++ if (team.getColor().getColor() == null) throw new IllegalStateException("Team colors must have hex values"); ++ net.kyori.adventure.text.format.TextColor color = net.kyori.adventure.text.format.TextColor.color(team.getColor().getColor()); ++ if (!(color instanceof net.kyori.adventure.text.format.NamedTextColor)) throw new IllegalStateException("Team doesn't have a NamedTextColor"); ++ return (net.kyori.adventure.text.format.NamedTextColor) color; ++ } ++ @Override ++ public void color(net.kyori.adventure.text.format.NamedTextColor color) { ++ CraftScoreboard scoreboard = checkState(); ++ if (color == null) { ++ this.team.setColor(net.minecraft.ChatFormatting.RESET); ++ } else { ++ this.team.setColor(io.papermc.paper.adventure.PaperAdventure.asVanilla(color)); ++ } ++ } ++ // Paper end + + @Override + public String getDisplayName() { +@@ -303,4 +360,20 @@ final class CraftTeam extends CraftScoreboardComponent implements Team { + return !(this.team != other.team && (this.team == null || !this.team.equals(other.team))); + } + ++ // Paper start - make Team extend ForwardingAudience ++ @Override ++ public @org.jetbrains.annotations.NotNull Iterable<? extends net.kyori.adventure.audience.Audience> audiences() { ++ this.checkState(); ++ java.util.List<net.kyori.adventure.audience.Audience> audiences = new java.util.ArrayList<>(); ++ for (String playerName : this.team.getPlayers()) { ++ org.bukkit.entity.Player player = Bukkit.getPlayerExact(playerName); ++ if (player != null) { ++ audiences.add(player); ++ } ++ } ++ ++ return audiences; ++ } ++ // Paper end - make Team extend ForwardingAudience ++ + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java b/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java +index 28038c3a531680201dcc8f2716b8f46f3886e769..5a9ddf71dc186c537a23083ac59434fb446a2140 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java +@@ -308,6 +308,7 @@ public final class CraftChatMessage { + + public static String fromComponent(Component component) { + if (component == null) return ""; ++ if (component instanceof io.papermc.paper.adventure.AdventureComponent) component = ((io.papermc.paper.adventure.AdventureComponent) component).deepConverted(); + StringBuilder out = new StringBuilder(); + + boolean hadFormat = false; +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 2fc68d129e2fdfd51e310ea5bdfb83322666c87b..fb22e04a4a9f039f0e942ff837124b00ca1b12be 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -81,6 +81,43 @@ public final class CraftMagicNumbers implements UnsafeValues { + + private CraftMagicNumbers() {} + ++ // Paper start ++ @Override ++ public net.kyori.adventure.text.flattener.ComponentFlattener componentFlattener() { ++ return io.papermc.paper.adventure.PaperAdventure.FLATTENER; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.serializer.gson.GsonComponentSerializer colorDownsamplingGsonComponentSerializer() { ++ return net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.colorDownsamplingGson(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.serializer.gson.GsonComponentSerializer gsonComponentSerializer() { ++ return net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.serializer.plain.PlainComponentSerializer plainComponentSerializer() { ++ return io.papermc.paper.adventure.PaperAdventure.PLAIN; ++ } ++ ++ @Override ++ public net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer plainTextSerializer() { ++ return net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer legacyComponentSerializer() { ++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component resolveWithContext(final net.kyori.adventure.text.Component component, final org.bukkit.command.CommandSender context, final org.bukkit.entity.Entity scoreboardSubject, final boolean bypassPermissions) throws IOException { ++ return io.papermc.paper.adventure.PaperAdventure.resolveWithContext(component, context, scoreboardSubject, bypassPermissions); ++ } ++ // Paper end ++ + public static BlockState getBlock(MaterialData material) { + return CraftMagicNumbers.getBlock(material.getItemType(), material.getData()); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java +index 62c66e3179b9557cdba46242df0fb15bce7e7710..73a37638abacdffbff8274291a64ea6cd0be7a5e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java +@@ -80,7 +80,7 @@ public abstract class LazyHashSet<E> implements Set<E> { + return this.reference = this.makeReference(); + } + +- abstract Set<E> makeReference(); ++ protected abstract Set<E> makeReference(); // Paper - protected + + public boolean isLazy() { + return this.reference == null; +diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java +index 0ab4c7eaffe69b314423732dd529aaeafc476e08..8a44e7260518bda87c6d0eeade98d5b81a04c3b6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java +@@ -16,9 +16,14 @@ public class LazyPlayerSet extends LazyHashSet<Player> { + } + + @Override +- HashSet<Player> makeReference() { ++ protected HashSet<Player> makeReference() { // Paper - protected + Preconditions.checkState(this.reference == null, "Reference already created!"); +- List<ServerPlayer> players = this.server.getPlayerList().players; ++ // Paper start ++ return makePlayerSet(this.server); ++ } ++ public static HashSet<Player> makePlayerSet(final MinecraftServer server) { ++ List<ServerPlayer> players = server.getPlayerList().players; ++ // Paper end + HashSet<Player> reference = new HashSet<Player>(players.size()); + for (ServerPlayer player : players) { + reference.add(player.getBukkitEntity()); +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.bossbar.BossBarImplementation$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.bossbar.BossBarImplementation$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..9b7119d0b88bf7f9d25fab37a15340cabc0c9b7b +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.bossbar.BossBarImplementation$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.BossBarImplementationProvider +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..845711e03c41c6b6a03d541f1c43d37b24c11733 +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.ClickCallbackProviderImpl +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..714cb03596627badb6ad7f23b17f2e686761a9b5 +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.DataComponentValueConverterProviderImpl +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider b/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider +new file mode 100644 +index 0000000000000000000000000000000000000000..399bde6e57cd82b50d3ebe0f51a3958fa2d52d43 +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.ComponentLoggerProviderImpl +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.minimessage.MiniMessage$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.minimessage.MiniMessage$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..6ce632b6c9dc5e4b3b978331df51c0ffd1526471 +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.minimessage.MiniMessage$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.MiniMessageProviderImpl +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.gson.GsonComponentSerializer$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.gson.GsonComponentSerializer$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..bc9f7398a0fe158af05b562a8ded9e74a22eae9b +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.gson.GsonComponentSerializer$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.GsonComponentSerializerProviderImpl +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..820f381981a91754b7f0c5106f93b773d885e321 +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.LegacyComponentSerializerProviderImpl +diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer$Provider +new file mode 100644 +index 0000000000000000000000000000000000000000..28d777610b52ba74f808bf3245d73b8333d01fa7 +--- /dev/null ++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer$Provider +@@ -0,0 +1 @@ ++io.papermc.paper.adventure.providers.PlainTextComponentSerializerProviderImpl +diff --git a/src/main/resources/data/minecraft/datapacks/paper/data/paper/chat_type/raw.json b/src/main/resources/data/minecraft/datapacks/paper/data/paper/chat_type/raw.json +new file mode 100644 +index 0000000000000000000000000000000000000000..3aedd0bbc97edacc1ebf71264b310e55aaaa5cb3 +--- /dev/null ++++ b/src/main/resources/data/minecraft/datapacks/paper/data/paper/chat_type/raw.json +@@ -0,0 +1,14 @@ ++{ ++ "chat": { ++ "parameters": [ ++ "content" ++ ], ++ "translation_key": "%s" ++ }, ++ "narration": { ++ "parameters": [ ++ "content" ++ ], ++ "translation_key": "%s" ++ } ++} +diff --git a/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java b/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69b3aa957161e75d6344e437a8b4a4533ef00523 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java +@@ -0,0 +1,403 @@ ++package io.papermc.paper.adventure; ++ ++import com.mojang.datafixers.util.Pair; ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.DataResult; ++import com.mojang.serialization.DynamicOps; ++import com.mojang.serialization.JavaOps; ++import com.mojang.serialization.JsonOps; ++import io.papermc.paper.util.MethodParameterSource; ++import java.io.IOException; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.util.List; ++import java.util.UUID; ++import java.util.function.Function; ++import java.util.stream.Stream; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.text.BlockNBTComponent; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.Style; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.nbt.ByteTag; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.IntTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.network.chat.ComponentSerialization; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; ++import org.apache.commons.lang3.RandomStringUtils; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.EnumSource; ++import org.junit.jupiter.params.provider.MethodSource; ++import org.junitpioneer.jupiter.cartesian.CartesianTest; ++ ++import static io.papermc.paper.adventure.AdventureCodecs.CLICK_EVENT_CODEC; ++import static io.papermc.paper.adventure.AdventureCodecs.COMPONENT_CODEC; ++import static io.papermc.paper.adventure.AdventureCodecs.HOVER_EVENT_CODEC; ++import static io.papermc.paper.adventure.AdventureCodecs.KEY_CODEC; ++import static io.papermc.paper.adventure.AdventureCodecs.STYLE_MAP_CODEC; ++import static io.papermc.paper.adventure.AdventureCodecs.TEXT_COLOR_CODEC; ++import static java.util.Objects.requireNonNull; ++import static net.kyori.adventure.key.Key.key; ++import static net.kyori.adventure.text.Component.blockNBT; ++import static net.kyori.adventure.text.Component.entityNBT; ++import static net.kyori.adventure.text.Component.keybind; ++import static net.kyori.adventure.text.Component.score; ++import static net.kyori.adventure.text.Component.selector; ++import static net.kyori.adventure.text.Component.storageNBT; ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.Component.translatable; ++import static net.kyori.adventure.text.TranslationArgument.numeric; ++import static net.kyori.adventure.text.event.ClickEvent.openUrl; ++import static net.kyori.adventure.text.event.ClickEvent.suggestCommand; ++import static net.kyori.adventure.text.event.HoverEvent.showEntity; ++import static net.kyori.adventure.text.format.Style.style; ++import static net.kyori.adventure.text.format.TextColor.color; ++import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage; ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertNotNull; ++import static org.junit.jupiter.api.Assertions.assertThrows; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++ ++class AdventureCodecsTest extends AbstractTestingBase { ++ ++ static final String PARAMETERIZED_NAME = "[{index}] {displayName}: {arguments}"; ++ ++ @Test ++ void testTextColor() { ++ final TextColor color = color(0x1d38df); ++ final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow(); ++ assertEquals(color.asHexString(), result.getAsString()); ++ final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); ++ assertEquals(color.value(), nms.getValue()); ++ } ++ ++ @Test ++ void testNamedTextColor() { ++ final NamedTextColor color = NamedTextColor.BLUE; ++ final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow(); ++ assertEquals(NamedTextColor.NAMES.keyOrThrow(color), result.getAsString()); ++ final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); ++ assertEquals(color.value(), nms.getValue()); ++ } ++ ++ @Test ++ void testKey() { ++ final Key key = key("hello", "there"); ++ final Tag result = KEY_CODEC.encodeStart(NbtOps.INSTANCE, key).result().orElseThrow(); ++ assertEquals(key.asString(), result.getAsString()); ++ final ResourceLocation location = ResourceLocation.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); ++ assertEquals(key.asString(), location.toString()); ++ } ++ ++ @ParameterizedTest(name = PARAMETERIZED_NAME) ++ @EnumSource(value = ClickEvent.Action.class, mode = EnumSource.Mode.EXCLUDE, names = {"OPEN_FILE"}) ++ void testClickEvent(final ClickEvent.Action action) { ++ final ClickEvent event = ClickEvent.clickEvent(action, RandomStringUtils.randomAlphanumeric(20)); ++ final Tag result = CLICK_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, event).result().orElseThrow(); ++ final net.minecraft.network.chat.ClickEvent nms = net.minecraft.network.chat.ClickEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); ++ assertEquals(event.action().toString(), nms.getAction().getSerializedName()); ++ assertEquals(event.value(), nms.getValue()); ++ } ++ ++ @Test ++ void testShowTextHoverEvent() { ++ final HoverEvent<Component> hoverEvent = HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("hello")); ++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); ++ final net.minecraft.network.chat.HoverEvent nms = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); ++ assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName()); ++ assertNotNull(nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_TEXT)); ++ } ++ ++ @Test ++ void testShowItemHoverEvent() throws IOException { ++ final ItemStack stack = new ItemStack(Items.PUMPKIN, 3); ++ stack.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("NAME")); ++ final HoverEvent<HoverEvent.ShowItem> hoverEvent = HoverEvent.showItem(key("minecraft:pumpkin"), 3, PaperAdventure.asAdventure(stack.getComponentsPatch())); ++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); ++ final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result); ++ assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present"); ++ final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst(); ++ assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName()); ++ final net.minecraft.network.chat.HoverEvent.ItemStackInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ITEM); ++ assertNotNull(value); ++ assertEquals(hoverEvent.value().count(), value.count); ++ assertEquals(hoverEvent.value().item().asString(), value.item.unwrapKey().orElseThrow().location().toString()); ++ assertEquals(stack.getComponentsPatch(), value.components); ++ } ++ ++ @Test ++ void testShowEntityHoverEvent() { ++ UUID uuid = UUID.randomUUID(); ++ final HoverEvent<HoverEvent.ShowEntity> hoverEvent = showEntity(key("minecraft:wolf"), uuid, text("NAME")); ++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); ++ final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result); ++ assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present"); ++ final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst(); ++ assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName()); ++ final net.minecraft.network.chat.HoverEvent.EntityTooltipInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ENTITY); ++ assertNotNull(value); ++ assertEquals(hoverEvent.value().type().asString(), BuiltInRegistries.ENTITY_TYPE.getKey(value.type).toString()); ++ assertEquals(hoverEvent.value().id(), value.id); ++ assertEquals("NAME", value.name.orElseThrow().getString()); ++ } ++ ++ @Test ++ void testSimpleStyle() { ++ final Style style = style().decorate(TextDecoration.BOLD).color(NamedTextColor.RED).build(); ++ final Tag result = STYLE_MAP_CODEC.codec().encodeStart(NbtOps.INSTANCE, style).result().orElseThrow(); ++ final DataResult<Pair<net.minecraft.network.chat.Style, Tag>> dataResult = net.minecraft.network.chat.Style.Serializer.CODEC.decode(NbtOps.INSTANCE, result); ++ assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present"); ++ final net.minecraft.network.chat.Style nms = dataResult.result().get().getFirst(); ++ assertTrue(nms.isBold()); ++ assertEquals(requireNonNull(style.color()).value(), requireNonNull(nms.getColor()).getValue()); ++ } ++ ++ @CartesianTest(name = PARAMETERIZED_NAME) ++ void testDirectRoundTripStyle( ++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps, ++ @MethodParameterSource("testStyles") final Style style ++ ) { ++ testDirectRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), style); ++ } ++ ++ @CartesianTest(name = PARAMETERIZED_NAME) ++ void testMinecraftRoundTripStyle( ++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps, ++ @MethodParameterSource("testStyles") final Style style ++ ) { ++ testMinecraftRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style); ++ } ++ ++ @CartesianTest(name = PARAMETERIZED_NAME) ++ void testDirectRoundTripComponent( ++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps, ++ @TestComponents final Component component ++ ) { ++ testDirectRoundTrip(dynamicOps, COMPONENT_CODEC, component); ++ } ++ ++ @CartesianTest(name = PARAMETERIZED_NAME) ++ void testMinecraftRoundTripComponent( ++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps, ++ @TestComponents final Component component ++ ) { ++ testMinecraftRoundTrip(dynamicOps, COMPONENT_CODEC, ComponentSerialization.CODEC, component); ++ } ++ ++ static List<? extends DynamicOps<?>> dynamicOps() { ++ return Stream.of( ++ NbtOps.INSTANCE, ++ JavaOps.INSTANCE, ++ JsonOps.INSTANCE ++ ) ++ .map(ops -> AbstractTestingBase.REGISTRY_CUSTOM.createSerializationContext(ops)) ++ .toList(); ++ } ++ ++ @ParameterizedTest(name = PARAMETERIZED_NAME) ++ @MethodSource({"invalidData"}) ++ void invalidThrows(final Tag input) { ++ assertThrows(RuntimeException.class, () -> { ++ require( ++ COMPONENT_CODEC.decode(NbtOps.INSTANCE, input), ++ msg -> "Failed to decode " + input + ": " + msg ++ ); ++ }); ++ } ++ ++ static <A, O> void testDirectRoundTrip(final DynamicOps<O> ops, final Codec<A> codec, final A adventure) { ++ final O encoded = require( ++ codec.encodeStart(ops, adventure), ++ msg -> "Failed to encode " + adventure + ": " + msg ++ ); ++ final Pair<A, O> roundTripResult = require( ++ codec.decode(ops, encoded), ++ msg -> "Failed to decode " + encoded + ": " + msg ++ ); ++ assertEquals(adventure, roundTripResult.getFirst()); ++ } ++ ++ static <A, M, O> void testMinecraftRoundTrip(final DynamicOps<O> ops, final Codec<A> adventureCodec, final Codec<M> minecraftCodec, final A adventure) { ++ final O encoded = require( ++ adventureCodec.encodeStart(ops, adventure), ++ msg -> "Failed to encode " + adventure + ": " + msg ++ ); ++ final M minecraftResult = require( ++ minecraftCodec.decode(ops, encoded), ++ msg -> "Failed to decode to Minecraft: " + encoded + "; " + msg ++ ).getFirst(); ++ final O minecraftReEncoded = require( ++ minecraftCodec.encodeStart(ops, minecraftResult), ++ msg -> "Failed to re-encode Minecraft: " + minecraftResult + "; " + msg ++ ); ++ final Pair<A, O> roundTripResult = require( ++ adventureCodec.decode(ops, minecraftReEncoded), ++ msg -> "Failed to decode " + minecraftReEncoded + ": " + msg ++ ); ++ assertEquals(adventure, roundTripResult.getFirst()); ++ } ++ ++ static <R> R require(final DataResult<R> result, final Function<String, String> errorMessage) { ++ return result.getOrThrow(s -> new RuntimeException(errorMessage.apply(s))); ++ } ++ ++ static List<Tag> invalidData() { ++ return List.of( ++ IntTag.valueOf(-1), ++ ByteTag.ZERO, ++ new CompoundTag(), ++ new ListTag() ++ ); ++ } ++ ++ static List<Style> testStyles() { ++ return List.of( ++ Style.empty(), ++ style(color(0x0a1ab9)), ++ style(NamedTextColor.LIGHT_PURPLE), ++ style(TextDecoration.BOLD), ++ style(TextDecoration.BOLD.withState(false)), ++ style(TextDecoration.BOLD.withState(TextDecoration.State.NOT_SET)), ++ style() ++ .font(key("kyori", "kittens")) ++ .color(NamedTextColor.RED) ++ .decoration(TextDecoration.BOLD, true) ++ .clickEvent(openUrl("https://github.com")) ++ .build(), ++ style() ++ .hoverEvent(HoverEvent.showEntity(HoverEvent.ShowEntity.showEntity( ++ Key.key(Key.MINECRAFT_NAMESPACE, "pig"), ++ UUID.randomUUID(), ++ Component.text("Dolores", TextColor.color(0x0a1ab9)) ++ ))) ++ .build() ++ ); ++ } ++ ++ @Retention(RetentionPolicy.RUNTIME) ++ @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) ++ @MethodParameterSource({ ++ "testTexts", "testTranslatables", "testKeybinds", "testScores", ++ "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts" ++ }) ++ @interface TestComponents { ++ } ++ ++ static List<Component> testTexts() { ++ return List.of( ++ Component.empty(), ++ text("Hello, world."), ++ text().content("c") ++ .color(NamedTextColor.GOLD) ++ .append(text("o", NamedTextColor.DARK_AQUA)) ++ .append(text("l", NamedTextColor.LIGHT_PURPLE)) ++ .append(text("o", NamedTextColor.DARK_PURPLE)) ++ .append(text("u", NamedTextColor.BLUE)) ++ .append(text("r", NamedTextColor.DARK_GREEN)) ++ .append(text("s", NamedTextColor.RED)) ++ .build(), ++ text().content("This is a test.") ++ .color(NamedTextColor.DARK_PURPLE) ++ .hoverEvent(HoverEvent.showText(text("A test."))) ++ .append(text(" ")) ++ .append(text("A what?", NamedTextColor.DARK_AQUA)) ++ .build(), ++ text().append(text("Hello")).build(), ++ miniMessage().deserialize("<rainbow>|||||||||||||||||||||||<bold>|||||||||||||</bold>|||||||||") ++ ); ++ } ++ ++ static List<Component> testTranslatables() { ++ final String key = "multiplayer.player.left"; ++ final UUID id = UUID.fromString("eb121687-8b1a-4944-bd4d-e0a818d9dfe2"); ++ final String name = "kashike"; ++ final String command = String.format("/msg %s ", name); ++ ++ return List.of( ++ translatable(key), ++ translatable() ++ .key("thisIsA") ++ .fallback("This is a test.") ++ .build(), ++ translatable(key, numeric(Integer.MAX_VALUE), text("HEY")), // boolean doesn't work in vanilla, can't test here ++ translatable( ++ key, ++ text().content(name) ++ .clickEvent(suggestCommand(command)) ++ .hoverEvent(showEntity(HoverEvent.ShowEntity.showEntity( ++ key("minecraft", "player"), ++ id, ++ text(name) ++ ))) ++ .build() ++ ).color(NamedTextColor.YELLOW) ++ ); ++ } ++ ++ static List<Component> testKeybinds() { ++ return List.of(keybind("key.jump")); ++ } ++ ++ static List<Component> testScores() { ++ final String name = "abc"; ++ final String objective = "def"; ++ ++ return List.of(score(name, objective)); ++ } ++ ++ static List<Component> testSelectors() { ++ final String selector = "@p"; ++ ++ return List.of( ++ selector(selector), ++ selector(selector, text(',')) ++ ); ++ } ++ ++ static List<Component> testBlockNbts() { ++ return List.of( ++ blockNBT().nbtPath("abc").localPos(1.23d, 2.0d, 3.89d).build(), ++ blockNBT().nbtPath("xyz").absoluteWorldPos(4, 5, 6).interpret(true).build(), ++ blockNBT().nbtPath("eeee").relativeWorldPos(7, 83, 900) ++ .separator(text(';')) ++ .build(), ++ blockNBT().nbtPath("qwert").worldPos( ++ BlockNBTComponent.WorldPos.Coordinate.absolute(12), ++ BlockNBTComponent.WorldPos.Coordinate.relative(3), ++ BlockNBTComponent.WorldPos.Coordinate.absolute(1200) ++ ).build() ++ ); ++ } ++ ++ static List<Component> testEntityNbts() { ++ return List.of( ++ entityNBT().nbtPath("abc").selector("test").build(), ++ entityNBT().nbtPath("abc").selector("test").separator(text(',')).build(), ++ entityNBT().nbtPath("abc").selector("test").interpret(true).build() ++ ); ++ } ++ ++ static List<Component> testStorageNbts() { ++ return List.of( ++ storageNBT().nbtPath("abc").storage(key("doom:apple")).build(), ++ storageNBT().nbtPath("abc").storage(key("doom:apple")).separator(text(", ")).build(), ++ storageNBT().nbtPath("abc").storage(key("doom:apple")).interpret(true).build() ++ ); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java b/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..98114b8fdc441f2a1642abf230457c2b5208bb5c +--- /dev/null ++++ b/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java +@@ -0,0 +1,23 @@ ++package io.papermc.paper.adventure; ++ ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++ ++public class ComponentServicesTest { ++ ++ @Test ++ public void testPlainTextComponentSerializerProvider() { ++ assertEquals("Done", PlainTextComponentSerializer.plainText().serialize(Component.translatable("narrator.loading.done"))); ++ } ++ ++ @Test ++ public void testLegacyComponentSerializerProvider() { ++ assertEquals("§cDone", LegacyComponentSerializer.legacySection().serialize(Component.translatable("narrator.loading.done", NamedTextColor.RED))); ++ assertEquals("&cDone", LegacyComponentSerializer.legacyAmpersand().serialize(Component.translatable("narrator.loading.done", NamedTextColor.RED))); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/util/MethodParameterSource.java b/src/test/java/io/papermc/paper/util/MethodParameterSource.java +index 6cbf11c898439834cffb99ef84e5df1494356809..381668df2f58df82ac6da85796752934e92d0796 100644 +--- a/src/test/java/io/papermc/paper/util/MethodParameterSource.java ++++ b/src/test/java/io/papermc/paper/util/MethodParameterSource.java +@@ -4,11 +4,13 @@ import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; ++import org.intellij.lang.annotations.Language; + import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource; + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) + @CartesianArgumentsSource(MethodParameterProvider.class) + public @interface MethodParameterSource { ++ @Language("jvm-method-name") + String[] value() default {}; + } diff --git a/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch b/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch new file mode 100644 index 0000000000..720176d247 --- /dev/null +++ b/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch @@ -0,0 +1,700 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Minecrell <[email protected]> +Date: Fri, 9 Jun 2017 19:03:43 +0200 +Subject: [PATCH] Use TerminalConsoleAppender for console improvements + +Rewrite console improvements (console colors, tab completion, +persistent input line, ...) using JLine 3.x and TerminalConsoleAppender. + +Also uses the new ANSIComponentSerializer to serialize components when +logging them via the ComponentLogger, or when sending messages to the +console, for hex color support. + +New features: + - Support console colors for Vanilla commands + - Add console colors for warnings and errors + - Server can now be turned off safely using CTRL + C. JLine catches + the signal and the implementation shuts down the server cleanly. + - Support console colors and persistent input line when running in + IntelliJ IDEA + +Other changes: + - Server starts 1-2 seconds faster thanks to optimizations in Log4j + configuration + +Co-Authored-By: Emilia Kond <[email protected]> + +diff --git a/build.gradle.kts b/build.gradle.kts +index 94dfc105b197c9eda481cbe90bb48b71c845e702..0a05e753ff5e7b1d741c7719524715d7364cac4f 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -5,9 +5,29 @@ plugins { + `maven-publish` + } + ++val log4jPlugins = sourceSets.create("log4jPlugins") ++configurations.named(log4jPlugins.compileClasspathConfigurationName) { ++ extendsFrom(configurations.compileClasspath.get()) ++} ++val alsoShade: Configuration by configurations.creating ++ + dependencies { + implementation(project(":paper-api")) +- implementation("jline:jline:2.12.1") ++ // Paper start ++ implementation("org.jline:jline-terminal-jansi:3.21.0") ++ implementation("net.minecrell:terminalconsoleappender:1.3.0") ++ implementation("net.kyori:adventure-text-serializer-ansi:4.17.0") // Keep in sync with adventureVersion from Paper-API build file ++ /* ++ Required to add the missing Log4j2Plugins.dat file from log4j-core ++ which has been removed by Mojang. Without it, log4j has to classload ++ all its classes to check if they are plugins. ++ Scanning takes about 1-2 seconds so adding this speeds up the server start. ++ */ ++ runtimeOnly("org.apache.logging.log4j:log4j-core:2.19.0") ++ log4jPlugins.annotationProcessorConfigurationName("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - Needed to generate meta for our Log4j plugins ++ runtimeOnly(log4jPlugins.output) ++ alsoShade(log4jPlugins.output) ++ // Paper end + implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") // Paper - remove exclusion + implementation("org.ow2.asm:asm-commons:9.7") + implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files +@@ -78,6 +98,19 @@ tasks.check { + dependsOn(scanJar) + } + // Paper end ++// Paper start - use TCA for console improvements ++tasks.serverJar { ++ from(alsoShade.elements.map { ++ it.map { f -> ++ if (f.asFile.isFile) { ++ zipTree(f.asFile) ++ } else { ++ f.asFile ++ } ++ } ++ }) ++} ++// Paper end - use TCA for console improvements + + tasks.test { + exclude("org/bukkit/craftbukkit/inventory/ItemStack*Test.class") +diff --git a/src/log4jPlugins/java/io/papermc/paper/console/StripANSIConverter.java b/src/log4jPlugins/java/io/papermc/paper/console/StripANSIConverter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..91547f6e6fe90006713beb2818da634304bdd236 +--- /dev/null ++++ b/src/log4jPlugins/java/io/papermc/paper/console/StripANSIConverter.java +@@ -0,0 +1,51 @@ ++package io.papermc.paper.console; ++ ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.config.Configuration; ++import org.apache.logging.log4j.core.config.plugins.Plugin; ++import org.apache.logging.log4j.core.layout.PatternLayout; ++import org.apache.logging.log4j.core.pattern.ConverterKeys; ++import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; ++import org.apache.logging.log4j.core.pattern.PatternConverter; ++import org.apache.logging.log4j.core.pattern.PatternFormatter; ++import org.apache.logging.log4j.core.pattern.PatternParser; ++ ++import java.util.List; ++import java.util.regex.Pattern; ++ ++@Plugin(name = "stripAnsi", category = PatternConverter.CATEGORY) ++@ConverterKeys({"stripAnsi"}) ++public final class StripANSIConverter extends LogEventPatternConverter { ++ final private Pattern ANSI_PATTERN = Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); ++ ++ private final List<PatternFormatter> formatters; ++ ++ private StripANSIConverter(List<PatternFormatter> formatters) { ++ super("stripAnsi", null); ++ this.formatters = formatters; ++ } ++ ++ @Override ++ public void format(LogEvent event, StringBuilder toAppendTo) { ++ int start = toAppendTo.length(); ++ for (PatternFormatter formatter : formatters) { ++ formatter.format(event, toAppendTo); ++ } ++ String content = toAppendTo.substring(start); ++ content = ANSI_PATTERN.matcher(content).replaceAll(""); ++ ++ toAppendTo.setLength(start); ++ toAppendTo.append(content); ++ } ++ ++ public static StripANSIConverter newInstance(Configuration config, String[] options) { ++ if (options.length != 1) { ++ LOGGER.error("Incorrect number of options on stripAnsi. Expected exactly 1, received " + options.length); ++ return null; ++ } ++ ++ PatternParser parser = PatternLayout.createPatternParser(config); ++ List<PatternFormatter> formatters = parser.parse(options[0]); ++ return new StripANSIConverter(formatters); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a4070b59e261f0f1ac4beec47b11492f4724bf27 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +@@ -0,0 +1,41 @@ ++package com.destroystokyo.paper.console; ++ ++import net.minecraft.server.dedicated.DedicatedServer; ++import net.minecrell.terminalconsole.SimpleTerminalConsole; ++import org.bukkit.craftbukkit.command.ConsoleCommandCompleter; ++import org.jline.reader.LineReader; ++import org.jline.reader.LineReaderBuilder; ++ ++public final class PaperConsole extends SimpleTerminalConsole { ++ ++ private final DedicatedServer server; ++ ++ public PaperConsole(DedicatedServer server) { ++ this.server = server; ++ } ++ ++ @Override ++ protected LineReader buildReader(LineReaderBuilder builder) { ++ return super.buildReader(builder ++ .appName("Paper") ++ .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) ++ .completer(new ConsoleCommandCompleter(this.server)) ++ ); ++ } ++ ++ @Override ++ protected boolean isRunning() { ++ return !this.server.isStopped() && this.server.isRunning(); ++ } ++ ++ @Override ++ protected void runCommand(String command) { ++ this.server.handleConsoleInput(command, this.server.createCommandSourceStack()); ++ } ++ ++ @Override ++ protected void shutdown() { ++ this.server.halt(false); ++ } ++ ++} +diff --git a/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java b/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8f07539a82f449ad217e316a7513a1708781fb63 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java +@@ -0,0 +1,26 @@ ++package com.destroystokyo.paper.console; ++ ++import net.kyori.adventure.audience.MessageType; ++import net.kyori.adventure.identity.Identity; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; ++import org.apache.logging.log4j.LogManager; ++import org.bukkit.craftbukkit.command.CraftConsoleCommandSender; ++ ++public class TerminalConsoleCommandSender extends CraftConsoleCommandSender { ++ ++ private static final ComponentLogger LOGGER = ComponentLogger.logger(LogManager.getRootLogger().getName()); ++ ++ @Override ++ public void sendRawMessage(String message) { ++ final Component msg = LegacyComponentSerializer.legacySection().deserialize(message); ++ this.sendMessage(Identity.nil(), msg, MessageType.SYSTEM); ++ } ++ ++ @Override ++ public void sendMessage(Identity identity, Component message, MessageType type) { ++ LOGGER.info(message); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java +index 7397918cf747bc2352bf5bb112a71e7f6844e0e0..0f9b744a977ec7ab8f138989b2336117b3de1412 100644 +--- a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java ++++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java +@@ -31,6 +31,7 @@ import net.kyori.adventure.text.flattener.ComponentFlattener; + import net.kyori.adventure.text.format.Style; + import net.kyori.adventure.text.format.TextColor; + import net.kyori.adventure.text.serializer.ComponentSerializer; ++import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; + import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; + import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; + import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +@@ -128,6 +129,7 @@ public final class PaperAdventure { + public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger + @Deprecated + public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build(); ++ public static final ANSIComponentSerializer ANSI_SERIALIZER = ANSIComponentSerializer.builder().flattener(FLATTENER).build(); + public static final Codec<Tag, String, CommandSyntaxException, RuntimeException> NBT_CODEC = new Codec<>() { + @Override + public @NotNull Tag decode(final @NotNull String encoded) throws CommandSyntaxException { +diff --git a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java +index 8323f135d6bf2e1f12525e05094ffa3f2420e7e1..a143ea1e58464a3122fbd8ccafe417bdb3c31c78 100644 +--- a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java ++++ b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java +@@ -1,9 +1,11 @@ + package io.papermc.paper.adventure.providers; + + import io.papermc.paper.adventure.PaperAdventure; ++import java.util.Locale; + import net.kyori.adventure.text.Component; + import net.kyori.adventure.text.logger.slf4j.ComponentLogger; + import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider; ++import net.kyori.adventure.translation.GlobalTranslator; + import org.jetbrains.annotations.NotNull; + import org.slf4j.LoggerFactory; + +@@ -15,6 +17,6 @@ public class ComponentLoggerProviderImpl implements ComponentLoggerProvider { + } + + private String serialize(final Component message) { +- return PaperAdventure.asPlain(message, null); ++ return PaperAdventure.ANSI_SERIALIZER.serialize(GlobalTranslator.render(message, Locale.getDefault())); + } + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 0742aaf07f37e51d24295e7819ac6cec961c7626..e82218de79759dfaa7fe2a5a78e150f135bf01a3 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -154,7 +154,7 @@ import com.mojang.serialization.Dynamic; + import com.mojang.serialization.Lifecycle; + import java.io.File; + import java.util.Random; +-import jline.console.ConsoleReader; ++// import jline.console.ConsoleReader; // Paper + import joptsimple.OptionSet; + import net.minecraft.nbt.NbtException; + import net.minecraft.nbt.ReportedNbtException; +@@ -296,7 +296,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + public org.bukkit.craftbukkit.CraftServer server; + public OptionSet options; + public org.bukkit.command.ConsoleCommandSender console; +- public ConsoleReader reader; + public static int currentTick = (int) (System.currentTimeMillis() / 50); + public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>(); + public int autosavePeriod; +@@ -384,7 +383,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.options = options; + this.worldLoader = worldLoader; + this.vanillaCommandDispatcher = worldstem.dataPackResources().commands; // CraftBukkit ++ // Paper start - Handled by TerminalConsoleAppender + // Try to see if we're actually running in a terminal, disable jline if not ++ /* + if (System.console() == null && System.getProperty("jline.terminal") == null) { + System.setProperty("jline.terminal", "jline.UnsupportedTerminal"); + Main.useJline = false; +@@ -405,6 +406,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + MinecraftServer.LOGGER.warn((String) null, ex); + } + } ++ */ ++ // Paper end + Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this)); + // CraftBukkit end + this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files +@@ -1120,7 +1123,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + org.spigotmc.WatchdogThread.doStop(); // Spigot + // CraftBukkit start - Restore terminal to original settings + try { +- this.reader.getTerminal().restore(); ++ net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender + } catch (Exception ignored) { + } + // CraftBukkit end +@@ -1664,7 +1667,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + @Override + public void sendSystemMessage(Component message) { +- MinecraftServer.LOGGER.info(message.getString()); ++ MinecraftServer.LOGGER.info(io.papermc.paper.adventure.PaperAdventure.ANSI_SERIALIZER.serialize(io.papermc.paper.adventure.PaperAdventure.asAdventure(message))); // Paper - Log message with colors + } + + public KeyPair getKeyPair() { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index cd26616aba6abd44abc5eb8b01cc96f29248aecd..b41eb920b5665b7a1b7cd9f38955c31eeb350847 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -112,6 +112,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + if (!org.bukkit.craftbukkit.Main.useConsole) { + return; + } ++ // Paper start - Use TerminalConsoleAppender ++ new com.destroystokyo.paper.console.PaperConsole(DedicatedServer.this).start(); ++ /* + jline.console.ConsoleReader bufferedreader = DedicatedServer.this.reader; + + // MC-33041, SPIGOT-5538: if System.in is not valid due to javaw, then return +@@ -143,7 +146,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + continue; + } + if (s.trim().length() > 0) { // Trim to filter lines which are just spaces +- DedicatedServer.this.handleConsoleInput(s, DedicatedServer.this.createCommandSourceStack()); ++ DedicatedServer.this.issueCommand(s, DedicatedServer.this.getServerCommandListener()); + } + // CraftBukkit end + } +@@ -151,6 +154,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + DedicatedServer.LOGGER.error("Exception handling console input", ioexception); + } + ++ */ ++ // Paper end + } + }; + +@@ -162,6 +167,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + global.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler()); + ++ // Paper start - Not needed with TerminalConsoleAppender ++ final org.apache.logging.log4j.Logger logger = LogManager.getRootLogger(); ++ /* + final org.apache.logging.log4j.core.Logger logger = ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()); + for (org.apache.logging.log4j.core.Appender appender : logger.getAppenders().values()) { + if (appender instanceof org.apache.logging.log4j.core.appender.ConsoleAppender) { +@@ -172,6 +180,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + TerminalConsoleWriterThread writerThread = new TerminalConsoleWriterThread(System.out, this.reader); + this.reader.setCompletionHandler(new TerminalCompletionHandler(writerThread, this.reader.getCompletionHandler())); + writerThread.start(); ++ */ ++ // Paper end - Not needed with TerminalConsoleAppender + + System.setOut(IoBuilder.forLogger(logger).setLevel(Level.INFO).buildPrintStream()); + System.setErr(IoBuilder.forLogger(logger).setLevel(Level.WARN).buildPrintStream()); +diff --git a/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java b/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java +index 3d92c61f7781221cfdc0324d11bd0088954e4a68..84a2c6c397604279ba821286f5c3c855e6041400 100644 +--- a/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java ++++ b/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java +@@ -166,7 +166,7 @@ public class MinecraftServerGui extends JComponent { + this.finalizers.forEach(Runnable::run); + } + +- private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]"); // CraftBukkit ++ private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); // CraftBukkit // Paper + public void print(JTextArea textArea, JScrollPane scrollPane, String message) { + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(() -> { +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 9113c183f1e977f71eea95a891618883b075ce5a..9807c5b2b248a62a476bfe3ae023d57d35811049 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -161,8 +161,7 @@ public abstract class PlayerList { + + public PlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registryManager, PlayerDataStorage saveHandler, int maxPlayers) { + this.cserver = server.server = new CraftServer((DedicatedServer) server, this); +- server.console = org.bukkit.craftbukkit.command.ColouredConsoleSender.getInstance(); +- server.reader.addCompleter(new org.bukkit.craftbukkit.command.ConsoleCommandCompleter(server.server)); ++ server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper + // CraftBukkit end + + this.bans = new UserBanList(PlayerList.USERBANLIST_FILE); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 0e3ff653211b0210f8679b475d5f62eecbdfd946..7eb94216cc556ad4c6c76ffab0ca81861d2c1883 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -43,7 +43,7 @@ import java.util.logging.Level; + import java.util.logging.Logger; + import java.util.stream.Collectors; + import javax.imageio.ImageIO; +-import jline.console.ConsoleReader; ++// import jline.console.ConsoleReader; + import net.minecraft.advancements.AdvancementHolder; + import net.minecraft.commands.CommandSourceStack; + import net.minecraft.commands.Commands; +@@ -1346,9 +1346,13 @@ public final class CraftServer implements Server { + return this.logger; + } + ++ // Paper start - JLine update ++ /* + public ConsoleReader getReader() { + return this.console.reader; + } ++ */ ++ // Paper end + + @Override + public PluginCommand getPluginCommand(String name) { +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 985b77911d03bc60a0210b796e901f31e2676268..9de87edb75947382fda114df883fb4b31c1a7141 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -13,7 +13,6 @@ import java.util.logging.Logger; + import joptsimple.OptionParser; + import joptsimple.OptionSet; + import joptsimple.util.PathConverter; +-import org.fusesource.jansi.AnsiConsole; + + public class Main { + public static boolean useJline = true; +@@ -207,6 +206,8 @@ public class Main { + } + + try { ++ // Paper start - Handled by TerminalConsoleAppender ++ /* + // This trick bypasses Maven Shade's clever rewriting of our getProperty call when using String literals + String jline_UnsupportedTerminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 'U', 'n', 's', 'u', 'p', 'p', 'o', 'r', 't', 'e', 'd', 'T', 'e', 'r', 'm', 'i', 'n', 'a', 'l'}); + String jline_terminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l'}); +@@ -224,9 +225,18 @@ public class Main { + // This ensures the terminal literal will always match the jline implementation + System.setProperty(jline.TerminalFactory.JLINE_TERMINAL, jline.UnsupportedTerminal.class.getName()); + } ++ */ ++ ++ if (options.has("nojline")) { ++ System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); ++ useJline = false; ++ } ++ // Paper end + + if (options.has("noconsole")) { + Main.useConsole = false; ++ useJline = false; // Paper ++ System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper + } + + if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { +@@ -242,6 +252,7 @@ public class Main { + } + } + ++ System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows + System.out.println("Loading libraries, please wait..."); + net.minecraft.server.Main.main(options); + } catch (Throwable t) { +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java b/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java +index bcf1c36d07b79520a39643d3a01020a67b1c9ef2..217e7e3b9db04c7fc5f6518f39cc9d3488f9128d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java +@@ -5,15 +5,13 @@ import java.util.EnumMap; + import java.util.Map; + import java.util.regex.Matcher; + import java.util.regex.Pattern; +-import jline.Terminal; ++//import jline.Terminal; + import org.bukkit.Bukkit; + import org.bukkit.ChatColor; + import org.bukkit.command.ConsoleCommandSender; + import org.bukkit.craftbukkit.CraftServer; +-import org.fusesource.jansi.Ansi; +-import org.fusesource.jansi.Ansi.Attribute; + +-public class ColouredConsoleSender extends CraftConsoleCommandSender { ++public class ColouredConsoleSender /*extends CraftConsoleCommandSender */{/* // Paper - disable + private final Terminal terminal; + private final Map<ChatColor, String> replacements = new EnumMap<ChatColor, String>(ChatColor.class); + private final ChatColor[] colors = ChatColor.values(); +@@ -93,5 +91,5 @@ public class ColouredConsoleSender extends CraftConsoleCommandSender { + } else { + return new ColouredConsoleSender(); + } +- } ++ }*/ // Paper + } +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +index 0b4c62387c1093652ac15b64a8703249de4cf088..d24acf28f5ed023acc550bcf877e4b9800ec8c9f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +@@ -4,50 +4,73 @@ import java.util.Collections; + import java.util.List; + import java.util.concurrent.ExecutionException; + import java.util.logging.Level; +-import jline.console.completer.Completer; ++import net.minecraft.server.dedicated.DedicatedServer; + import org.bukkit.craftbukkit.CraftServer; + import org.bukkit.craftbukkit.util.Waitable; ++ ++// Paper start - JLine update ++import org.jline.reader.Candidate; ++import org.jline.reader.Completer; ++import org.jline.reader.LineReader; ++import org.jline.reader.ParsedLine; ++// Paper end + import org.bukkit.event.server.TabCompleteEvent; + + public class ConsoleCommandCompleter implements Completer { +- private final CraftServer server; ++ private final DedicatedServer server; // Paper - CraftServer -> DedicatedServer + +- public ConsoleCommandCompleter(CraftServer server) { ++ public ConsoleCommandCompleter(DedicatedServer server) { // Paper - CraftServer -> DedicatedServer + this.server = server; + } + ++ // Paper start - Change method signature for JLine update + @Override +- public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { ++ public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { ++ final CraftServer server = this.server.server; ++ final String buffer = line.line(); ++ // Paper end + Waitable<List<String>> waitable = new Waitable<List<String>>() { + @Override + protected List<String> evaluate() { +- List<String> offers = ConsoleCommandCompleter.this.server.getCommandMap().tabComplete(ConsoleCommandCompleter.this.server.getConsoleSender(), buffer); ++ List<String> offers = server.getCommandMap().tabComplete(server.getConsoleSender(), buffer); // Paper - Remove "this." + +- TabCompleteEvent tabEvent = new TabCompleteEvent(ConsoleCommandCompleter.this.server.getConsoleSender(), buffer, (offers == null) ? Collections.EMPTY_LIST : offers); +- ConsoleCommandCompleter.this.server.getPluginManager().callEvent(tabEvent); ++ TabCompleteEvent tabEvent = new TabCompleteEvent(server.getConsoleSender(), buffer, (offers == null) ? Collections.EMPTY_LIST : offers); // Paper - Remove "this." ++ server.getPluginManager().callEvent(tabEvent); // Paper - Remove "this." + + return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions(); + } + }; +- this.server.getServer().processQueue.add(waitable); ++ server.getServer().processQueue.add(waitable); // Paper - Remove "this." + try { + List<String> offers = waitable.get(); + if (offers == null) { +- return cursor; ++ return; // Paper - Method returns void ++ } ++ ++ // Paper start - JLine update ++ for (String completion : offers) { ++ if (completion.isEmpty()) { ++ continue; ++ } ++ ++ candidates.add(new Candidate(completion)); + } +- candidates.addAll(offers); ++ // Paper end + ++ // Paper start - JLine handles cursor now ++ /* + final int lastSpace = buffer.lastIndexOf(' '); + if (lastSpace == -1) { + return cursor - buffer.length(); + } else { + return cursor - (buffer.length() - lastSpace - 1); + } ++ */ ++ // Paper end + } catch (ExecutionException e) { +- this.server.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); ++ server.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); // Paper - Remove "this." + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } +- return cursor; + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +index 8390f5b5b957b5435efece26507a89756d0a7b3c..c6e8441e299f477ddb22c1ce2618710763978f1a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +@@ -16,7 +16,7 @@ public class ServerShutdownThread extends Thread { + this.server.close(); + } finally { + try { +- this.server.reader.getTerminal().restore(); ++ net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender + } catch (Exception e) { + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java b/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java +index 7d1c0810181c51ffa64f9a23b251e05c789870b0..09804513265e6db0c1f3501355335551100a36a8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java +@@ -4,14 +4,12 @@ import java.io.IOException; + import java.util.HashSet; + import java.util.List; + import java.util.Set; +-import jline.console.ConsoleReader; +-import jline.console.completer.CompletionHandler; + + /** + * SPIGOT-6705: Make sure we print the display line again on tab completion, so that the user does not get stuck on it + * e.g. The user needs to press y / n to continue + */ +-public class TerminalCompletionHandler implements CompletionHandler { ++public class TerminalCompletionHandler /* implements CompletionHandler */ { /* Paper - comment out whole class + + private final TerminalConsoleWriterThread writerThread; + private final CompletionHandler delegate; +@@ -50,4 +48,5 @@ public class TerminalCompletionHandler implements CompletionHandler { + + return result; + } ++*/ // Paper end - comment out whole class + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java b/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java +index 2e5101fca081548bb616bc5eaa0134698719b63a..6e4d50a4863d43a2d6a2d574350c74d0819df402 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java +@@ -7,13 +7,9 @@ import java.util.Locale; + import java.util.ResourceBundle; + import java.util.logging.Level; + import java.util.logging.Logger; +-import jline.console.ConsoleReader; +-import jline.console.completer.CandidateListCompletionHandler; + import org.bukkit.craftbukkit.Main; +-import org.fusesource.jansi.Ansi; +-import org.fusesource.jansi.Ansi.Erase; + +-public class TerminalConsoleWriterThread extends Thread { ++public class TerminalConsoleWriterThread /*extends Thread*/ {/* // Paper - Comment out entire class + private final ResourceBundle bundle = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName(), Locale.getDefault()); + private final ConsoleReader reader; + private final OutputStream output; +@@ -70,4 +66,5 @@ public class TerminalConsoleWriterThread extends Thread { + void setCompletion(int completion) { + this.completion = completion; + } ++*/ // Paper - Comment out entire class + } +diff --git a/src/main/resources/log4j2.component.properties b/src/main/resources/log4j2.component.properties +new file mode 100644 +index 0000000000000000000000000000000000000000..0694b21465fb9e4164e71862ff24b62241b191f2 +--- /dev/null ++++ b/src/main/resources/log4j2.component.properties +@@ -0,0 +1 @@ ++log4j.skipJansi=true +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 0ff3f750fb7f684c12080894cd8660f0455c658b..301874c1fe16c52ffa6228d79e6617d746e9a035 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -1,17 +1,14 @@ + <?xml version="1.0" encoding="UTF-8"?> + <Configuration status="WARN"> + <Appenders> +- <Console name="SysOut" target="SYSTEM_OUT"> +- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" /> +- </Console> + <Queue name="ServerGuiConsole"> + <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" /> + </Queue> +- <Queue name="TerminalConsole"> +- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" /> +- </Queue> ++ <TerminalConsole name="TerminalConsole"> ++ <PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" /> ++ </TerminalConsole> + <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"> +- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" /> ++ <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" /> + <Policies> + <TimeBasedTriggeringPolicy /> + <OnStartupTriggeringPolicy /> +@@ -24,10 +21,9 @@ + <filters> + <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" /> + </filters> +- <AppenderRef ref="SysOut" level="info"/> + <AppenderRef ref="File"/> +- <AppenderRef ref="ServerGuiConsole" level="info"/> + <AppenderRef ref="TerminalConsole" level="info"/> ++ <AppenderRef ref="ServerGuiConsole" level="info"/> + </Root> + </Loggers> + </Configuration> diff --git a/patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch b/patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch new file mode 100644 index 0000000000..714d232818 --- /dev/null +++ b/patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch @@ -0,0 +1,71 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Minecrell <[email protected]> +Date: Thu, 21 Sep 2017 16:14:55 +0200 +Subject: [PATCH] Handle plugin prefixes using Log4J configuration + +Display logger name in the console for all loggers except the +root logger, Bukkit's logger ("Minecraft") and Minecraft loggers. +Since plugins now use the plugin name as logger name this will +restore the plugin prefixes without having to prepend them manually +to the log messages. + +Logger prefixes are shown by default for all loggers except for +the root logger, the Minecraft/Mojang loggers and the Bukkit loggers. +This may cause additional prefixes to be disabled for plugins bypassing +the plugin logger. + +diff --git a/build.gradle.kts b/build.gradle.kts +index 0a05e753ff5e7b1d741c7719524715d7364cac4f..d82d1e90cbda544b3d20edcc13d1cb955c48f731 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -23,7 +23,7 @@ dependencies { + all its classes to check if they are plugins. + Scanning takes about 1-2 seconds so adding this speeds up the server start. + */ +- runtimeOnly("org.apache.logging.log4j:log4j-core:2.19.0") ++ implementation("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - implementation + log4jPlugins.annotationProcessorConfigurationName("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - Needed to generate meta for our Log4j plugins + runtimeOnly(log4jPlugins.output) + alsoShade(log4jPlugins.output) +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index fbbc08c5a189b99f8047e0f0f5cd31101149dbec..b717c9d8b6edc2cafc9281140913b7bdb6108cf0 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -288,7 +288,7 @@ public class SpigotConfig + private static void playerSample() + { + SpigotConfig.playerSample = SpigotConfig.getInt( "settings.sample-count", 12 ); +- System.out.println( "Server Ping Player Sample Count: " + SpigotConfig.playerSample ); ++ Bukkit.getLogger().log( Level.INFO, "Server Ping Player Sample Count: {0}", playerSample ); // Paper - Use logger + } + + public static int playerShuffle; +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 301874c1fe16c52ffa6228d79e6617d746e9a035..e073707a46397f62bedf1d413f9e5764e77dda6a 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -5,10 +5,22 @@ + <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" /> + </Queue> + <TerminalConsole name="TerminalConsole"> +- <PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" /> ++ <PatternLayout> ++ <LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx}"> ++ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix --> ++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang." ++ pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" /> ++ </LoggerNamePatternSelector> ++ </PatternLayout> + </TerminalConsole> + <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"> +- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" /> ++ <PatternLayout> ++ <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n"> ++ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix --> ++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang." ++ pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" /> ++ </LoggerNamePatternSelector> ++ </PatternLayout> + <Policies> + <TimeBasedTriggeringPolicy /> + <OnStartupTriggeringPolicy /> diff --git a/patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch b/patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch new file mode 100644 index 0000000000..4f654facd6 --- /dev/null +++ b/patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Minecrell <[email protected]> +Date: Sat, 23 Sep 2017 21:07:20 +0200 +Subject: [PATCH] Improve Log4J Configuration / Plugin Loggers + +Add full exceptions to log4j to not truncate stack traces + +Disable logger prefix for various plugins bypassing the plugin logger + +Some plugins bypass the plugin logger and add the plugin prefix +manually to the log message. Since they use other logger names +(e.g. qualified class names) these would now also appear in the +log. Disable the logger prefix for these plugins so the messages +show up correctly. + +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index e073707a46397f62bedf1d413f9e5764e77dda6a..ab1caec640128aa90f246e4bbecf5ca275e7982e 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -6,19 +6,21 @@ + </Queue> + <TerminalConsole name="TerminalConsole"> + <PatternLayout> +- <LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx}"> ++ <LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx{full}}"> + <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix --> +- <PatternMatch key=",net.minecraft.,Minecraft,com.mojang." +- pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" /> ++ <!-- Disable prefix for various plugins that bypass the plugin logger --> ++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang.,com.sk89q.,ru.tehkode.,Minecraft.AWE" ++ pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx{full}}" /> + </LoggerNamePatternSelector> + </PatternLayout> + </TerminalConsole> + <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"> + <PatternLayout> +- <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n"> ++ <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n%xEx{full}"> + <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix --> +- <PatternMatch key=",net.minecraft.,Minecraft,com.mojang." +- pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" /> ++ <!-- Disable prefix for various plugins that bypass the plugin logger --> ++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang.,com.sk89q.,ru.tehkode.,Minecraft.AWE" ++ pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n%xEx{full}" /> + </LoggerNamePatternSelector> + </PatternLayout> + <Policies> diff --git a/patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch b/patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch new file mode 100644 index 0000000000..1d920095cb --- /dev/null +++ b/patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <[email protected]> +Date: Thu, 12 Aug 2021 04:46:41 -0700 +Subject: [PATCH] Use AsyncAppender to keep logging IO off main thread + + +diff --git a/build.gradle.kts b/build.gradle.kts +index d82d1e90cbda544b3d20edcc13d1cb955c48f731..3bd5c2a2add9b462523beb9dfaf2eb5a00d470b9 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -34,6 +34,7 @@ dependencies { + implementation("commons-lang:commons-lang:2.6") + runtimeOnly("org.xerial:sqlite-jdbc:3.46.0.0") + runtimeOnly("com.mysql:mysql-connector-j:8.4.0") ++ runtimeOnly("com.lmax:disruptor:3.4.4") // Paper + + runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6") + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index ab1caec640128aa90f246e4bbecf5ca275e7982e..18e961a37b2830da6e5dab7aa35116b2f5215898 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -29,15 +29,18 @@ + </Policies> + <DefaultRolloverStrategy max="1000"/> + </RollingRandomAccessFile> ++ <Async name="Async"> ++ <AppenderRef ref="File"/> ++ <AppenderRef ref="TerminalConsole" level="info"/> ++ <AppenderRef ref="ServerGuiConsole" level="info"/> ++ </Async> + </Appenders> + <Loggers> + <Root level="info"> + <filters> + <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" /> + </filters> +- <AppenderRef ref="File"/> +- <AppenderRef ref="TerminalConsole" level="info"/> +- <AppenderRef ref="ServerGuiConsole" level="info"/> ++ <AppenderRef ref="Async"/> + </Root> + </Loggers> + </Configuration> diff --git a/patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch b/patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch new file mode 100644 index 0000000000..7e2c824c7e --- /dev/null +++ b/patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch @@ -0,0 +1,581 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <[email protected]> +Date: Sun, 20 Jun 2021 18:19:09 -0700 +Subject: [PATCH] Deobfuscate stacktraces in log messages, crash reports, and + etc. + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 3bd5c2a2add9b462523beb9dfaf2eb5a00d470b9..a2bb659ae3a502d4c181d1ccbd15eefc38e4823b 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -45,6 +45,7 @@ dependencies { + testImplementation("org.mockito:mockito-core:5.11.0") + testImplementation("org.ow2.asm:asm-tree:9.7") + testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest ++ implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling + } + + paperweight { +diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java b/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java +new file mode 100644 +index 0000000000000000000000000000000000000000..66b6011ee3684695b2ab9292961c80bf2a420ee9 +--- /dev/null ++++ b/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java +@@ -0,0 +1,66 @@ ++package io.papermc.paper.logging; ++ ++import java.lang.invoke.MethodHandle; ++import java.lang.invoke.MethodHandles; ++import java.lang.invoke.VarHandle; ++import org.apache.logging.log4j.core.Core; ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy; ++import org.apache.logging.log4j.core.config.plugins.Plugin; ++import org.apache.logging.log4j.core.config.plugins.PluginFactory; ++import org.apache.logging.log4j.core.impl.Log4jLogEvent; ++import org.checkerframework.checker.nullness.qual.NonNull; ++ ++@Plugin( ++ name = "StacktraceDeobfuscatingRewritePolicy", ++ category = Core.CATEGORY_NAME, ++ elementType = "rewritePolicy", ++ printObject = true ++) ++public final class StacktraceDeobfuscatingRewritePolicy implements RewritePolicy { ++ private static final MethodHandle DEOBFUSCATE_THROWABLE; ++ ++ static { ++ try { ++ final Class<?> cls = Class.forName("io.papermc.paper.util.StacktraceDeobfuscator"); ++ final MethodHandles.Lookup lookup = MethodHandles.lookup(); ++ final VarHandle instanceHandle = lookup.findStaticVarHandle(cls, "INSTANCE", cls); ++ final Object deobfuscator = instanceHandle.get(); ++ DEOBFUSCATE_THROWABLE = lookup ++ .unreflect(cls.getDeclaredMethod("deobfuscateThrowable", Throwable.class)) ++ .bindTo(deobfuscator); ++ } catch (final ReflectiveOperationException ex) { ++ throw new IllegalStateException(ex); ++ } ++ } ++ ++ private StacktraceDeobfuscatingRewritePolicy() { ++ } ++ ++ @Override ++ public @NonNull LogEvent rewrite(final @NonNull LogEvent rewrite) { ++ final Throwable thrown = rewrite.getThrown(); ++ if (thrown != null) { ++ deobfuscateThrowable(thrown); ++ return new Log4jLogEvent.Builder(rewrite) ++ .setThrownProxy(null) ++ .build(); ++ } ++ return rewrite; ++ } ++ ++ private static void deobfuscateThrowable(final Throwable thrown) { ++ try { ++ DEOBFUSCATE_THROWABLE.invoke(thrown); ++ } catch (final Error e) { ++ throw e; ++ } catch (final Throwable e) { ++ throw new RuntimeException(e); ++ } ++ } ++ ++ @PluginFactory ++ public static @NonNull StacktraceDeobfuscatingRewritePolicy createPolicy() { ++ return new StacktraceDeobfuscatingRewritePolicy(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/ObfHelper.java b/src/main/java/io/papermc/paper/util/ObfHelper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9e6d48335b37fa5204bfebf396d748089884555b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/ObfHelper.java +@@ -0,0 +1,156 @@ ++package io.papermc.paper.util; ++ ++import java.io.IOException; ++import java.io.InputStream; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Objects; ++import java.util.Set; ++import java.util.stream.Collectors; ++import net.neoforged.srgutils.IMappingFile; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public enum ObfHelper { ++ INSTANCE; ++ ++ private final @Nullable Map<String, ClassMapping> mappingsByObfName; ++ private final @Nullable Map<String, ClassMapping> mappingsByMojangName; ++ ++ ObfHelper() { ++ final @Nullable Set<ClassMapping> maps = loadMappingsIfPresent(); ++ if (maps != null) { ++ this.mappingsByObfName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::obfName, map -> map)); ++ this.mappingsByMojangName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::mojangName, map -> map)); ++ } else { ++ this.mappingsByObfName = null; ++ this.mappingsByMojangName = null; ++ } ++ } ++ ++ public @Nullable Map<String, ClassMapping> mappingsByObfName() { ++ return this.mappingsByObfName; ++ } ++ ++ public @Nullable Map<String, ClassMapping> mappingsByMojangName() { ++ return this.mappingsByMojangName; ++ } ++ ++ /** ++ * Attempts to get the obf name for a given class by its Mojang name. Will ++ * return the input string if mappings are not present. ++ * ++ * @param fullyQualifiedMojangName fully qualified class name (dotted) ++ * @return mapped or original fully qualified (dotted) class name ++ */ ++ public String reobfClassName(final String fullyQualifiedMojangName) { ++ if (this.mappingsByMojangName == null) { ++ return fullyQualifiedMojangName; ++ } ++ ++ final ClassMapping map = this.mappingsByMojangName.get(fullyQualifiedMojangName); ++ if (map == null) { ++ return fullyQualifiedMojangName; ++ } ++ ++ return map.obfName(); ++ } ++ ++ /** ++ * Attempts to get the Mojang name for a given class by its obf name. Will ++ * return the input string if mappings are not present. ++ * ++ * @param fullyQualifiedObfName fully qualified class name (dotted) ++ * @return mapped or original fully qualified (dotted) class name ++ */ ++ public String deobfClassName(final String fullyQualifiedObfName) { ++ if (this.mappingsByObfName == null) { ++ return fullyQualifiedObfName; ++ } ++ ++ final ClassMapping map = this.mappingsByObfName.get(fullyQualifiedObfName); ++ if (map == null) { ++ return fullyQualifiedObfName; ++ } ++ ++ return map.mojangName(); ++ } ++ ++ private static @Nullable Set<ClassMapping> loadMappingsIfPresent() { ++ try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) { ++ if (mappingsInputStream == null) { ++ return null; ++ } ++ final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot ++ final Set<ClassMapping> classes = new HashSet<>(); ++ ++ final StringPool pool = new StringPool(); ++ for (final IMappingFile.IClass cls : mappings.getClasses()) { ++ final Map<String, String> methods = new HashMap<>(); ++ final Map<String, String> fields = new HashMap<>(); ++ final Map<String, String> strippedMethods = new HashMap<>(); ++ ++ for (final IMappingFile.IMethod methodMapping : cls.getMethods()) { ++ methods.put( ++ pool.string(methodKey( ++ Objects.requireNonNull(methodMapping.getMapped()), ++ Objects.requireNonNull(methodMapping.getMappedDescriptor()) ++ )), ++ pool.string(Objects.requireNonNull(methodMapping.getOriginal())) ++ ); ++ ++ strippedMethods.put( ++ pool.string(pool.string(strippedMethodKey( ++ methodMapping.getMapped(), ++ methodMapping.getDescriptor() ++ ))), ++ pool.string(methodMapping.getOriginal()) ++ ); ++ } ++ for (final IMappingFile.IField field : cls.getFields()) { ++ fields.put( ++ pool.string(field.getMapped()), ++ pool.string(field.getOriginal()) ++ ); ++ } ++ ++ final ClassMapping map = new ClassMapping( ++ Objects.requireNonNull(cls.getMapped()).replace('/', '.'), ++ Objects.requireNonNull(cls.getOriginal()).replace('/', '.'), ++ Map.copyOf(methods), ++ Map.copyOf(fields), ++ Map.copyOf(strippedMethods) ++ ); ++ classes.add(map); ++ } ++ ++ return Set.copyOf(classes); ++ } catch (final IOException ex) { ++ System.err.println("Failed to load mappings."); ++ ex.printStackTrace(); ++ return null; ++ } ++ } ++ ++ public static String strippedMethodKey(final String methodName, final String methodDescriptor) { ++ final String methodKey = methodKey(methodName, methodDescriptor); ++ final int returnDescriptorEnd = methodKey.indexOf(')'); ++ return methodKey.substring(0, returnDescriptorEnd + 1); ++ } ++ ++ public static String methodKey(final String methodName, final String methodDescriptor) { ++ return methodName + methodDescriptor; ++ } ++ ++ public record ClassMapping( ++ String obfName, ++ String mojangName, ++ Map<String, String> methodsByObf, ++ Map<String, String> fieldsByObf, ++ // obf name with mapped desc to mapped name. return value is excluded from desc as reflection doesn't use it ++ Map<String, String> strippedMethods ++ ) {} ++} +diff --git a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..242811578a786e3807a1a7019d472d5a68f87116 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java +@@ -0,0 +1,144 @@ ++package io.papermc.paper.util; ++ ++import io.papermc.paper.configuration.GlobalConfiguration; ++import it.unimi.dsi.fastutil.ints.Int2ObjectMap; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import java.io.IOException; ++import java.io.InputStream; ++import java.util.Collections; ++import java.util.LinkedHashMap; ++import java.util.Map; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.objectweb.asm.ClassReader; ++import org.objectweb.asm.ClassVisitor; ++import org.objectweb.asm.Label; ++import org.objectweb.asm.MethodVisitor; ++import org.objectweb.asm.Opcodes; ++ ++@DefaultQualifier(NonNull.class) ++public enum StacktraceDeobfuscator { ++ INSTANCE; ++ ++ private final Map<Class<?>, Int2ObjectMap<String>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) { ++ @Override ++ protected boolean removeEldestEntry(final Map.Entry<Class<?>, Int2ObjectMap<String>> eldest) { ++ return this.size() > 127; ++ } ++ }); ++ ++ public void deobfuscateThrowable(final Throwable throwable) { ++ if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true ++ return; ++ } ++ ++ throwable.setStackTrace(this.deobfuscateStacktrace(throwable.getStackTrace())); ++ final Throwable cause = throwable.getCause(); ++ if (cause != null) { ++ this.deobfuscateThrowable(cause); ++ } ++ for (final Throwable suppressed : throwable.getSuppressed()) { ++ this.deobfuscateThrowable(suppressed); ++ } ++ } ++ ++ public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) { ++ if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true ++ return traceElements; ++ } ++ ++ final @Nullable Map<String, ObfHelper.ClassMapping> mappings = ObfHelper.INSTANCE.mappingsByObfName(); ++ if (mappings == null || traceElements.length == 0) { ++ return traceElements; ++ } ++ final StackTraceElement[] result = new StackTraceElement[traceElements.length]; ++ for (int i = 0; i < traceElements.length; i++) { ++ final StackTraceElement element = traceElements[i]; ++ ++ final String className = element.getClassName(); ++ final String methodName = element.getMethodName(); ++ ++ final ObfHelper.ClassMapping classMapping = mappings.get(className); ++ if (classMapping == null) { ++ result[i] = element; ++ continue; ++ } ++ ++ final Class<?> clazz; ++ try { ++ clazz = Class.forName(className); ++ } catch (final ClassNotFoundException ex) { ++ throw new RuntimeException(ex); ++ } ++ final @Nullable String methodKey = this.determineMethodForLine(clazz, element.getLineNumber()); ++ final @Nullable String mappedMethodName = methodKey == null ? null : classMapping.methodsByObf().get(methodKey); ++ ++ result[i] = new StackTraceElement( ++ element.getClassLoaderName(), ++ element.getModuleName(), ++ element.getModuleVersion(), ++ classMapping.mojangName(), ++ mappedMethodName != null ? mappedMethodName : methodName, ++ sourceFileName(classMapping.mojangName()), ++ element.getLineNumber() ++ ); ++ } ++ return result; ++ } ++ ++ private @Nullable String determineMethodForLine(final Class<?> clazz, final int lineNumber) { ++ return this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap).get(lineNumber); ++ } ++ ++ private static String sourceFileName(final String fullClassName) { ++ final int dot = fullClassName.lastIndexOf('.'); ++ final String className = dot == -1 ++ ? fullClassName ++ : fullClassName.substring(dot + 1); ++ final String rootClassName = className.split("\\$")[0]; ++ return rootClassName + ".java"; ++ } ++ ++ private static Int2ObjectMap<String> buildLineMap(final Class<?> key) { ++ final StringPool pool = new StringPool(); ++ final Int2ObjectMap<String> lineMap = new Int2ObjectOpenHashMap<>(); ++ final class LineCollectingMethodVisitor extends MethodVisitor { ++ private final String name; ++ private final String descriptor; ++ ++ LineCollectingMethodVisitor(final String name, final String descriptor) { ++ super(Opcodes.ASM9); ++ this.name = name; ++ this.descriptor = descriptor; ++ } ++ ++ @Override ++ public void visitLineNumber(final int line, final Label start) { ++ lineMap.put(line, pool.string(ObfHelper.methodKey(this.name, this.descriptor))); ++ } ++ } ++ final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) { ++ @Override ++ public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { ++ return new LineCollectingMethodVisitor(name, descriptor); ++ } ++ }; ++ try { ++ final @Nullable InputStream inputStream = StacktraceDeobfuscator.class.getClassLoader() ++ .getResourceAsStream(key.getName().replace('.', '/') + ".class"); ++ if (inputStream == null) { ++ throw new IllegalStateException("Could not find class file: " + key.getName()); ++ } ++ final byte[] classData; ++ try (inputStream) { ++ classData = inputStream.readAllBytes(); ++ } ++ final ClassReader reader = new ClassReader(classData); ++ reader.accept(classVisitor, 0); ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ return lineMap; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/StringPool.java b/src/main/java/io/papermc/paper/util/StringPool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0a486cb46ff30353c3ff09567891cd36238eeb4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/StringPool.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.util; ++ ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Function; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++/** ++ * De-duplicates {@link String} instances without using {@link String#intern()}. ++ * ++ * <p>Interning may not be desired as we may want to use the heap for our pool, ++ * so it can be garbage collected as normal, etc.</p> ++ * ++ * <p>Additionally, interning can be slow due to the potentially large size of the ++ * pool (as it is shared for the entire JVM), and because most JVMs implement ++ * it using JNI.</p> ++ */ ++@DefaultQualifier(NonNull.class) ++public final class StringPool { ++ private final Map<String, String> pool; ++ ++ public StringPool() { ++ this(new HashMap<>()); ++ } ++ ++ public StringPool(final Map<String, String> map) { ++ this.pool = map; ++ } ++ ++ public String string(final String string) { ++ return this.pool.computeIfAbsent(string, Function.identity()); ++ } ++} +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index 1938ae691dafec1fc1e5a68792d1191bd52b4e5c..268310642181a715815d3b2d1c0f090e6252971a 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -34,6 +34,7 @@ public class CrashReport { + private final SystemReport systemReport = new SystemReport(); + + public CrashReport(String message, Throwable cause) { ++ 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 +diff --git a/src/main/java/net/minecraft/CrashReportCategory.java b/src/main/java/net/minecraft/CrashReportCategory.java +index f367ba058018074bfe6e4fe88bcc875ea9794d9e..2176171954609fd88f97f93408e14e018c1d6eaa 100644 +--- a/src/main/java/net/minecraft/CrashReportCategory.java ++++ b/src/main/java/net/minecraft/CrashReportCategory.java +@@ -110,6 +110,7 @@ public class CrashReportCategory { + } else { + this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount]; + System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length); ++ this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper + return this.stackTrace.length; + } + } +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 77985072928a1b892fb4f7dec1d0899324780082..f5e6610d271ef2c997fb3d1a5f65e0bf0740805a 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -82,13 +82,13 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> { + marker.add(Connection.PACKET_MARKER); + }); + public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(() -> { +- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).build()); ++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> { +- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build()); ++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(() -> { +- return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).build()); ++ return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; + private final PacketFlow receiving; +@@ -197,7 +197,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> { + + } + } +- if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) throwable.printStackTrace(); // Spigot ++ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper + } + + protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index b41eb920b5665b7a1b7cd9f38955c31eeb350847..bb59986c211f7d6ea50b1ad4bd5565227bec8a6c 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -210,6 +210,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings")); + org.spigotmc.SpigotConfig.registerCommands(); + // Spigot end ++ io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // Paper - load mappings for stacktrace deobf and etc. + // Paper start - initialize global and world-defaults configuration + this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess()); + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); +diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +index 987360a266a5a870f06929b00c9f451901188fd6..2cf3e79ec5e8706b71d27ebad4668773f0b91195 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java ++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +@@ -52,10 +52,10 @@ public class ServerConnectionListener { + + private static final Logger LOGGER = LogUtils.getLogger(); + public static final Supplier<NioEventLoopGroup> SERVER_EVENT_GROUP = Suppliers.memoize(() -> { +- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build()); ++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + public static final Supplier<EpollEventLoopGroup> SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(() -> { +- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build()); ++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + final MinecraftServer server; + public volatile boolean running; +diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java +index 8d06e8d286da2573e40794adab695ff77e5afd86..68551947f5b7d3471f15bd74ccd86519ab34c1c1 100644 +--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java ++++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java +@@ -356,7 +356,7 @@ public class OldUsersConverter { + try { + root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap()); + } catch (Exception exception) { +- exception.printStackTrace(); ++ io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper + } + + if (root != null) { +@@ -369,7 +369,7 @@ public class OldUsersConverter { + try { + NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2)); + } catch (Exception exception) { +- exception.printStackTrace(); ++ io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper + } + } + // CraftBukkit end +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index c4bf7053d83d207caca0e13e19f5c1afa7062de3..f697d45e0ac4e9cdc8a46121510a04c0f294d91f 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -130,7 +130,7 @@ public class WatchdogThread extends Thread + } + log.log( Level.SEVERE, "\tStack:" ); + // +- for ( StackTraceElement stack : thread.getStackTrace() ) ++ for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace()) ) // Paper + { + log.log( Level.SEVERE, "\t\t" + stack ); + } +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 18e961a37b2830da6e5dab7aa35116b2f5215898..128fa1376f22d3429a23d79a2772abf2e7fec2bc 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -30,10 +30,14 @@ + <DefaultRolloverStrategy max="1000"/> + </RollingRandomAccessFile> + <Async name="Async"> ++ <AppenderRef ref="rewrite"/> ++ </Async> ++ <Rewrite name="rewrite"> ++ <StacktraceDeobfuscatingRewritePolicy /> + <AppenderRef ref="File"/> + <AppenderRef ref="TerminalConsole" level="info"/> + <AppenderRef ref="ServerGuiConsole" level="info"/> +- </Async> ++ </Rewrite> + </Appenders> + <Loggers> + <Root level="info"> diff --git a/patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch b/patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch new file mode 100644 index 0000000000..0fdcde78a6 --- /dev/null +++ b/patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch @@ -0,0 +1,246 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: SirYwell <[email protected]> +Date: Sat, 10 Jul 2021 11:12:30 +0200 +Subject: [PATCH] Rewrite LogEvents to contain the source jars in stack traces + + +diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/DelegateLogEvent.java b/src/log4jPlugins/java/io/papermc/paper/logging/DelegateLogEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6ffd1befe64c6c3036c22e05ed1c44808d64bd28 +--- /dev/null ++++ b/src/log4jPlugins/java/io/papermc/paper/logging/DelegateLogEvent.java +@@ -0,0 +1,130 @@ ++package io.papermc.paper.logging; ++ ++import org.apache.logging.log4j.Level; ++import org.apache.logging.log4j.Marker; ++import org.apache.logging.log4j.ThreadContext; ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.impl.ThrowableProxy; ++import org.apache.logging.log4j.core.time.Instant; ++import org.apache.logging.log4j.message.Message; ++import org.apache.logging.log4j.util.ReadOnlyStringMap; ++ ++import java.util.Map; ++ ++public class DelegateLogEvent implements LogEvent { ++ private final LogEvent original; ++ ++ protected DelegateLogEvent(LogEvent original) { ++ this.original = original; ++ } ++ ++ @Override ++ public LogEvent toImmutable() { ++ return this.original.toImmutable(); ++ } ++ ++ @Override ++ public Map<String, String> getContextMap() { ++ return this.original.getContextMap(); ++ } ++ ++ @Override ++ public ReadOnlyStringMap getContextData() { ++ return this.original.getContextData(); ++ } ++ ++ @Override ++ public ThreadContext.ContextStack getContextStack() { ++ return this.original.getContextStack(); ++ } ++ ++ @Override ++ public String getLoggerFqcn() { ++ return this.original.getLoggerFqcn(); ++ } ++ ++ @Override ++ public Level getLevel() { ++ return this.original.getLevel(); ++ } ++ ++ @Override ++ public String getLoggerName() { ++ return this.original.getLoggerName(); ++ } ++ ++ @Override ++ public Marker getMarker() { ++ return this.original.getMarker(); ++ } ++ ++ @Override ++ public Message getMessage() { ++ return this.original.getMessage(); ++ } ++ ++ @Override ++ public long getTimeMillis() { ++ return this.original.getTimeMillis(); ++ } ++ ++ @Override ++ public Instant getInstant() { ++ return this.original.getInstant(); ++ } ++ ++ @Override ++ public StackTraceElement getSource() { ++ return this.original.getSource(); ++ } ++ ++ @Override ++ public String getThreadName() { ++ return this.original.getThreadName(); ++ } ++ ++ @Override ++ public long getThreadId() { ++ return this.original.getThreadId(); ++ } ++ ++ @Override ++ public int getThreadPriority() { ++ return this.original.getThreadPriority(); ++ } ++ ++ @Override ++ public Throwable getThrown() { ++ return this.original.getThrown(); ++ } ++ ++ @Override ++ public ThrowableProxy getThrownProxy() { ++ return this.original.getThrownProxy(); ++ } ++ ++ @Override ++ public boolean isEndOfBatch() { ++ return this.original.isEndOfBatch(); ++ } ++ ++ @Override ++ public boolean isIncludeLocation() { ++ return this.original.isIncludeLocation(); ++ } ++ ++ @Override ++ public void setEndOfBatch(boolean endOfBatch) { ++ this.original.setEndOfBatch(endOfBatch); ++ } ++ ++ @Override ++ public void setIncludeLocation(boolean locationRequired) { ++ this.original.setIncludeLocation(locationRequired); ++ } ++ ++ @Override ++ public long getNanoTime() { ++ return this.original.getNanoTime(); ++ } ++} +diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoLogEvent.java b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoLogEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..558427c65b4051923f73d15d85ee519be005060a +--- /dev/null ++++ b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoLogEvent.java +@@ -0,0 +1,48 @@ ++package io.papermc.paper.logging; ++ ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.impl.ExtendedClassInfo; ++import org.apache.logging.log4j.core.impl.ExtendedStackTraceElement; ++import org.apache.logging.log4j.core.impl.ThrowableProxy; ++ ++public class ExtraClassInfoLogEvent extends DelegateLogEvent { ++ ++ private boolean fixed; ++ ++ public ExtraClassInfoLogEvent(LogEvent original) { ++ super(original); ++ } ++ ++ @Override ++ public ThrowableProxy getThrownProxy() { ++ if (fixed) { ++ return super.getThrownProxy(); ++ } ++ rewriteStackTrace(super.getThrownProxy()); ++ fixed = true; ++ return super.getThrownProxy(); ++ } ++ ++ private void rewriteStackTrace(ThrowableProxy throwable) { ++ ExtendedStackTraceElement[] stackTrace = throwable.getExtendedStackTrace(); ++ for (int i = 0; i < stackTrace.length; i++) { ++ ExtendedClassInfo classInfo = stackTrace[i].getExtraClassInfo(); ++ if (classInfo.getLocation().equals("?")) { ++ StackTraceElement element = stackTrace[i].getStackTraceElement(); ++ String classLoaderName = element.getClassLoaderName(); ++ if (classLoaderName != null) { ++ stackTrace[i] = new ExtendedStackTraceElement(element, ++ new ExtendedClassInfo(classInfo.getExact(), classLoaderName, "?")); ++ } ++ } ++ } ++ if (throwable.getCauseProxy() != null) { ++ rewriteStackTrace(throwable.getCauseProxy()); ++ } ++ if (throwable.getSuppressedProxies() != null) { ++ for (ThrowableProxy proxy : throwable.getSuppressedProxies()) { ++ rewriteStackTrace(proxy); ++ } ++ } ++ } ++} +diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoRewritePolicy.java b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoRewritePolicy.java +new file mode 100644 +index 0000000000000000000000000000000000000000..34734bb969a1a74c7a4f9c17d40ebf007ad5d701 +--- /dev/null ++++ b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoRewritePolicy.java +@@ -0,0 +1,29 @@ ++package io.papermc.paper.logging; ++ ++import org.apache.logging.log4j.core.Core; ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy; ++import org.apache.logging.log4j.core.config.plugins.Plugin; ++import org.apache.logging.log4j.core.config.plugins.PluginFactory; ++import org.jetbrains.annotations.NotNull; ++ ++@Plugin( ++ name = "ExtraClassInfoRewritePolicy", ++ category = Core.CATEGORY_NAME, ++ elementType = "rewritePolicy", ++ printObject = true ++) ++public final class ExtraClassInfoRewritePolicy implements RewritePolicy { ++ @Override ++ public LogEvent rewrite(LogEvent source) { ++ if (source.getThrown() != null) { ++ return new ExtraClassInfoLogEvent(source); ++ } ++ return source; ++ } ++ ++ @PluginFactory ++ public static @NotNull ExtraClassInfoRewritePolicy createPolicy() { ++ return new ExtraClassInfoRewritePolicy(); ++ } ++} +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 128fa1376f22d3429a23d79a2772abf2e7fec2bc..637d64da9938e51a97338b9253b43889585c67bb 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -34,6 +34,10 @@ + </Async> + <Rewrite name="rewrite"> + <StacktraceDeobfuscatingRewritePolicy /> ++ <AppenderRef ref="rewrite2"/> ++ </Rewrite> ++ <Rewrite name="rewrite2"> ++ <ExtraClassInfoRewritePolicy /> + <AppenderRef ref="File"/> + <AppenderRef ref="TerminalConsole" level="info"/> + <AppenderRef ref="ServerGuiConsole" level="info"/> diff --git a/patches/server/0017-Paper-command.patch b/patches/server/0017-Paper-command.patch new file mode 100644 index 0000000000..54d41bd762 --- /dev/null +++ b/patches/server/0017-Paper-command.patch @@ -0,0 +1,665 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <[email protected]> +Date: Mon, 29 Feb 2016 21:02:09 -0600 +Subject: [PATCH] Paper command + +Co-authored-by: Zach Brown <[email protected]> + +diff --git a/src/main/java/io/papermc/paper/command/CallbackCommand.java b/src/main/java/io/papermc/paper/command/CallbackCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fa4202abd13c1c286bd398938103d1103d5443e7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/CallbackCommand.java +@@ -0,0 +1,35 @@ ++package io.papermc.paper.command; ++ ++import io.papermc.paper.adventure.providers.ClickCallbackProviderImpl; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import java.util.UUID; ++ ++@DefaultQualifier(NonNull.class) ++public class CallbackCommand extends Command { ++ ++ protected CallbackCommand(final String name) { ++ super(name); ++ this.description = "ClickEvent callback"; ++ this.usageMessage = "/callback <uuid>"; ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ if (args.length != 1) { ++ return false; ++ } ++ ++ final UUID id; ++ try { ++ id = UUID.fromString(args[0]); ++ } catch (final IllegalArgumentException ignored) { ++ return false; ++ } ++ ++ ClickCallbackProviderImpl.CALLBACK_MANAGER.runCallback(sender, id); ++ return true; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/CommandUtil.java b/src/main/java/io/papermc/paper/command/CommandUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..953c30500892e5f0c55b8597bc708ea85bf56d6e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/CommandUtil.java +@@ -0,0 +1,69 @@ ++package io.papermc.paper.command; ++ ++import com.google.common.base.Functions; ++import com.google.common.collect.Iterables; ++import com.google.common.collect.Lists; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.Iterator; ++import java.util.List; ++import net.minecraft.resources.ResourceLocation; ++import org.bukkit.command.CommandSender; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class CommandUtil { ++ private CommandUtil() { ++ } ++ ++ // Code from Mojang - copyright them ++ public static List<String> getListMatchingLast( ++ final CommandSender sender, ++ final String[] args, ++ final String... matches ++ ) { ++ return getListMatchingLast(sender, args, Arrays.asList(matches)); ++ } ++ ++ public static boolean matches(final String s, final String s1) { ++ return s1.regionMatches(true, 0, s, 0, s.length()); ++ } ++ ++ public static List<String> getListMatchingLast( ++ final CommandSender sender, ++ final String[] strings, ++ final Collection<?> collection ++ ) { ++ String last = strings[strings.length - 1]; ++ ArrayList<String> results = Lists.newArrayList(); ++ ++ if (!collection.isEmpty()) { ++ Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator(); ++ ++ while (iterator.hasNext()) { ++ String s1 = (String) iterator.next(); ++ ++ if (matches(last, s1) && (sender.hasPermission(PaperCommand.BASE_PERM + s1) || sender.hasPermission("bukkit.command.paper"))) { ++ results.add(s1); ++ } ++ } ++ ++ if (results.isEmpty()) { ++ iterator = collection.iterator(); ++ ++ while (iterator.hasNext()) { ++ Object object = iterator.next(); ++ ++ if (object instanceof ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) { ++ results.add(String.valueOf(object)); ++ } ++ } ++ } ++ } ++ ++ return results; ++ } ++ // end copy stuff ++} +diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..95d3320c865d24609252063be07cddfd07d0d6d8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java +@@ -0,0 +1,143 @@ ++package io.papermc.paper.command; ++ ++import io.papermc.paper.command.subcommands.*; ++import it.unimi.dsi.fastutil.Pair; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.Set; ++import java.util.stream.Collectors; ++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 static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++@DefaultQualifier(NonNull.class) ++public final class PaperCommand extends Command { ++ static final String BASE_PERM = "bukkit.command.paper."; ++ // subcommand label -> subcommand ++ private static final Map<String, PaperSubcommand> SUBCOMMANDS = Util.make(() -> { ++ final Map<Set<String>, PaperSubcommand> commands = new HashMap<>(); ++ ++ commands.put(Set.of("heap"), new HeapDumpCommand()); ++ commands.put(Set.of("entity"), new EntityCommand()); ++ commands.put(Set.of("reload"), new ReloadCommand()); ++ commands.put(Set.of("version"), new VersionCommand()); ++ ++ return commands.entrySet().stream() ++ .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) ++ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); ++ }); ++ private static final Set<String> COMPLETABLE_SUBCOMMANDS = SUBCOMMANDS.entrySet().stream().filter(entry -> entry.getValue().tabCompletes()).map(Map.Entry::getKey).collect(Collectors.toSet()); ++ // alias -> subcommand label ++ private static final Map<String, String> ALIASES = Util.make(() -> { ++ final Map<String, Set<String>> aliases = new HashMap<>(); ++ ++ 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 PaperCommand(final String name) { ++ super(name); ++ this.description = "Paper related commands"; ++ this.usageMessage = "/paper [" + String.join(" | ", SUBCOMMANDS.keySet()) + "]"; ++ final List<String> permissions = new ArrayList<>(); ++ permissions.add("bukkit.command.paper"); ++ permissions.addAll(SUBCOMMANDS.keySet().stream().map(s -> BASE_PERM + s).toList()); ++ this.setPermission(String.join(";", permissions)); ++ final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); ++ for (final String perm : permissions) { ++ pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); ++ } ++ } ++ ++ private static boolean testPermission(final CommandSender sender, final String permission) { ++ if (sender.hasPermission(BASE_PERM + permission) || sender.hasPermission("bukkit.command.paper")) { ++ return true; ++ } ++ sender.sendMessage(text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", RED)); ++ return false; ++ } ++ ++ @Override ++ public List<String> tabComplete( ++ final CommandSender sender, ++ final String alias, ++ final String[] args, ++ final @Nullable Location location ++ ) throws IllegalArgumentException { ++ if (args.length <= 1) { ++ return CommandUtil.getListMatchingLast(sender, args, COMPLETABLE_SUBCOMMANDS); ++ } ++ ++ final @Nullable Pair<String, PaperSubcommand> subCommand = resolveCommand(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, RED)); ++ return false; ++ } ++ final @Nullable Pair<String, PaperSubcommand> subCommand = resolveCommand(args[0]); ++ ++ if (subCommand == null) { ++ sender.sendMessage(text("Usage: " + this.usageMessage, 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<String, PaperSubcommand> resolveCommand(String label) { ++ label = label.toLowerCase(Locale.ROOT); ++ @Nullable PaperSubcommand subCommand = SUBCOMMANDS.get(label); ++ if (subCommand == null) { ++ final @Nullable String command = ALIASES.get(label); ++ if (command != null) { ++ label = command; ++ subCommand = SUBCOMMANDS.get(command); ++ } ++ } ++ ++ if (subCommand != null) { ++ return Pair.of(label, subCommand); ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5e5ec700a368cfdaa1ea0b3f0fa82089895d4b92 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java +@@ -0,0 +1,28 @@ ++package io.papermc.paper.command; ++ ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.Command; ++ ++import java.util.HashMap; ++import java.util.Map; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class PaperCommands { ++ ++ private PaperCommands() { ++ } ++ ++ private static final Map<String, Command> COMMANDS = new HashMap<>(); ++ static { ++ COMMANDS.put("paper", new PaperCommand("paper")); ++ COMMANDS.put("callback", new CallbackCommand("callback")); ++ } ++ ++ public static void registerCommands(final MinecraftServer server) { ++ COMMANDS.forEach((s, command) -> { ++ server.server.getCommandMap().register(s, "Paper", command); ++ }); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/PaperSubcommand.java b/src/main/java/io/papermc/paper/command/PaperSubcommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7e9e0ff8639be135bf8575e375cbada5b57164e1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/PaperSubcommand.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.command; ++ ++import java.util.Collections; ++import java.util.List; ++import org.bukkit.command.CommandSender; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public interface PaperSubcommand { ++ boolean execute(CommandSender sender, String subCommand, String[] args); ++ ++ default List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) { ++ return Collections.emptyList(); ++ } ++ ++ default boolean tabCompletes() { ++ return true; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..67fcba634f8183bb33834ac3b2c3dcfb8d87129e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java +@@ -0,0 +1,158 @@ ++package io.papermc.paper.command.subcommands; ++ ++import com.google.common.collect.Maps; ++import io.papermc.paper.command.CommandUtil; ++import io.papermc.paper.command.PaperSubcommand; ++import java.util.Collections; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.Set; ++import java.util.stream.Collectors; ++ ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.level.ChunkPos; ++import org.apache.commons.lang3.tuple.MutablePair; ++import org.apache.commons.lang3.tuple.Pair; ++import org.bukkit.Bukkit; ++import org.bukkit.HeightMap; ++import org.bukkit.World; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.entity.Player; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++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; ++ ++@DefaultQualifier(NonNull.class) ++public final class EntityCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.listEntities(sender, args); ++ return true; ++ } ++ ++ @Override ++ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) { ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, "help", "list"); ++ } else if (args.length == 2) { ++ return CommandUtil.getListMatchingLast(sender, args, BuiltInRegistries.ENTITY_TYPE.keySet().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new)); ++ } ++ return Collections.emptyList(); ++ } ++ ++ /* ++ * Ported from MinecraftForge - author: LexManos <[email protected]> - License: LGPLv2.1 ++ */ ++ private void listEntities(final CommandSender sender, final String[] args) { ++ // help ++ if (args.length < 1 || !args[0].toLowerCase(Locale.ROOT).equals("list")) { ++ sender.sendMessage(text("Use /paper entity [list] help for more information on a specific command", RED)); ++ return; ++ } ++ ++ if ("list".equals(args[0].toLowerCase(Locale.ROOT))) { ++ String filter = "*"; ++ if (args.length > 1) { ++ if (args[1].toLowerCase(Locale.ROOT).equals("help")) { ++ sender.sendMessage(text("Use /paper entity list [filter] [worldName] to get entity info that matches the optional filter.", RED)); ++ return; ++ } ++ filter = args[1]; ++ } ++ final String cleanfilter = filter.replace("?", ".?").replace("*", ".*?"); ++ Set<ResourceLocation> names = BuiltInRegistries.ENTITY_TYPE.keySet().stream() ++ .filter(n -> n.toString().matches(cleanfilter)) ++ .collect(Collectors.toSet()); ++ if (names.isEmpty()) { ++ sender.sendMessage(text("Invalid filter, does not match any entities. Use /paper entity list for a proper list", RED)); ++ sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); ++ return; ++ } ++ String worldName; ++ if (args.length > 2) { ++ worldName = args[2]; ++ } else if (sender instanceof Player) { ++ worldName = ((Player) sender).getWorld().getName(); ++ } else { ++ sender.sendMessage(text("Please specify the name of a world", RED)); ++ sender.sendMessage(text("To do so without a filter, specify '*' as the filter", RED)); ++ sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); ++ return; ++ } ++ Map<ResourceLocation, MutablePair<Integer, Map<ChunkPos, Integer>>> list = Maps.newHashMap(); ++ @Nullable World bukkitWorld = Bukkit.getWorld(worldName); ++ if (bukkitWorld == null) { ++ sender.sendMessage(text("Could not load world for " + worldName + ". Please select a valid world.", RED)); ++ sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); ++ return; ++ } ++ ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); ++ Map<ResourceLocation, Integer> nonEntityTicking = Maps.newHashMap(); ++ ServerChunkCache chunkProviderServer = world.getChunkSource(); ++ world.getAllEntities().forEach(e -> { ++ ResourceLocation key = EntityType.getKey(e.getType()); ++ ++ MutablePair<Integer, Map<ChunkPos, Integer>> info = list.computeIfAbsent(key, k -> MutablePair.of(0, Maps.newHashMap())); ++ ChunkPos chunk = e.chunkPosition(); ++ info.left++; ++ info.right.put(chunk, info.right.getOrDefault(chunk, 0) + 1); ++ if (!chunkProviderServer.isPositionTicking(e)) { ++ nonEntityTicking.merge(key, 1, Integer::sum); ++ } ++ }); ++ if (names.size() == 1) { ++ ResourceLocation name = names.iterator().next(); ++ Pair<Integer, Map<ChunkPos, Integer>> info = list.get(name); ++ int nonTicking = nonEntityTicking.getOrDefault(name, 0); ++ if (info == null) { ++ sender.sendMessage(text("No entities found.", RED)); ++ return; ++ } ++ sender.sendMessage("Entity: " + name + " Total Ticking: " + (info.getLeft() - nonTicking) + ", Total Non-Ticking: " + nonTicking); ++ info.getRight().entrySet().stream() ++ .sorted((a, b) -> !a.getValue().equals(b.getValue()) ? b.getValue() - a.getValue() : a.getKey().toString().compareTo(b.getKey().toString())) ++ .limit(10).forEach(e -> { ++ final int x = (e.getKey().x << 4) + 8; ++ final int z = (e.getKey().z << 4) + 8; ++ final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)")) ++ .hoverEvent(HoverEvent.showText(text("Click to teleport to chunk", GREEN))) ++ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (world.getWorld().getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1) + " " + z)); ++ sender.sendMessage(message); ++ }); ++ } else { ++ List<Pair<ResourceLocation, Integer>> info = list.entrySet().stream() ++ .filter(e -> names.contains(e.getKey())) ++ .map(e -> Pair.of(e.getKey(), e.getValue().left)) ++ .sorted((a, b) -> !a.getRight().equals(b.getRight()) ? b.getRight() - a.getRight() : a.getKey().toString().compareTo(b.getKey().toString())) ++ .toList(); ++ ++ if (info.isEmpty()) { ++ sender.sendMessage(text("No entities found.", RED)); ++ return; ++ } ++ ++ int count = info.stream().mapToInt(Pair::getRight).sum(); ++ int nonTickingCount = nonEntityTicking.values().stream().mapToInt(Integer::intValue).sum(); ++ sender.sendMessage("Total Ticking: " + (count - nonTickingCount) + ", Total Non-Ticking: " + nonTickingCount); ++ info.forEach(e -> { ++ int nonTicking = nonEntityTicking.getOrDefault(e.getKey(), 0); ++ sender.sendMessage(" " + (e.getValue() - nonTicking) + " (" + nonTicking + ") " + ": " + e.getKey()); ++ }); ++ sender.sendMessage("* First number is ticking entities, second number is non-ticking entities"); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cd2e4d792e972b8bf1e07b8961594a670ae949cf +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +@@ -0,0 +1,38 @@ ++package io.papermc.paper.command.subcommands; ++ ++import io.papermc.paper.command.PaperSubcommand; ++import java.time.LocalDateTime; ++import java.time.format.DateTimeFormatter; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftServer; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++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 static net.kyori.adventure.text.format.NamedTextColor.YELLOW; ++ ++@DefaultQualifier(NonNull.class) ++public final class HeapDumpCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.dumpHeap(sender); ++ return true; ++ } ++ ++ private void dumpHeap(final CommandSender sender) { ++ java.nio.file.Path dir = java.nio.file.Paths.get("./dumps"); ++ String name = "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()); ++ ++ Command.broadcastCommandMessage(sender, text("Writing JVM heap data...", YELLOW)); ++ ++ java.nio.file.Path file = CraftServer.dumpHeap(dir, name); ++ if (file != null) { ++ Command.broadcastCommandMessage(sender, text("Heap dump saved to " + file, GREEN)); ++ } else { ++ Command.broadcastCommandMessage(sender, text("Failed to write heap dump, see server log for details", RED)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bd68139ae635f2ad7ec8e7a21e0056a139c4c62e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +@@ -0,0 +1,33 @@ ++package io.papermc.paper.command.subcommands; ++ ++import io.papermc.paper.command.PaperSubcommand; ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftServer; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++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; ++ ++@DefaultQualifier(NonNull.class) ++public final class ReloadCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.doReload(sender); ++ return true; ++ } ++ ++ private void doReload(final CommandSender sender) { ++ Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED)); ++ Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED)); ++ ++ MinecraftServer server = ((CraftServer) sender.getServer()).getServer(); ++ server.paperConfigurations.reloadConfigs(server); ++ server.server.reloadCount++; ++ ++ Command.broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN)); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ae60bd96b5284d54676d8e7e4dd5d170b526ec1e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java +@@ -0,0 +1,21 @@ ++package io.papermc.paper.command.subcommands; ++ ++import io.papermc.paper.command.PaperSubcommand; ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class VersionCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ final @Nullable Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); ++ if (ver != null) { ++ ver.execute(sender, "paper", new String[0]); ++ } ++ return true; ++ } ++} +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index bb59986c211f7d6ea50b1ad4bd5565227bec8a6c..9c950fc1de15b5039e34a9fdf893e97a8cc13237 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -215,6 +215,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess()); + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); + // Paper end - initialize global and world-defaults configuration ++ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command + + this.setPvpAllowed(dedicatedserverproperties.pvp); + this.setFlightAllowed(dedicatedserverproperties.allowFlight); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 7eb94216cc556ad4c6c76ffab0ca81861d2c1883..e7301f38be4fae26404fd8dd33798ef0764565ac 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -978,6 +978,7 @@ public final class CraftServer implements Server { + this.commandMap.clearCommands(); + this.reloadData(); + org.spigotmc.SpigotConfig.registerCommands(); // Spigot ++ io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); + +@@ -2703,6 +2704,34 @@ public final class CraftServer implements Server { + // Paper end + + // Paper start ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ public static java.nio.file.Path dumpHeap(java.nio.file.Path dir, String name) { ++ try { ++ java.nio.file.Files.createDirectories(dir); ++ ++ javax.management.MBeanServer server = java.lang.management.ManagementFactory.getPlatformMBeanServer(); ++ java.nio.file.Path file; ++ ++ try { ++ Class clazz = Class.forName("openj9.lang.management.OpenJ9DiagnosticsMXBean"); ++ Object openj9Mbean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "openj9.lang.management:type=OpenJ9Diagnostics", clazz); ++ java.lang.reflect.Method m = clazz.getMethod("triggerDumpToFile", String.class, String.class); ++ file = dir.resolve(name + ".phd"); ++ m.invoke(openj9Mbean, "heap", file.toString()); ++ } catch (ClassNotFoundException e) { ++ Class clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean"); ++ Object hotspotMBean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "com.sun.management:type=HotSpotDiagnostic", clazz); ++ java.lang.reflect.Method m = clazz.getMethod("dumpHeap", String.class, boolean.class); ++ file = dir.resolve(name + ".hprof"); ++ m.invoke(hotspotMBean, file.toString(), true); ++ } ++ ++ return file; ++ } catch (Throwable t) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not write heap", t); ++ return null; ++ } ++ } + private Iterable<? extends net.kyori.adventure.audience.Audience> adventure$audiences; + @Override + public Iterable<? extends net.kyori.adventure.audience.Audience> audiences() { diff --git a/patches/server/0018-Paper-Metrics.patch b/patches/server/0018-Paper-Metrics.patch new file mode 100644 index 0000000000..46c111b03f --- /dev/null +++ b/patches/server/0018-Paper-Metrics.patch @@ -0,0 +1,731 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Fri, 24 Mar 2017 23:56:01 -0500 +Subject: [PATCH] Paper Metrics + +Removes Spigot's mcstats metrics in favor of a system using bStats + +To disable for privacy or other reasons go to the bStats folder in your plugins folder +and edit the config.yml file present there. + +Please keep in mind the data collected is anonymous and collection should have no +tangible effect on server performance. The data is used to allow the authors of +PaperMC to track version and platform usage so that we can make better management +decisions on behalf of the project. + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6aaed8e8bf8c721fc834da5c76ac72a4c3e92458 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -0,0 +1,678 @@ ++package com.destroystokyo.paper; ++ ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.Bukkit; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.bukkit.craftbukkit.util.CraftMagicNumbers; ++import org.bukkit.plugin.Plugin; ++ ++import org.json.simple.JSONArray; ++import org.json.simple.JSONObject; ++ ++import javax.net.ssl.HttpsURLConnection; ++import java.io.ByteArrayOutputStream; ++import java.io.DataOutputStream; ++import java.io.File; ++import java.io.IOException; ++import java.net.URL; ++import java.util.*; ++import java.util.concurrent.Callable; ++import java.util.concurrent.Executors; ++import java.util.concurrent.ScheduledExecutorService; ++import java.util.concurrent.TimeUnit; ++import java.util.logging.Level; ++import java.util.logging.Logger; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++import java.util.zip.GZIPOutputStream; ++ ++/** ++ * bStats collects some data for plugin authors. ++ * ++ * Check out https://bStats.org/ to learn more about bStats! ++ */ ++public class Metrics { ++ ++ // Executor service for requests ++ // We use an executor service because the Bukkit scheduler is affected by server lags ++ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); ++ ++ // The version of this bStats class ++ public static final int B_STATS_VERSION = 1; ++ ++ // The url to which the data is sent ++ private static final String URL = "https://bStats.org/submitData/server-implementation"; ++ ++ // Should failed requests be logged? ++ private static boolean logFailedRequests = false; ++ ++ // The logger for the failed requests ++ private static Logger logger = Logger.getLogger("bStats"); ++ ++ // The name of the server software ++ private final String name; ++ ++ // The uuid of the server ++ private final String serverUUID; ++ ++ // A list with all custom charts ++ private final List<CustomChart> charts = new ArrayList<>(); ++ ++ /** ++ * Class constructor. ++ * ++ * @param name The name of the server software. ++ * @param serverUUID The uuid of the server. ++ * @param logFailedRequests Whether failed requests should be logged or not. ++ * @param logger The logger for the failed requests. ++ */ ++ public Metrics(String name, String serverUUID, boolean logFailedRequests, Logger logger) { ++ this.name = name; ++ this.serverUUID = serverUUID; ++ Metrics.logFailedRequests = logFailedRequests; ++ Metrics.logger = logger; ++ ++ // Start submitting the data ++ startSubmitting(); ++ } ++ ++ /** ++ * Adds a custom chart. ++ * ++ * @param chart The chart to add. ++ */ ++ public void addCustomChart(CustomChart chart) { ++ if (chart == null) { ++ throw new IllegalArgumentException("Chart cannot be null!"); ++ } ++ charts.add(chart); ++ } ++ ++ /** ++ * Starts the Scheduler which submits our data every 30 minutes. ++ */ ++ private void startSubmitting() { ++ final Runnable submitTask = this::submitData; ++ ++ // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the ++ // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. ++ // WARNING: You must not modify any part of this Metrics class, including the submit delay or frequency! ++ // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! ++ long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); ++ long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); ++ scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); ++ scheduler.scheduleAtFixedRate(submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); ++ } ++ ++ /** ++ * Gets the plugin specific data. ++ * ++ * @return The plugin specific data. ++ */ ++ private JSONObject getPluginData() { ++ JSONObject data = new JSONObject(); ++ ++ data.put("pluginName", name); // Append the name of the server software ++ JSONArray customCharts = new JSONArray(); ++ for (CustomChart customChart : charts) { ++ // Add the data of the custom charts ++ JSONObject chart = customChart.getRequestJsonObject(); ++ if (chart == null) { // If the chart is null, we skip it ++ continue; ++ } ++ customCharts.add(chart); ++ } ++ data.put("customCharts", customCharts); ++ ++ return data; ++ } ++ ++ /** ++ * Gets the server specific data. ++ * ++ * @return The server specific data. ++ */ ++ private JSONObject getServerData() { ++ // OS specific data ++ String osName = System.getProperty("os.name"); ++ String osArch = System.getProperty("os.arch"); ++ String osVersion = System.getProperty("os.version"); ++ int coreCount = Runtime.getRuntime().availableProcessors(); ++ ++ JSONObject data = new JSONObject(); ++ ++ data.put("serverUUID", serverUUID); ++ ++ data.put("osName", osName); ++ data.put("osArch", osArch); ++ data.put("osVersion", osVersion); ++ data.put("coreCount", coreCount); ++ ++ return data; ++ } ++ ++ /** ++ * Collects the data and sends it afterwards. ++ */ ++ private void submitData() { ++ final JSONObject data = getServerData(); ++ ++ JSONArray pluginData = new JSONArray(); ++ pluginData.add(getPluginData()); ++ data.put("plugins", pluginData); ++ ++ try { ++ // We are still in the Thread of the timer, so nothing get blocked :) ++ sendData(data); ++ } catch (Exception e) { ++ // Something went wrong! :( ++ if (logFailedRequests) { ++ logger.log(Level.WARNING, "Could not submit stats of " + name, e); ++ } ++ } ++ } ++ ++ /** ++ * Sends the data to the bStats server. ++ * ++ * @param data The data to send. ++ * @throws Exception If the request failed. ++ */ ++ private static void sendData(JSONObject data) throws Exception { ++ if (data == null) { ++ throw new IllegalArgumentException("Data cannot be null!"); ++ } ++ HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); ++ ++ // Compress the data to save bandwidth ++ byte[] compressedData = compress(data.toString()); ++ ++ // Add headers ++ connection.setRequestMethod("POST"); ++ connection.addRequestProperty("Accept", "application/json"); ++ connection.addRequestProperty("Connection", "close"); ++ connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request ++ connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); ++ connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format ++ connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); ++ ++ // Send data ++ connection.setDoOutput(true); ++ DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); ++ outputStream.write(compressedData); ++ outputStream.flush(); ++ outputStream.close(); ++ ++ connection.getInputStream().close(); // We don't care about the response - Just send our data :) ++ } ++ ++ /** ++ * Gzips the given String. ++ * ++ * @param str The string to gzip. ++ * @return The gzipped String. ++ * @throws IOException If the compression failed. ++ */ ++ private static byte[] compress(final String str) throws IOException { ++ if (str == null) { ++ return null; ++ } ++ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ++ GZIPOutputStream gzip = new GZIPOutputStream(outputStream); ++ gzip.write(str.getBytes("UTF-8")); ++ gzip.close(); ++ return outputStream.toByteArray(); ++ } ++ ++ /** ++ * Represents a custom chart. ++ */ ++ public static abstract class CustomChart { ++ ++ // The id of the chart ++ final String chartId; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ */ ++ CustomChart(String chartId) { ++ if (chartId == null || chartId.isEmpty()) { ++ throw new IllegalArgumentException("ChartId cannot be null or empty!"); ++ } ++ this.chartId = chartId; ++ } ++ ++ private JSONObject getRequestJsonObject() { ++ JSONObject chart = new JSONObject(); ++ chart.put("chartId", chartId); ++ try { ++ JSONObject data = getChartData(); ++ if (data == null) { ++ // If the data is null we don't send the chart. ++ return null; ++ } ++ chart.put("data", data); ++ } catch (Throwable t) { ++ if (logFailedRequests) { ++ logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); ++ } ++ return null; ++ } ++ return chart; ++ } ++ ++ protected abstract JSONObject getChartData() throws Exception; ++ ++ } ++ ++ /** ++ * Represents a custom simple pie. ++ */ ++ public static class SimplePie extends CustomChart { ++ ++ private final Callable<String> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public SimplePie(String chartId, Callable<String> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ String value = callable.call(); ++ if (value == null || value.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("value", value); ++ return data; ++ } ++ } ++ ++ /** ++ * Represents a custom advanced pie. ++ */ ++ public static class AdvancedPie extends CustomChart { ++ ++ private final Callable<Map<String, Integer>> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Integer> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean allSkipped = true; ++ for (Map.Entry<String, Integer> entry : map.entrySet()) { ++ if (entry.getValue() == 0) { ++ continue; // Skip this invalid ++ } ++ allSkipped = false; ++ values.put(entry.getKey(), entry.getValue()); ++ } ++ if (allSkipped) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("values", values); ++ return data; ++ } ++ } ++ ++ /** ++ * Represents a custom drilldown pie. ++ */ ++ public static class DrilldownPie extends CustomChart { ++ ++ private final Callable<Map<String, Map<String, Integer>>> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ public JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Map<String, Integer>> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean reallyAllSkipped = true; ++ for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) { ++ JSONObject value = new JSONObject(); ++ boolean allSkipped = true; ++ for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) { ++ value.put(valueEntry.getKey(), valueEntry.getValue()); ++ allSkipped = false; ++ } ++ if (!allSkipped) { ++ reallyAllSkipped = false; ++ values.put(entryValues.getKey(), value); ++ } ++ } ++ if (reallyAllSkipped) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("values", values); ++ return data; ++ } ++ } ++ ++ /** ++ * Represents a custom single line chart. ++ */ ++ public static class SingleLineChart extends CustomChart { ++ ++ private final Callable<Integer> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public SingleLineChart(String chartId, Callable<Integer> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ int value = callable.call(); ++ if (value == 0) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("value", value); ++ return data; ++ } ++ ++ } ++ ++ /** ++ * Represents a custom multi line chart. ++ */ ++ public static class MultiLineChart extends CustomChart { ++ ++ private final Callable<Map<String, Integer>> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Integer> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean allSkipped = true; ++ for (Map.Entry<String, Integer> entry : map.entrySet()) { ++ if (entry.getValue() == 0) { ++ continue; // Skip this invalid ++ } ++ allSkipped = false; ++ values.put(entry.getKey(), entry.getValue()); ++ } ++ if (allSkipped) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("values", values); ++ return data; ++ } ++ ++ } ++ ++ /** ++ * Represents a custom simple bar chart. ++ */ ++ public static class SimpleBarChart extends CustomChart { ++ ++ private final Callable<Map<String, Integer>> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Integer> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ for (Map.Entry<String, Integer> entry : map.entrySet()) { ++ JSONArray categoryValues = new JSONArray(); ++ categoryValues.add(entry.getValue()); ++ values.put(entry.getKey(), categoryValues); ++ } ++ data.put("values", values); ++ return data; ++ } ++ ++ } ++ ++ /** ++ * Represents a custom advanced bar chart. ++ */ ++ public static class AdvancedBarChart extends CustomChart { ++ ++ private final Callable<Map<String, int[]>> callable; ++ ++ /** ++ * Class constructor. ++ * ++ * @param chartId The id of the chart. ++ * @param callable The callable which is used to request the chart data. ++ */ ++ public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, int[]> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean allSkipped = true; ++ for (Map.Entry<String, int[]> entry : map.entrySet()) { ++ if (entry.getValue().length == 0) { ++ continue; // Skip this invalid ++ } ++ allSkipped = false; ++ JSONArray categoryValues = new JSONArray(); ++ for (int categoryValue : entry.getValue()) { ++ categoryValues.add(categoryValue); ++ } ++ values.put(entry.getKey(), categoryValues); ++ } ++ if (allSkipped) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("values", values); ++ return data; ++ } ++ ++ } ++ ++ public static class PaperMetrics { ++ public static void startMetrics() { ++ // Get the config file ++ File configFile = new File(new File((File) MinecraftServer.getServer().options.valueOf("plugins"), "bStats"), "config.yml"); ++ YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); ++ ++ // Check if the config file exists ++ if (!config.isSet("serverUuid")) { ++ ++ // Add default values ++ config.addDefault("enabled", true); ++ // Every server gets it's unique random id. ++ config.addDefault("serverUuid", UUID.randomUUID().toString()); ++ // Should failed request be logged? ++ config.addDefault("logFailedRequests", false); ++ ++ // Inform the server owners about bStats ++ config.options().header( ++ "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + ++ "To honor their work, you should not disable it.\n" + ++ "This has nearly no effect on the server performance!\n" + ++ "Check out https://bStats.org/ to learn more :)" ++ ).copyDefaults(true); ++ try { ++ config.save(configFile); ++ } catch (IOException ignored) { ++ } ++ } ++ // Load the data ++ String serverUUID = config.getString("serverUuid"); ++ 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.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { ++ String minecraftVersion = Bukkit.getVersion(); ++ minecraftVersion = minecraftVersion.substring(minecraftVersion.indexOf("MC: ") + 4, minecraftVersion.length() - 1); ++ return minecraftVersion; ++ })); ++ ++ 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.DrilldownPie("java_version", () -> { ++ Map<String, Map<String, Integer>> map = new HashMap<>(); ++ String javaVersion = System.getProperty("java.version"); ++ Map<String, Integer> entry = new HashMap<>(); ++ entry.put(javaVersion, 1); ++ ++ // http://openjdk.java.net/jeps/223 ++ // Java decided to change their versioning scheme and in doing so modified the java.version system ++ // property to return $major[.$minor][.$secuity][-ea], as opposed to 1.$major.0_$identifier ++ // we can handle pre-9 by checking if the "major" is equal to "1", otherwise, 9+ ++ String majorVersion = javaVersion.split("\\.")[0]; ++ String release; ++ ++ int indexOf = javaVersion.lastIndexOf('.'); ++ ++ if (majorVersion.equals("1")) { ++ release = "Java " + javaVersion.substring(0, indexOf); ++ } else { ++ // of course, it really wouldn't be all that simple if they didn't add a quirk, now would it ++ // valid strings for the major may potentially include values such as -ea to deannotate a pre release ++ Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); ++ if (versionMatcher.find()) { ++ majorVersion = versionMatcher.group(0); ++ } ++ release = "Java " + majorVersion; ++ } ++ map.put(release, entry); ++ ++ return map; ++ })); ++ ++ metrics.addCustomChart(new Metrics.DrilldownPie("legacy_plugins", () -> { ++ Map<String, Map<String, Integer>> map = new HashMap<>(); ++ ++ // count legacy plugins ++ int legacy = 0; ++ for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { ++ if (CraftMagicNumbers.isLegacy(plugin.getDescription())) { ++ legacy++; ++ } ++ } ++ ++ // insert real value as lower dimension ++ Map<String, Integer> entry = new HashMap<>(); ++ entry.put(String.valueOf(legacy), 1); ++ ++ // create buckets as higher dimension ++ if (legacy == 0) { ++ map.put("0 \uD83D\uDE0E", entry); // :sunglasses: ++ } else if (legacy <= 5) { ++ map.put("1-5", entry); ++ } else if (legacy <= 10) { ++ map.put("6-10", entry); ++ } else if (legacy <= 25) { ++ map.put("11-25", entry); ++ } else if (legacy <= 50) { ++ map.put("26-50", entry); ++ } else { ++ map.put("50+ \uD83D\uDE2D", entry); // :cry: ++ } ++ ++ return map; ++ })); ++ } ++ ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 9c950fc1de15b5039e34a9fdf893e97a8cc13237..6ba90739c20995362a5275e2259cac9e17fbcf59 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -216,6 +216,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); + // Paper end - initialize global and world-defaults configuration + io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command ++ com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics + + this.setPvpAllowed(dedicatedserverproperties.pvp); + this.setFlightAllowed(dedicatedserverproperties.allowFlight); +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index b717c9d8b6edc2cafc9281140913b7bdb6108cf0..ba621fdc82896245f6ce448e084847edc4d3fe08 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -83,6 +83,7 @@ public class SpigotConfig + MinecraftServer.getServer().server.getCommandMap().register( entry.getKey(), "Spigot", entry.getValue() ); + } + ++ /* // Paper - Replace with our own + if ( SpigotConfig.metrics == null ) + { + try +@@ -94,6 +95,7 @@ public class SpigotConfig + Bukkit.getServer().getLogger().log( Level.SEVERE, "Could not start metrics service", ex ); + } + } ++ */ // Paper end + } + + public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public diff --git a/patches/server/0019-Paper-Plugins.patch b/patches/server/0019-Paper-Plugins.patch new file mode 100644 index 0000000000..84a4fe0f8f --- /dev/null +++ b/patches/server/0019-Paper-Plugins.patch @@ -0,0 +1,8165 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <[email protected]> +Date: Wed, 6 Jul 2022 23:00:31 -0400 +Subject: [PATCH] Paper Plugins + +Co-authored-by: Micah Rao <[email protected]> + +diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java +index 95d3320c865d24609252063be07cddfd07d0d6d8..5b070d158760789bbcaa984426a55d20767abe4a 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java +@@ -37,6 +37,7 @@ public final class PaperCommand extends Command { + commands.put(Set.of("entity"), new EntityCommand()); + commands.put(Set.of("reload"), new ReloadCommand()); + commands.put(Set.of("version"), new VersionCommand()); ++ commands.put(Set.of("dumpplugins"), new DumpPluginsCommand()); + + return commands.entrySet().stream() + .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) +diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java +index 5e5ec700a368cfdaa1ea0b3f0fa82089895d4b92..72f2e81b9905a0d57ed8e2a88578f62d5235c456 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommands.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java +@@ -24,5 +24,6 @@ public final class PaperCommands { + COMMANDS.forEach((s, command) -> { + server.server.getCommandMap().register(s, "Paper", command); + }); ++ server.server.getCommandMap().register("bukkit", new PaperPluginsCommand()); + } + } +diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f0fce4113fb07c64adbec029d177c236cbdcbae8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +@@ -0,0 +1,215 @@ ++package io.papermc.paper.command; ++ ++import com.google.common.collect.Lists; ++import io.leangen.geantyref.GenericTypeReflector; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.ProviderStatus; ++import io.papermc.paper.plugin.provider.ProviderStatusHolder; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.JoinConfiguration; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextColor; ++import org.bukkit.Bukkit; ++import org.bukkit.command.CommandSender; ++import org.bukkit.command.defaults.BukkitCommand; ++import org.bukkit.craftbukkit.util.CraftMagicNumbers; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.NotNull; ++ ++import java.lang.reflect.Type; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.List; ++import java.util.TreeMap; ++ ++public class PaperPluginsCommand extends BukkitCommand { ++ ++ private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218); ++ ++ // TODO: LINK? ++ private static final Component SERVER_PLUGIN_INFO = Component.text("ℹ What is a server plugin?", INFO_COLOR) ++ .append(asPlainComponents(""" ++ Server plugins can add new behavior to your server! ++ You can find new plugins on Paper's plugin repository, Hangar. ++ ++ <link to hangar> ++ """)); ++ ++ private static final Component SERVER_INITIALIZER_INFO = Component.text("ℹ What is a server initializer?", INFO_COLOR) ++ .append(asPlainComponents(""" ++ Server initializers are ran before your server ++ starts and are provided by paper plugins. ++ """)); ++ ++ private static final Component LEGACY_PLUGIN_INFO = Component.text("ℹ What is a legacy plugin?", INFO_COLOR) ++ .append(asPlainComponents(""" ++ A legacy plugin is a plugin that was made on ++ very old unsupported versions of the game. ++ ++ It is encouraged that you replace this plugin, ++ as they might not work in the future and may cause ++ performance issues. ++ """)); ++ ++ private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO); ++ private static final Component INFO_ICON_START = Component.text("ℹ ", INFO_COLOR); ++ private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209)); ++ private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6)); ++ private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY); ++ private static final Component PLUGIN_TICK_EMPTY = Component.text(" "); ++ ++ private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken<PluginProvider<JavaPlugin>>() {}.getType(); ++ ++ public PaperPluginsCommand() { ++ super("plugins"); ++ this.description = "Gets a list of plugins running on the server"; ++ this.usageMessage = "/plugins"; ++ this.setPermission("bukkit.command.plugins"); ++ this.setAliases(Arrays.asList("pl")); ++ } ++ ++ private static <T> List<Component> formatProviders(TreeMap<String, PluginProvider<T>> plugins) { ++ List<Component> components = new ArrayList<>(plugins.size()); ++ for (PluginProvider<T> entry : plugins.values()) { ++ components.add(formatProvider(entry)); ++ } ++ ++ boolean isFirst = true; ++ List<Component> formattedSublists = new ArrayList<>(); ++ /* ++ Split up the plugin list for each 10 plugins to get size down ++ ++ Plugin List: ++ - Plugin 1, Plugin 2, .... Plugin 10, ++ Plugin 11, Plugin 12 ... Plugin 20, ++ */ ++ for (List<Component> componentSublist : Lists.partition(components, 10)) { ++ Component component = Component.space(); ++ if (isFirst) { ++ component = component.append(PLUGIN_TICK); ++ isFirst = false; ++ } else { ++ component = PLUGIN_TICK_EMPTY; ++ //formattedSublists.add(Component.empty()); // Add an empty line, the auto chat wrapping and this makes it quite jarring. ++ } ++ ++ formattedSublists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist))); ++ } ++ ++ return formattedSublists; ++ } ++ ++ private static Component formatProvider(PluginProvider<?> provider) { ++ TextComponent.Builder builder = Component.text(); ++ if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) { ++ builder.append(LEGACY_PLUGIN_STAR); ++ } ++ ++ String name = provider.getMeta().getName(); ++ Component pluginName = Component.text(name, fromStatus(provider)) ++ .clickEvent(ClickEvent.runCommand("/version " + name)); ++ ++ builder.append(pluginName); ++ ++ return builder.build(); ++ } ++ ++ private static Component asPlainComponents(String strings) { ++ net.kyori.adventure.text.TextComponent.Builder builder = Component.text(); ++ for (String string : strings.split("\n")) { ++ builder.append(Component.newline()); ++ builder.append(Component.text(string, NamedTextColor.WHITE)); ++ } ++ ++ return builder.build(); ++ } ++ ++ private static TextColor fromStatus(PluginProvider<?> provider) { ++ if (provider instanceof ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) { ++ ProviderStatus status = statusHolder.getLastProvidedStatus(); ++ ++ // Handle enabled/disabled game plugins ++ if (status == ProviderStatus.INITIALIZED && GenericTypeReflector.isSuperType(JAVA_PLUGIN_PROVIDER_TYPE, provider.getClass())) { ++ Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName()); ++ // Plugin doesn't exist? Could be due to it being removed. ++ if (plugin == null) { ++ return NamedTextColor.RED; ++ } ++ ++ return plugin.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED; ++ } ++ ++ return switch (status) { ++ case INITIALIZED -> NamedTextColor.GREEN; ++ case ERRORED -> NamedTextColor.RED; ++ }; ++ } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) { ++ // Paper plugins will be skipped if their provider is skipped due to their initializer failing. ++ // Show them as red ++ return NamedTextColor.RED; ++ } else { ++ // Separated for future logic choice, but this indicated a provider that failed to load due to ++ // dependency issues or what not. ++ return NamedTextColor.RED; ++ } ++ } ++ ++ @Override ++ public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) { ++ if (!this.testPermission(sender)) return true; ++ ++ TreeMap<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); ++ TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); ++ ++ ++ for (PluginProvider<JavaPlugin> provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { ++ PluginMeta configuration = provider.getMeta(); ++ ++ if (provider instanceof SpigotPluginProvider) { ++ spigotPlugins.put(configuration.getDisplayName(), provider); ++ } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { ++ paperPlugins.put(configuration.getDisplayName(), provider); ++ } ++ } ++ ++ 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); ++ ++ if (!paperPlugins.isEmpty()) { ++ sender.sendMessage(PAPER_HEADER); ++ } ++ ++ for (Component component : formatProviders(paperPlugins)) { ++ sender.sendMessage(component); ++ } ++ ++ if (!spigotPlugins.isEmpty()) { ++ sender.sendMessage(BUKKIT_HEADER); ++ } ++ ++ for (Component component : formatProviders(spigotPlugins)) { ++ sender.sendMessage(component); ++ } ++ ++ return true; ++ } ++ ++ @NotNull ++ @Override ++ public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { ++ return Collections.emptyList(); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d4a092243e587e3a555fbc0f00c8f78c00b3d1c6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java +@@ -0,0 +1,203 @@ ++package io.papermc.paper.command.subcommands; ++ ++import com.google.common.graph.GraphBuilder; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++import com.google.gson.internal.Streams; ++import com.google.gson.stream.JsonWriter; ++import io.papermc.paper.command.PaperSubcommand; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.entrypoint.classloader.group.LockingClassLoaderGroup; ++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage; ++import io.papermc.paper.plugin.entrypoint.classloader.group.SimpleListPluginClassLoaderGroup; ++import io.papermc.paper.plugin.entrypoint.classloader.group.SpigotPluginClassLoaderGroup; ++import io.papermc.paper.plugin.entrypoint.classloader.group.StaticPluginClassLoaderGroup; ++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; ++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; ++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; ++import io.papermc.paper.plugin.storage.ConfiguredProviderStorage; ++import io.papermc.paper.plugin.storage.ProviderStorage; ++import net.minecraft.server.MinecraftServer; ++import org.bukkit.command.CommandSender; ++import org.bukkit.plugin.Plugin; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import java.io.PrintStream; ++import java.io.StringWriter; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.time.LocalDateTime; ++import java.time.format.DateTimeFormatter; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++ ++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; ++ ++@DefaultQualifier(NonNull.class) ++public final class DumpPluginsCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.dumpPlugins(sender, args); ++ return true; ++ } ++ ++ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); ++ ++ private void dumpPlugins(final CommandSender sender, final String[] args) { ++ Path parent = Path.of("debug"); ++ Path path = parent.resolve("plugin-info-" + FORMATTER.format(LocalDateTime.now()) + ".txt"); ++ try { ++ Files.createDirectories(parent); ++ Files.createFile(path); ++ sender.sendMessage(text("Writing plugin information to " + path, GREEN)); ++ ++ final JsonObject data = this.writeDebug(); ++ ++ StringWriter stringWriter = new StringWriter(); ++ JsonWriter jsonWriter = new JsonWriter(stringWriter); ++ jsonWriter.setIndent(" "); ++ jsonWriter.setLenient(false); ++ Streams.write(data, jsonWriter); ++ ++ try (PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8)) { ++ out.print(stringWriter); ++ } ++ sender.sendMessage(text("Successfully written plugin debug information!", GREEN)); ++ } catch (Throwable e) { ++ sender.sendMessage(text("Failed to write plugin information! See the console for more info.", RED)); ++ MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", e); ++ } ++ } ++ ++ private JsonObject writeDebug() { ++ JsonObject root = new JsonObject(); ++ if (ConfiguredProviderStorage.LEGACY_PLUGIN_LOADING) { ++ root.addProperty("legacy-loading-strategy", true); ++ } ++ ++ this.writeProviders(root); ++ this.writePlugins(root); ++ this.writeClassloaders(root); ++ ++ return root; ++ } ++ ++ private void writeProviders(JsonObject root) { ++ JsonObject rootProviders = new JsonObject(); ++ root.add("providers", rootProviders); ++ ++ for (Map.Entry<Entrypoint<?>, ProviderStorage<?>> entry : LaunchEntryPointHandler.INSTANCE.getStorage().entrySet()) { ++ JsonObject entrypoint = new JsonObject(); ++ ++ JsonArray providers = new JsonArray(); ++ entrypoint.add("providers", providers); ++ ++ List<PluginProvider<Object>> pluginProviders = new ArrayList<>(); ++ for (PluginProvider<?> provider : entry.getValue().getRegisteredProviders()) { ++ JsonObject providerObj = new JsonObject(); ++ providerObj.addProperty("name", provider.getMeta().getName()); ++ providerObj.addProperty("version", provider.getMeta().getVersion()); ++ providerObj.addProperty("dependencies", provider.getMeta().getPluginDependencies().toString()); ++ providerObj.addProperty("soft-dependencies", provider.getMeta().getPluginSoftDependencies().toString()); ++ providerObj.addProperty("load-before", provider.getMeta().getLoadBeforePlugins().toString()); ++ ++ ++ providers.add(providerObj); ++ pluginProviders.add((PluginProvider<Object>) provider); ++ } ++ ++ JsonArray loadOrder = new JsonArray(); ++ entrypoint.add("load-order", loadOrder); ++ ++ ModernPluginLoadingStrategy<Object> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() { ++ @Override ++ public void applyContext(PluginProvider<Object> provider, DependencyContext dependencyContext) { ++ } ++ ++ @Override ++ public boolean load(PluginProvider<Object> provider, Object provided) { ++ return true; ++ } ++ ++ @Override ++ public boolean preloadProvider(PluginProvider<Object> provider) { ++ // Don't load provider ++ loadOrder.add(provider.getMeta().getName()); ++ return false; ++ } ++ }); ++ modernPluginLoadingStrategy.loadProviders(pluginProviders, new SimpleMetaDependencyTree(GraphBuilder.directed().build())); ++ ++ rootProviders.add(entry.getKey().getDebugName(), entrypoint); ++ } ++ } ++ ++ private void writePlugins(JsonObject root) { ++ JsonArray rootPlugins = new JsonArray(); ++ root.add("plugins", rootPlugins); ++ ++ for (Plugin plugin : PaperPluginManagerImpl.getInstance().getPlugins()) { ++ rootPlugins.add(plugin.toString()); ++ } ++ } ++ ++ private void writeClassloaders(JsonObject root) { ++ JsonObject classLoadersRoot = new JsonObject(); ++ root.add("classloaders", classLoadersRoot); ++ ++ PaperPluginClassLoaderStorage storage = (PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance(); ++ classLoadersRoot.addProperty("global", storage.getGlobalGroup().toString()); ++ classLoadersRoot.addProperty("dependency_graph", PaperPluginManagerImpl.getInstance().getInstanceManagerGraph().toString()); ++ ++ JsonArray array = new JsonArray(); ++ classLoadersRoot.add("children", array); ++ for (PluginClassLoaderGroup group : storage.getGroups()) { ++ array.add(this.writeClassloader(group)); ++ } ++ } ++ ++ private JsonObject writeClassloader(PluginClassLoaderGroup group) { ++ JsonObject classLoadersRoot = new JsonObject(); ++ if (group instanceof SimpleListPluginClassLoaderGroup listGroup) { ++ JsonArray array = new JsonArray(); ++ classLoadersRoot.addProperty("main", listGroup.toString()); ++ if (group instanceof StaticPluginClassLoaderGroup staticPluginClassLoaderGroup) { ++ classLoadersRoot.addProperty("plugin-holder", staticPluginClassLoaderGroup.getPluginClassloader().toString()); ++ } else if (group instanceof SpigotPluginClassLoaderGroup spigotPluginClassLoaderGroup) { ++ classLoadersRoot.addProperty("plugin-holder", spigotPluginClassLoaderGroup.getPluginClassLoader().toString()); ++ } ++ ++ classLoadersRoot.add("children", array); ++ for (ConfiguredPluginClassLoader innerGroup : listGroup.getClassLoaders()) { ++ array.add(this.writeClassloader(innerGroup)); ++ } ++ ++ } else if (group instanceof LockingClassLoaderGroup locking) { ++ // Unwrap ++ return this.writeClassloader(locking.getParent()); ++ } else { ++ classLoadersRoot.addProperty("raw", group.toString()); ++ } ++ ++ return classLoadersRoot; ++ } ++ ++ private JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) { ++ return new JsonPrimitive(innerGroup.toString()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..708e5bb9bbf0476fcc2c4b92c6830b094703b43e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +@@ -0,0 +1,134 @@ ++package io.papermc.paper.plugin; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.PaperConfigurations; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import joptsimple.OptionSet; ++import net.minecraft.server.dedicated.DedicatedServer; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.bukkit.craftbukkit.CraftServer; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import org.slf4j.Logger; ++ ++import java.io.File; ++import java.io.IOException; ++import java.nio.file.Files; ++import java.nio.file.Path; ++ ++public class PluginInitializerManager { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private static PluginInitializerManager impl; ++ private final Path pluginDirectory; ++ private final Path updateDirectory; ++ ++ PluginInitializerManager(final Path pluginDirectory, final Path updateDirectory) { ++ this.pluginDirectory = pluginDirectory; ++ this.updateDirectory = updateDirectory; ++ } ++ ++ private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception { ++ // We have to load the bukkit configuration inorder to get the update folder location. ++ final File configFileLocationBukkit = (File) minecraftOptionSet.valueOf("bukkit-settings"); ++ ++ final Path pluginDirectory = ((File) minecraftOptionSet.valueOf("plugins")).toPath(); ++ ++ final YamlConfiguration configuration = PaperConfigurations.loadLegacyConfigFile(configFileLocationBukkit); ++ ++ final String updateDirectoryName = configuration.getString("settings.update-folder", "update"); ++ if (updateDirectoryName.isBlank()) { ++ return new PluginInitializerManager(pluginDirectory, null); ++ } ++ ++ final Path resolvedUpdateDirectory = pluginDirectory.resolve(updateDirectoryName); ++ if (!Files.isDirectory(resolvedUpdateDirectory)) { ++ if (Files.exists(resolvedUpdateDirectory)) { ++ LOGGER.error("Misconfigured update directory!"); ++ LOGGER.error("Your configured update directory ({}) in bukkit.yml is pointing to a non-directory path. " + ++ "Auto updating functionality will not work.", resolvedUpdateDirectory); ++ } ++ return new PluginInitializerManager(pluginDirectory, null); ++ } ++ ++ boolean isSameFile; ++ try { ++ isSameFile = Files.isSameFile(resolvedUpdateDirectory, pluginDirectory); ++ } catch (final IOException e) { ++ LOGGER.error("Misconfigured update directory!"); ++ LOGGER.error("Failed to compare update/plugin directory", e); ++ return new PluginInitializerManager(pluginDirectory, null); ++ } ++ ++ if (isSameFile) { ++ LOGGER.error("Misconfigured update directory!"); ++ LOGGER.error(("Your configured update directory (%s) in bukkit.yml is pointing to the same location as the plugin directory (%s). " + ++ "Disabling auto updating functionality.").formatted(resolvedUpdateDirectory, pluginDirectory)); ++ ++ return new PluginInitializerManager(pluginDirectory, null); ++ } ++ ++ return new PluginInitializerManager(pluginDirectory, resolvedUpdateDirectory); ++ } ++ ++ public static PluginInitializerManager init(final OptionSet optionSet) throws Exception { ++ impl = parse(optionSet); ++ return impl; ++ } ++ ++ public static PluginInitializerManager instance() { ++ return impl; ++ } ++ ++ @NotNull ++ public Path pluginDirectoryPath() { ++ return pluginDirectory; ++ } ++ ++ @Nullable ++ public Path pluginUpdatePath() { ++ return updateDirectory; ++ } ++ ++ public static void load(OptionSet optionSet) throws Exception { ++ // We have to load the bukkit configuration inorder to get the update folder location. ++ io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionSet); ++ ++ // Register the default plugin directory ++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath()); ++ ++ // Register plugins from the flag ++ @SuppressWarnings("unchecked") ++ java.util.List<Path> files = ((java.util.List<File>) optionSet.valuesOf("add-plugin")).stream().map(File::toPath).toList(); ++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files); ++ } ++ ++ // This will be the end of me... ++ public static void reload(DedicatedServer dedicatedServer) { ++ // Wipe the provider storage ++ LaunchEntryPointHandler.INSTANCE.populateProviderStorage(); ++ try { ++ load(dedicatedServer.options); ++ } catch (Exception e) { ++ throw new RuntimeException("Failed to reload!", e); ++ } ++ ++ boolean hasPaperPlugin = false; ++ for (PluginProvider<?> provider : LaunchEntryPointHandler.INSTANCE.getStorage().get(Entrypoint.PLUGIN).getRegisteredProviders()) { ++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { ++ hasPaperPlugin = true; ++ break; ++ } ++ } ++ ++ if (hasPaperPlugin) { ++ LOGGER.warn("======== WARNING ========"); ++ LOGGER.warn("You are reloading while having Paper plugins installed on your server."); ++ LOGGER.warn("Paper plugins do NOT support being reloaded. This will cause some unexpected issues."); ++ LOGGER.warn("========================="); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..30b50e6294c6eaade5e17cfaf34600d122e6251c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java +@@ -0,0 +1,48 @@ ++package io.papermc.paper.plugin.bootstrap; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import java.nio.file.Path; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import org.jetbrains.annotations.NotNull; ++ ++public final class PluginBootstrapContextImpl implements BootstrapContext { ++ ++ private final PluginMeta config; ++ private final Path dataFolder; ++ private final ComponentLogger logger; ++ private final Path pluginSource; ++ ++ public PluginBootstrapContextImpl(PluginMeta config, Path dataFolder, ComponentLogger logger, Path pluginSource) { ++ this.config = config; ++ this.dataFolder = dataFolder; ++ this.logger = logger; ++ this.pluginSource = pluginSource; ++ } ++ ++ public static PluginBootstrapContextImpl create(PluginProvider<?> provider, Path pluginFolder) { ++ Path dataFolder = pluginFolder.resolve(provider.getMeta().getName()); ++ ++ return new PluginBootstrapContextImpl(provider.getMeta(), dataFolder, provider.getLogger(), provider.getSource()); ++ } ++ ++ @Override ++ public @NotNull PluginMeta getConfiguration() { ++ return this.config; ++ } ++ ++ @Override ++ public @NotNull Path getDataDirectory() { ++ return this.dataFolder; ++ } ++ ++ @Override ++ public @NotNull ComponentLogger getLogger() { ++ return this.logger; ++ } ++ ++ @Override ++ public @NotNull Path getPluginSource() { ++ return this.pluginSource; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ef74ca64c589c9158909a2356e73a23f4f01faa2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java +@@ -0,0 +1,49 @@ ++package io.papermc.paper.plugin.bootstrap; ++ ++import io.papermc.paper.plugin.PluginInitializerManager; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import org.jetbrains.annotations.NotNull; ++ ++import java.nio.file.Path; ++ ++public final class PluginProviderContextImpl implements PluginProviderContext { ++ ++ private final PluginMeta config; ++ private final Path dataFolder; ++ private final ComponentLogger logger; ++ private final Path pluginSource; ++ ++ public PluginProviderContextImpl(PluginMeta config, Path dataFolder, ComponentLogger logger, Path pluginSource) { ++ this.config = config; ++ this.dataFolder = dataFolder; ++ this.logger = logger; ++ this.pluginSource = pluginSource; ++ } ++ ++ public static PluginProviderContextImpl create(PluginMeta config, ComponentLogger logger, Path pluginSource) { ++ Path dataFolder = PluginInitializerManager.instance().pluginDirectoryPath().resolve(config.getName()); ++ ++ return new PluginProviderContextImpl(config, dataFolder, logger, pluginSource); ++ } ++ ++ @Override ++ public @NotNull PluginMeta getConfiguration() { ++ return this.config; ++ } ++ ++ @Override ++ public @NotNull Path getDataDirectory() { ++ return this.dataFolder; ++ } ++ ++ @Override ++ public @NotNull ComponentLogger getLogger() { ++ return this.logger; ++ } ++ ++ @Override ++ public @NotNull Path getPluginSource() { ++ return this.pluginSource; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java +new file mode 100644 +index 0000000000000000000000000000000000000000..125008ac7db8b9f3fb57c49f8e4facc4ad4bb136 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java +@@ -0,0 +1,25 @@ ++package io.papermc.paper.plugin.entrypoint; ++ ++import io.papermc.paper.plugin.bootstrap.PluginBootstrap; ++import org.bukkit.plugin.java.JavaPlugin; ++ ++/** ++ * Used to mark a certain place that {@link EntrypointHandler} will register {@link io.papermc.paper.plugin.provider.PluginProvider} under. ++ * Used for loading only certain providers at a certain time. ++ * @param <T> provider type ++ */ ++public final class Entrypoint<T> { ++ ++ public static final Entrypoint<PluginBootstrap> BOOTSTRAPPER = new Entrypoint<>("bootstrapper"); ++ public static final Entrypoint<JavaPlugin> PLUGIN = new Entrypoint<>("plugin"); ++ ++ private final String debugName; ++ ++ private Entrypoint(String debugName) { ++ this.debugName = debugName; ++ } ++ ++ public String getDebugName() { ++ return debugName; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b38e1e0f3d3055086f51bb191fd4b60ecf32d016 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java +@@ -0,0 +1,14 @@ ++package io.papermc.paper.plugin.entrypoint; ++ ++import io.papermc.paper.plugin.provider.PluginProvider; ++ ++/** ++ * Represents a register that will register providers at a certain {@link Entrypoint}, ++ * where then when the given {@link Entrypoint} is registered those will be loaded. ++ */ ++public interface EntrypointHandler { ++ ++ <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider); ++ ++ void enter(Entrypoint<?> entrypoint); ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6c0f2c315387734f8dd4a7eca633aa0a9856dd17 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.plugin.entrypoint; ++ ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.storage.BootstrapProviderStorage; ++import io.papermc.paper.plugin.storage.ProviderStorage; ++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage; ++import org.jetbrains.annotations.ApiStatus; ++ ++import java.util.HashMap; ++import java.util.Map; ++ ++/** ++ * Used by the server to register/load plugin bootstrappers and plugins. ++ */ ++public class LaunchEntryPointHandler implements EntrypointHandler { ++ ++ public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler(); ++ private final Map<Entrypoint<?>, ProviderStorage<?>> storage = new HashMap<>(); ++ ++ LaunchEntryPointHandler() { ++ this.populateProviderStorage(); ++ } ++ ++ // Utility ++ public static void enterBootstrappers() { ++ LaunchEntryPointHandler.INSTANCE.enter(Entrypoint.BOOTSTRAPPER); ++ } ++ ++ @Override ++ public void enter(Entrypoint<?> entrypoint) { ++ ProviderStorage<?> storage = this.storage.get(entrypoint); ++ if (storage == null) { ++ throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint)); ++ } ++ ++ storage.enter(); ++ } ++ ++ @Override ++ public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) { ++ ProviderStorage<T> storage = this.get(entrypoint); ++ if (storage == null) { ++ throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint)); ++ } ++ ++ storage.register(provider); ++ } ++ ++ @SuppressWarnings("unchecked") ++ public <T> ProviderStorage<T> get(Entrypoint<T> entrypoint) { ++ return (ProviderStorage<T>) this.storage.get(entrypoint); ++ } ++ ++ // Debug only ++ @ApiStatus.Internal ++ public Map<Entrypoint<?>, ProviderStorage<?>> getStorage() { ++ return storage; ++ } ++ ++ // Reload only ++ public void populateProviderStorage() { ++ this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage()); ++ this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java +new file mode 100644 +index 0000000000000000000000000000000000000000..93b5196a960f3efbe0d28f5527ea2752426213ce +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java +@@ -0,0 +1,22 @@ ++package io.papermc.paper.plugin.entrypoint.classloader; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import net.kyori.adventure.util.Services; ++import org.jetbrains.annotations.ApiStatus; ++ ++public interface ClassloaderBytecodeModifier { ++ ++ static ClassloaderBytecodeModifier bytecodeModifier() { ++ return Provider.INSTANCE; ++ } ++ ++ byte[] modify(PluginMeta config, byte[] bytecode); ++ ++ class Provider { ++ ++ private static final ClassloaderBytecodeModifier INSTANCE = Services.service(ClassloaderBytecodeModifier.class).orElseThrow(); ++ ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f9a2c55a354c877749db3f92956de802ae575788 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java +@@ -0,0 +1,12 @@ ++package io.papermc.paper.plugin.entrypoint.classloader; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++ ++// Stub, implement in future. ++public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier { ++ ++ @Override ++ public byte[] modify(PluginMeta configuration, byte[] bytecode) { ++ return bytecode; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..18401ede9cd1fc7094c6b74859929938e01795ca +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java +@@ -0,0 +1,209 @@ ++package io.papermc.paper.plugin.entrypoint.classloader; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage; ++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; ++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import org.bukkit.Bukkit; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.io.File; ++import java.io.IOException; ++import java.net.URL; ++import java.net.URLClassLoader; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.Enumeration; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.jar.JarFile; ++import java.util.logging.Logger; ++ ++/** ++ * This is similar to a {@link org.bukkit.plugin.java.PluginClassLoader} but is completely kept hidden from the api. ++ * This is only used with Paper plugins. ++ * ++ * @see PaperPluginClassLoaderStorage ++ */ ++public class PaperPluginClassLoader extends PaperSimplePluginClassLoader implements ConfiguredPluginClassLoader { ++ ++ static { ++ registerAsParallelCapable(); ++ } ++ ++ private final URLClassLoader libraryLoader; ++ private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>()); ++ private final Logger logger; ++ @Nullable ++ private JavaPlugin loadedJavaPlugin; ++ @Nullable ++ private PluginClassLoaderGroup group; ++ ++ public PaperPluginClassLoader(Logger logger, Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader, URLClassLoader libraryLoader) throws IOException { ++ super(source, file, configuration, parentLoader); ++ this.libraryLoader = libraryLoader; ++ ++ this.logger = logger; ++ if (this.configuration().hasOpenClassloader()) { ++ this.group = PaperClassLoaderStorage.instance().registerOpenGroup(this); ++ } ++ } ++ ++ private PaperPluginMeta configuration() { ++ return (PaperPluginMeta) this.configuration; ++ } ++ ++ public void refreshClassloaderDependencyTree(DependencyContext dependencyContext) { ++ if (this.configuration().hasOpenClassloader()) { ++ return; ++ } ++ if (this.group != null) { ++ // We need to unregister the classloader inorder to allow for dependencies ++ // to be recalculated ++ PaperClassLoaderStorage.instance().unregisterClassloader(this); ++ } ++ ++ this.group = PaperClassLoaderStorage.instance().registerAccessBackedGroup(this, (classLoader) -> { ++ return dependencyContext.isTransitiveDependency(PaperPluginClassLoader.this.configuration, classLoader.getConfiguration()); ++ }); ++ } ++ ++ @Override ++ public URL getResource(String name) { ++ URL resource = findResource(name); ++ if (resource == null && this.libraryLoader != null) { ++ return this.libraryLoader.getResource(name); ++ } ++ return resource; ++ } ++ ++ @Override ++ public Enumeration<URL> getResources(String name) throws IOException { ++ List<URL> resources = new ArrayList<>(); ++ this.addEnumeration(resources, this.findResources(name)); ++ if (this.libraryLoader != null) { ++ addEnumeration(resources, this.libraryLoader.getResources(name)); ++ } ++ return Collections.enumeration(resources); ++ } ++ ++ private <T> void addEnumeration(List<T> list, Enumeration<T> enumeration) { ++ while (enumeration.hasMoreElements()) { ++ list.add(enumeration.nextElement()); ++ } ++ } ++ ++ @Override ++ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { ++ return this.loadClass(name, resolve, true, true); ++ } ++ ++ @Override ++ public PluginMeta getConfiguration() { ++ return this.configuration; ++ } ++ ++ @Override ++ public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGroup, boolean checkLibraries) throws ClassNotFoundException { ++ try { ++ Class<?> result = super.loadClass(name, resolve); ++ ++ // SPIGOT-6749: Library classes will appear in the above, but we don't want to return them to other plugins ++ if (checkGroup || result.getClassLoader() == this) { ++ return result; ++ } ++ } catch (ClassNotFoundException ignored) { ++ } ++ ++ if (checkLibraries) { ++ try { ++ return this.libraryLoader.loadClass(name); ++ } catch (ClassNotFoundException ignored) { ++ } ++ } ++ ++ if (checkGroup) { ++ // This ignores the libraries of other plugins, unless they are transitive dependencies. ++ if (this.group == null) { ++ throw new IllegalStateException("Tried to resolve class while group was not yet initialized"); ++ } ++ ++ Class<?> clazz = this.group.getClassByName(name, resolve, this); ++ if (clazz != null) { ++ return clazz; ++ } ++ } ++ ++ throw new ClassNotFoundException(name); ++ } ++ ++ @Override ++ public void init(JavaPlugin plugin) { ++ PluginMeta config = this.configuration; ++ PluginDescriptionFile pluginDescriptionFile = new PluginDescriptionFile( ++ config.getName(), ++ config.getName(), ++ config.getProvidedPlugins(), ++ config.getMainClass(), ++ "", // Classloader load order api ++ config.getPluginDependencies(), // Dependencies ++ config.getPluginSoftDependencies(), // Soft Depends ++ config.getLoadBeforePlugins(), // Load Before ++ config.getVersion(), ++ Map.of(), // Commands, we use a separate system ++ config.getDescription(), ++ config.getAuthors(), ++ config.getContributors(), ++ config.getWebsite(), ++ config.getLoggerPrefix(), ++ config.getLoadOrder(), ++ config.getPermissions(), ++ config.getPermissionDefault(), ++ Set.of(), // Aware api ++ config.getAPIVersion(), ++ List.of() // Libraries ++ ); ++ ++ File dataFolder = new File(Bukkit.getPluginsFolder(), pluginDescriptionFile.getName()); ++ ++ plugin.init(Bukkit.getServer(), pluginDescriptionFile, dataFolder, this.source.toFile(), this, config, this.logger); ++ ++ this.loadedJavaPlugin = plugin; ++ } ++ ++ @Nullable ++ @Override ++ public JavaPlugin getPlugin() { ++ return this.loadedJavaPlugin; ++ } ++ ++ @Override ++ public String toString() { ++ return "PaperPluginClassLoader{" + ++ "libraryLoader=" + this.libraryLoader + ++ ", seenIllegalAccess=" + this.seenIllegalAccess + ++ ", loadedJavaPlugin=" + this.loadedJavaPlugin + ++ '}'; ++ } ++ ++ @Override ++ public void close() throws IOException { ++ try (this.jar; this.libraryLoader) { ++ super.close(); ++ } ++ } ++ ++ @Override ++ public @Nullable PluginClassLoaderGroup getGroup() { ++ return this.group; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..967465e542483e93a736129b5f5c6622cefd33fa +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java +@@ -0,0 +1,116 @@ ++package io.papermc.paper.plugin.entrypoint.classloader; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.util.NamespaceChecker; ++import org.jetbrains.annotations.ApiStatus; ++ ++import java.io.IOException; ++import java.io.InputStream; ++import java.net.URL; ++import java.net.URLClassLoader; ++import java.nio.file.Path; ++import java.security.CodeSigner; ++import java.security.CodeSource; ++import java.util.Enumeration; ++import java.util.jar.JarEntry; ++import java.util.jar.JarFile; ++import java.util.jar.Manifest; ++ ++/** ++ * Represents a simple classloader used for paper plugin bootstrappers. ++ */ ++public class PaperSimplePluginClassLoader extends URLClassLoader { ++ ++ static { ++ ClassLoader.registerAsParallelCapable(); ++ } ++ ++ protected final PluginMeta configuration; ++ protected final Path source; ++ protected final Manifest jarManifest; ++ protected final URL jarUrl; ++ protected final JarFile jar; ++ ++ public PaperSimplePluginClassLoader(Path source, JarFile file, PluginMeta configuration, ClassLoader parentLoader) throws IOException { ++ super(source.getFileName().toString(), new URL[]{source.toUri().toURL()}, parentLoader); ++ ++ this.source = source; ++ this.jarManifest = file.getManifest(); ++ this.jarUrl = source.toUri().toURL(); ++ this.configuration = configuration; ++ this.jar = file; ++ } ++ ++ @Override ++ public URL getResource(String name) { ++ return this.findResource(name); ++ } ++ ++ @Override ++ public Enumeration<URL> getResources(String name) throws IOException { ++ return this.findResources(name); ++ } ++ ++ // Bytecode modification supported loader ++ @Override ++ protected Class<?> findClass(String name) throws ClassNotFoundException { ++ NamespaceChecker.validateNameSpaceForClassloading(name); ++ ++ // See UrlClassLoader#findClass(String) ++ String path = name.replace('.', '/').concat(".class"); ++ JarEntry entry = this.jar.getJarEntry(path); ++ if (entry == null) { ++ throw new ClassNotFoundException(name); ++ } ++ ++ // See URLClassLoader#defineClass(String, Resource) ++ byte[] classBytes; ++ ++ try (InputStream is = this.jar.getInputStream(entry)) { ++ classBytes = is.readAllBytes(); ++ } catch (IOException ex) { ++ throw new ClassNotFoundException(name, ex); ++ } ++ ++ classBytes = ClassloaderBytecodeModifier.bytecodeModifier().modify(this.configuration, classBytes); ++ ++ int dot = name.lastIndexOf('.'); ++ if (dot != -1) { ++ String pkgName = name.substring(0, dot); ++ // Get defined package does not correctly handle sealed packages. ++ if (this.getDefinedPackage(pkgName) == null) { ++ try { ++ if (this.jarManifest != null) { ++ this.definePackage(pkgName, this.jarManifest, this.jarUrl); ++ } else { ++ this.definePackage(pkgName, null, null, null, null, null, null, null); ++ } ++ } catch (IllegalArgumentException ex) { ++ // parallel-capable class loaders: re-verify in case of a ++ // race condition ++ if (this.getDefinedPackage(pkgName) == null) { ++ // Should never happen ++ throw new IllegalStateException("Cannot find package " + pkgName); ++ } ++ } ++ } ++ } ++ ++ CodeSigner[] signers = entry.getCodeSigners(); ++ CodeSource source = new CodeSource(this.jarUrl, signers); ++ ++ return this.defineClass(name, classBytes, 0, classBytes.length, source); ++ } ++ ++ @Override ++ public String toString() { ++ return "PaperSimplePluginClassLoader{" + ++ "configuration=" + this.configuration + ++ ", source=" + this.source + ++ ", jarManifest=" + this.jarManifest + ++ ", jarUrl=" + this.jarUrl + ++ ", jar=" + this.jar + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eaf5c794cbe8d6138c9d60eaae20f5fc7711f541 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java +@@ -0,0 +1,47 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import org.jetbrains.annotations.ApiStatus; ++ ++import java.util.ArrayList; ++ ++public class DependencyBasedPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { ++ ++ private final GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup; ++ private final ClassLoaderAccess access; ++ ++ public DependencyBasedPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, ClassLoaderAccess access) { ++ super(new ArrayList<>()); ++ this.access = access; ++ this.globalPluginClassLoaderGroup = globalPluginClassLoaderGroup; ++ } ++ ++ /** ++ * This will refresh the dependencies of the current classloader. ++ */ ++ public void populateDependencies() { ++ this.classloaders.clear(); ++ for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalPluginClassLoaderGroup.getClassLoaders()) { ++ if (this.access.canAccess(configuredPluginClassLoader)) { ++ this.classloaders.add(configuredPluginClassLoader); ++ } ++ } ++ ++ } ++ ++ @Override ++ public ClassLoaderAccess getAccess() { ++ return this.access; ++ } ++ ++ @Override ++ public String toString() { ++ return "DependencyBasedPluginClassLoaderGroup{" + ++ "globalPluginClassLoaderGroup=" + this.globalPluginClassLoaderGroup + ++ ", access=" + this.access + ++ ", classloaders=" + this.classloaders + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2a4933088928a51c8135a3a60b7447d9d10c66c4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java +@@ -0,0 +1,18 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import org.jetbrains.annotations.ApiStatus; ++ ++public class GlobalPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { ++ ++ @Override ++ public ClassLoaderAccess getAccess() { ++ return (v) -> true; ++ } ++ ++ @Override ++ public String toString() { ++ return "GLOBAL:" + super.toString(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aae50ebba6ba1579b75af5370c8b020d2a927b2c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java +@@ -0,0 +1,76 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.HashMap; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.locks.ReentrantReadWriteLock; ++ ++public class LockingClassLoaderGroup implements PluginClassLoaderGroup { ++ ++ private final PluginClassLoaderGroup parent; ++ private final Map<String, ClassLockEntry> classLoadLock = new HashMap<>(); ++ ++ public LockingClassLoaderGroup(PluginClassLoaderGroup parent) { ++ this.parent = parent; ++ } ++ ++ @Override ++ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) { ++ // make MT safe ++ ClassLockEntry lock; ++ synchronized (this.classLoadLock) { ++ lock = this.classLoadLock.computeIfAbsent(name, (x) -> new ClassLockEntry(new AtomicInteger(0), new java.util.concurrent.locks.ReentrantReadWriteLock())); ++ lock.count.incrementAndGet(); ++ } ++ lock.reentrantReadWriteLock.writeLock().lock(); ++ try { ++ return parent.getClassByName(name, resolve, requester); ++ } finally { ++ synchronized (this.classLoadLock) { ++ lock.reentrantReadWriteLock.writeLock().unlock(); ++ if (lock.count.get() == 1) { ++ this.classLoadLock.remove(name); ++ } else { ++ lock.count.decrementAndGet(); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ this.parent.remove(configuredPluginClassLoader); ++ } ++ ++ @Override ++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ this.parent.add(configuredPluginClassLoader); ++ } ++ ++ @Override ++ public ClassLoaderAccess getAccess() { ++ return this.parent.getAccess(); ++ } ++ ++ public PluginClassLoaderGroup getParent() { ++ return parent; ++ } ++ ++ record ClassLockEntry(AtomicInteger count, ReentrantReadWriteLock reentrantReadWriteLock) { ++ } ++ ++ @Override ++ public String toString() { ++ return "LockingClassLoaderGroup{" + ++ "parent=" + this.parent + ++ ", classLoadLock=" + this.classLoadLock + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e6fcdeb96356ff4713627b1458ac8bbfad1866b1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java +@@ -0,0 +1,93 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; ++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; ++import org.bukkit.plugin.java.PluginClassLoader; ++import org.jetbrains.annotations.ApiStatus; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.CopyOnWriteArrayList; ++ ++/** ++ * This is used for connecting multiple classloaders. ++ */ ++public final class PaperPluginClassLoaderStorage implements PaperClassLoaderStorage { ++ ++ private final GlobalPluginClassLoaderGroup globalGroup = new GlobalPluginClassLoaderGroup(); ++ private final List<PluginClassLoaderGroup> groups = new CopyOnWriteArrayList<>(); ++ ++ public PaperPluginClassLoaderStorage() { ++ this.groups.add(this.globalGroup); ++ } ++ ++ @Override ++ public PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader) { ++ return this.registerGroup(pluginClassLoader, new SpigotPluginClassLoaderGroup(this.globalGroup, (library) -> { ++ return pluginClassLoader.dependencyContext.isTransitiveDependency(pluginClassLoader.getConfiguration(), library.getConfiguration()); ++ }, pluginClassLoader)); ++ } ++ ++ @Override ++ public PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader) { ++ return this.registerGroup(classLoader, this.globalGroup); ++ } ++ ++ @Override ++ public PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access) { ++ List<ConfiguredPluginClassLoader> allowedLoaders = new ArrayList<>(); ++ for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalGroup.getClassLoaders()) { ++ if (access.canAccess(configuredPluginClassLoader)) { ++ allowedLoaders.add(configuredPluginClassLoader); ++ } ++ } ++ ++ return this.registerGroup(classLoader, new StaticPluginClassLoaderGroup(allowedLoaders, access, classLoader)); ++ } ++ ++ private PluginClassLoaderGroup registerGroup(ConfiguredPluginClassLoader classLoader, PluginClassLoaderGroup group) { ++ // Now add this classloader to any groups that allows it (includes global) ++ for (PluginClassLoaderGroup loaderGroup : this.groups) { ++ if (loaderGroup.getAccess().canAccess(classLoader)) { ++ loaderGroup.add(classLoader); ++ } ++ } ++ ++ group = new LockingClassLoaderGroup(group); ++ this.groups.add(group); ++ return group; ++ } ++ ++ @Override ++ public void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ this.globalGroup.remove(configuredPluginClassLoader); ++ this.groups.remove(configuredPluginClassLoader.getGroup()); ++ for (PluginClassLoaderGroup group : this.groups) { ++ group.remove(configuredPluginClassLoader); ++ } ++ } ++ ++ @Override ++ public boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader) { ++ if (this.globalGroup.getClassLoaders().contains(pluginLoader)) { ++ return false; ++ } else { ++ this.globalGroup.add(pluginLoader); ++ return true; ++ } ++ } ++ ++ // Debug only ++ @ApiStatus.Internal ++ public GlobalPluginClassLoaderGroup getGlobalGroup() { ++ return this.globalGroup; ++ } ++ ++ // Debug only ++ @ApiStatus.Internal ++ public List<PluginClassLoaderGroup> getGroups() { ++ return this.groups; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..23b6cb297f46c9c2b2944a3ab4031c31414620ad +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java +@@ -0,0 +1,69 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.List; ++import java.util.concurrent.CopyOnWriteArrayList; ++ ++public abstract class SimpleListPluginClassLoaderGroup implements PluginClassLoaderGroup { ++ ++ private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization"); ++ ++ protected final List<ConfiguredPluginClassLoader> classloaders; ++ ++ protected SimpleListPluginClassLoaderGroup() { ++ this(new CopyOnWriteArrayList<>()); ++ } ++ ++ protected SimpleListPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders) { ++ this.classloaders = classloaders; ++ } ++ ++ @Override ++ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) { ++ if (!DISABLE_CLASS_PRIORITIZATION) { ++ try { ++ return this.lookupClass(name, false, requester); // First check the requester ++ } catch (ClassNotFoundException ignored) { ++ } ++ } ++ ++ for (ConfiguredPluginClassLoader loader : this.classloaders) { ++ try { ++ return this.lookupClass(name, resolve, loader); ++ } catch (ClassNotFoundException ignored) { ++ } ++ } ++ ++ return null; ++ } ++ ++ protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException { ++ return current.loadClass(name, resolve, false, true); ++ } ++ ++ @Override ++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ this.classloaders.remove(configuredPluginClassLoader); ++ } ++ ++ @Override ++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ this.classloaders.add(configuredPluginClassLoader); ++ } ++ ++ public List<ConfiguredPluginClassLoader> getClassLoaders() { ++ return classloaders; ++ } ++ ++ @Override ++ public String toString() { ++ return "SimpleListPluginClassLoaderGroup{" + ++ "classloaders=" + this.classloaders + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3b670bd6b35ae7f56488a9b50df54709a0b28901 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java +@@ -0,0 +1,60 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Nullable; ++ ++public class SingletonPluginClassLoaderGroup implements PluginClassLoaderGroup { ++ ++ private final ConfiguredPluginClassLoader configuredPluginClassLoader; ++ private final Access access; ++ ++ public SingletonPluginClassLoaderGroup(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ this.configuredPluginClassLoader = configuredPluginClassLoader; ++ this.access = new Access(); ++ } ++ ++ @Override ++ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) { ++ try { ++ return this.configuredPluginClassLoader.loadClass(name, resolve, false, true); ++ } catch (ClassNotFoundException ignored) { ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ } ++ ++ @Override ++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ } ++ ++ @Override ++ public ClassLoaderAccess getAccess() { ++ return this.access; ++ } ++ ++ @ApiStatus.Internal ++ private class Access implements ClassLoaderAccess { ++ ++ @Override ++ public boolean canAccess(ConfiguredPluginClassLoader classLoader) { ++ return SingletonPluginClassLoaderGroup.this.configuredPluginClassLoader == classLoader; ++ } ++ ++ } ++ ++ @Override ++ public String toString() { ++ return "SingletonPluginClassLoaderGroup{" + ++ "configuredPluginClassLoader=" + this.configuredPluginClassLoader + ++ ", access=" + this.access + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5a9f5b18e24a89dd642149a7a3d49390328b864b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java +@@ -0,0 +1,58 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import org.bukkit.plugin.java.PluginClassLoader; ++import org.jetbrains.annotations.ApiStatus; ++ ++import java.util.function.Predicate; ++ ++/** ++ * Spigot classloaders have the ability to see everything. ++ * However, libraries are ONLY shared depending on their dependencies. ++ */ ++public class SpigotPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { ++ ++ private final Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate; ++ private final PluginClassLoader pluginClassLoader; ++ ++ public SpigotPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate, PluginClassLoader pluginClassLoader) { ++ super(globalPluginClassLoaderGroup.getClassLoaders()); ++ this.libraryClassloaderPredicate = libraryClassloaderPredicate; ++ this.pluginClassLoader = pluginClassLoader; ++ } ++ ++ // Mirrors global list ++ @Override ++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ } ++ ++ @Override ++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ } ++ ++ // Don't allow other plugins to access spigot dependencies, they should instead reference the global list ++ @Override ++ public ClassLoaderAccess getAccess() { ++ return v -> false; ++ } ++ ++ @Override ++ protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException { ++ return current.loadClass(name, resolve, false, this.libraryClassloaderPredicate.test(current)); ++ } ++ ++ // DEBUG ++ public PluginClassLoader getPluginClassLoader() { ++ return pluginClassLoader; ++ } ++ ++ @Override ++ public String toString() { ++ return "SpigotPluginClassLoaderGroup{" + ++ "libraryClassloaderPredicate=" + this.libraryClassloaderPredicate + ++ ", classloaders=" + this.classloaders + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2412155ddfd559023f42ff534b8f06a52588e1e0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java +@@ -0,0 +1,40 @@ ++package io.papermc.paper.plugin.entrypoint.classloader.group; ++ ++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import org.jetbrains.annotations.ApiStatus; ++ ++import java.util.List; ++ ++public class StaticPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { ++ ++ private final ClassLoaderAccess access; ++ // Debug only ++ private final ConfiguredPluginClassLoader mainClassloaderHolder; ++ ++ public StaticPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders, ClassLoaderAccess access, ConfiguredPluginClassLoader mainClassloaderHolder) { ++ super(classloaders); ++ this.access = access; ++ this.mainClassloaderHolder = mainClassloaderHolder; ++ } ++ ++ @Override ++ public ClassLoaderAccess getAccess() { ++ return this.access; ++ } ++ ++ // DEBUG ++ @ApiStatus.Internal ++ public ConfiguredPluginClassLoader getPluginClassloader() { ++ return this.mainClassloaderHolder; ++ } ++ ++ @Override ++ public String toString() { ++ return "StaticPluginClassLoaderGroup{" + ++ "access=" + this.access + ++ ", classloaders=" + this.classloaders + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/BootstrapMetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/BootstrapMetaDependencyTree.java +new file mode 100644 +index 0000000000000000000000000000000000000000..74bf07be300d4893402614c9e9d9ac97dc574ac7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/BootstrapMetaDependencyTree.java +@@ -0,0 +1,44 @@ ++package io.papermc.paper.plugin.entrypoint.dependency; ++ ++import com.google.common.graph.GraphBuilder; ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++ ++public class BootstrapMetaDependencyTree extends MetaDependencyTree { ++ public BootstrapMetaDependencyTree() { ++ this(GraphBuilder.directed().build()); ++ } ++ ++ public BootstrapMetaDependencyTree(MutableGraph<String> graph) { ++ super(graph); ++ } ++ ++ @Override ++ protected void registerDependencies(final String identifier, final PluginMeta meta) { ++ if (!(meta instanceof PaperPluginMeta paperPluginMeta)) { ++ throw new IllegalStateException("Only paper plugins can have a bootstrapper!"); ++ } ++ // Build a validated provider's dependencies into the graph ++ for (String dependency : paperPluginMeta.getBoostrapDependencies().keySet()) { ++ this.graph.putEdge(identifier, dependency); ++ } ++ } ++ ++ @Override ++ protected void unregisterDependencies(final String identifier, final PluginMeta meta) { ++ if (!(meta instanceof PaperPluginMeta paperPluginMeta)) { ++ throw new IllegalStateException("PluginMeta must be a PaperPluginMeta"); ++ } ++ ++ // Build a validated provider's dependencies into the graph ++ for (String dependency : paperPluginMeta.getBoostrapDependencies().keySet()) { ++ this.graph.removeEdge(identifier, dependency); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "BootstrapDependencyTree{" + "graph=" + this.graph + '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f43295fdeaa587cf30c35a1d545167071d58ce4b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java +@@ -0,0 +1,9 @@ ++package io.papermc.paper.plugin.entrypoint.dependency; ++ ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++ ++public interface DependencyContextHolder { ++ ++ void setContext(DependencyContext context); ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a2fa8406bc3f0dcab6805633ae984d031d24692a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java +@@ -0,0 +1,54 @@ ++package io.papermc.paper.plugin.entrypoint.dependency; ++ ++import com.google.common.graph.Graph; ++import com.google.common.graph.Graphs; ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++ ++import java.util.Set; ++ ++@SuppressWarnings("UnstableApiUsage") ++public class GraphDependencyContext implements DependencyContext { ++ ++ private final MutableGraph<String> dependencyGraph; ++ ++ public GraphDependencyContext(MutableGraph<String> dependencyGraph) { ++ this.dependencyGraph = dependencyGraph; ++ } ++ ++ @Override ++ public boolean isTransitiveDependency(PluginMeta plugin, PluginMeta depend) { ++ String pluginIdentifier = plugin.getName(); ++ ++ if (this.dependencyGraph.nodes().contains(pluginIdentifier)) { ++ Set<String> reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier); ++ if (reachableNodes.contains(depend.getName())) { ++ return true; ++ } ++ for (String provided : depend.getProvidedPlugins()) { ++ if (reachableNodes.contains(provided)) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean hasDependency(String pluginIdentifier) { ++ return this.dependencyGraph.nodes().contains(pluginIdentifier); ++ } ++ ++ public MutableGraph<String> getDependencyGraph() { ++ return dependencyGraph; ++ } ++ ++ @Override ++ public String toString() { ++ return "GraphDependencyContext{" + ++ "dependencyGraph=" + this.dependencyGraph + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java +new file mode 100644 +index 0000000000000000000000000000000000000000..461ef285764aac88ced32be5e650b48e8fbc6bfd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java +@@ -0,0 +1,110 @@ ++package io.papermc.paper.plugin.entrypoint.dependency; ++ ++import com.google.common.graph.GraphBuilder; ++import com.google.common.graph.Graphs; ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import org.jetbrains.annotations.NotNull; ++import java.util.HashSet; ++import java.util.Set; ++ ++public abstract class MetaDependencyTree implements DependencyContext { ++ ++ protected final MutableGraph<String> graph; ++ ++ // We need to upkeep a separate collection since when populating ++ // a graph it adds nodes even if they are not present ++ protected final Set<String> dependencies = new HashSet<>(); ++ ++ public MetaDependencyTree() { ++ this(GraphBuilder.directed().build()); ++ } ++ ++ public MetaDependencyTree(MutableGraph<String> graph) { ++ this.graph = graph; ++ } ++ ++ public void add(PluginMeta configuration) { ++ String identifier = configuration.getName(); ++ // Build a validated provider's dependencies into the graph ++ this.registerDependencies(identifier, configuration); ++ ++ this.graph.addNode(identifier); // Make sure dependencies at least have a node ++ ++ // Add the provided plugins to the graph as well ++ for (String provides : configuration.getProvidedPlugins()) { ++ this.graph.putEdge(identifier, provides); ++ this.dependencies.add(provides); ++ } ++ this.dependencies.add(identifier); ++ } ++ ++ protected abstract void registerDependencies(String identifier, PluginMeta meta); ++ ++ public void remove(PluginMeta configuration) { ++ String identifier = configuration.getName(); ++ // Remove a validated provider's dependencies into the graph ++ this.unregisterDependencies(identifier, configuration); ++ ++ this.graph.removeNode(identifier); // Remove root node ++ ++ // Remove the provided plugins to the graph as well ++ for (String provides : configuration.getProvidedPlugins()) { ++ this.graph.removeEdge(identifier, provides); ++ this.dependencies.remove(provides); ++ } ++ this.dependencies.remove(identifier); ++ } ++ ++ protected abstract void unregisterDependencies(String identifier, PluginMeta meta); ++ ++ @Override ++ public boolean isTransitiveDependency(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) { ++ String pluginIdentifier = plugin.getName(); ++ ++ if (this.graph.nodes().contains(pluginIdentifier)) { ++ Set<String> reachableNodes = Graphs.reachableNodes(this.graph, pluginIdentifier); ++ if (reachableNodes.contains(depend.getName())) { ++ return true; ++ } ++ for (String provided : depend.getProvidedPlugins()) { ++ if (reachableNodes.contains(provided)) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean hasDependency(@NotNull String pluginIdentifier) { ++ return this.dependencies.contains(pluginIdentifier); ++ } ++ ++ public void addDirectDependency(String dependency) { ++ this.dependencies.add(dependency); ++ } ++ ++ @Override ++ public String toString() { ++ return "SimpleDependencyTree{" + ++ "graph=" + this.graph + ++ '}'; ++ } ++ ++ public MutableGraph<String> getGraph() { ++ return graph; ++ } ++ ++ public void add(PluginProvider<?> provider) { ++ this.add(provider.getMeta()); ++ } ++ ++ public void remove(PluginProvider<?> provider) { ++ this.remove(provider.getMeta()); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/SimpleMetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/SimpleMetaDependencyTree.java +new file mode 100644 +index 0000000000000000000000000000000000000000..910caf78636769977dff55dd048c0150a36d218d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/SimpleMetaDependencyTree.java +@@ -0,0 +1,40 @@ ++package io.papermc.paper.plugin.entrypoint.dependency; ++ ++import com.google.common.graph.GraphBuilder; ++import com.google.common.graph.Graphs; ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.HashSet; ++import java.util.Set; ++ ++public class SimpleMetaDependencyTree extends MetaDependencyTree { ++ ++ public SimpleMetaDependencyTree() { ++ } ++ ++ public SimpleMetaDependencyTree(final MutableGraph<String> graph) { ++ super(graph); ++ } ++ ++ @Override ++ protected void registerDependencies(final String identifier, final PluginMeta meta) { ++ for (String dependency : meta.getPluginDependencies()) { ++ this.graph.putEdge(identifier, dependency); ++ } ++ for (String dependency : meta.getPluginSoftDependencies()) { ++ this.graph.putEdge(identifier, dependency); ++ } ++ } ++ ++ @Override ++ protected void unregisterDependencies(final String identifier, final PluginMeta meta) { ++ for (String dependency : meta.getPluginDependencies()) { ++ this.graph.removeEdge(identifier, dependency); ++ } ++ for (String dependency : meta.getPluginSoftDependencies()) { ++ this.graph.removeEdge(identifier, dependency); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a9bca905eba67972e4d1b07b1d243272b62fec66 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java +@@ -0,0 +1,354 @@ ++/* ++ * (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors. ++ * ++ * JGraphT : a free Java graph-theory library ++ * ++ * See the CONTRIBUTORS.md file distributed with this work for additional ++ * information regarding copyright ownership. ++ * ++ * This program and the accompanying materials are made available under the ++ * terms of the Eclipse Public License 2.0 which is available at ++ * http://www.eclipse.org/legal/epl-2.0, or the ++ * GNU Lesser General Public License v2.1 or later ++ * which is available at ++ * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html. ++ * ++ * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later ++ */ ++ ++// MODIFICATIONS: ++// - Modified to use a guava graph directly ++ ++package io.papermc.paper.plugin.entrypoint.strategy; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.graph.Graph; ++import com.google.common.graph.GraphBuilder; ++import com.google.common.graph.MutableGraph; ++import com.mojang.datafixers.util.Pair; ++ ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.function.BiConsumer; ++import java.util.function.Consumer; ++ ++/** ++ * Find all simple cycles of a directed graph using the Johnson's algorithm. ++ * ++ * <p> ++ * See:<br> ++ * D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975), ++ * pp. 77-84. ++ * ++ * @param <V> the vertex type. ++ * ++ * @author Nikolay Ognyanov ++ */ ++public class JohnsonSimpleCycles<V> ++{ ++ // The graph. ++ private Graph<V> graph; ++ ++ // The main state of the algorithm. ++ private Consumer<List<V>> cycleConsumer = null; ++ private BiConsumer<V, V> cycleVertexSuccessorConsumer = null; // Paper ++ private V[] iToV = null; ++ private Map<V, Integer> vToI = null; ++ private Set<V> blocked = null; ++ private Map<V, Set<V>> bSets = null; ++ private ArrayDeque<V> stack = null; ++ ++ // The state of the embedded Tarjan SCC algorithm. ++ private List<Set<V>> foundSCCs = null; ++ private int index = 0; ++ private Map<V, Integer> vIndex = null; ++ private Map<V, Integer> vLowlink = null; ++ private ArrayDeque<V> path = null; ++ private Set<V> pathSet = null; ++ ++ /** ++ * Create a simple cycle finder for the specified graph. ++ * ++ * @param graph - the DirectedGraph in which to find cycles. ++ * ++ * @throws IllegalArgumentException if the graph argument is <code> ++ * null</code>. ++ */ ++ public JohnsonSimpleCycles(Graph<V> graph) ++ { ++ Preconditions.checkState(graph.isDirected(), "Graph must be directed"); ++ this.graph = graph; ++ } ++ ++ /** ++ * Find the simple cycles of the graph. ++ * ++ * @return The list of all simple cycles. Possibly empty but never <code>null</code>. ++ */ ++ public List<List<V>> findAndRemoveSimpleCycles() ++ { ++ List<List<V>> result = new ArrayList<>(); ++ findSimpleCycles(result::add, (v, s) -> ((MutableGraph<V>) graph).removeEdge(v, s)); // Paper ++ return result; ++ } ++ ++ /** ++ * Find the simple cycles of the graph. ++ * ++ * @param consumer Consumer that will be called with each cycle found. ++ */ ++ public void findSimpleCycles(Consumer<List<V>> consumer, BiConsumer<V, V> vertexSuccessorConsumer) // Paper ++ { ++ if (graph == null) { ++ throw new IllegalArgumentException("Null graph."); ++ } ++ cycleVertexSuccessorConsumer = vertexSuccessorConsumer; // Paper ++ initState(consumer); ++ ++ int startIndex = 0; ++ int size = graph.nodes().size(); ++ while (startIndex < size) { ++ Pair<Graph<V>, Integer> minSCCGResult = findMinSCSG(startIndex); ++ if (minSCCGResult != null) { ++ startIndex = minSCCGResult.getSecond(); ++ Graph<V> scg = minSCCGResult.getFirst(); ++ V startV = toV(startIndex); ++ for (V v : scg.successors(startV)) { ++ blocked.remove(v); ++ getBSet(v).clear(); ++ } ++ findCyclesInSCG(startIndex, startIndex, scg); ++ startIndex++; ++ } else { ++ break; ++ } ++ } ++ ++ clearState(); ++ } ++ ++ private Pair<Graph<V>, Integer> findMinSCSG(int startIndex) ++ { ++ /* ++ * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph ++ * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected ++ * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum ++ * (among those SCCs) vertex index. We return that index together with the graph. ++ */ ++ initMinSCGState(); ++ ++ List<Set<V>> foundSCCs = findSCCS(startIndex); ++ ++ // find the SCC with the minimum index ++ int minIndexFound = Integer.MAX_VALUE; ++ Set<V> minSCC = null; ++ for (Set<V> scc : foundSCCs) { ++ for (V v : scc) { ++ int t = toI(v); ++ if (t < minIndexFound) { ++ minIndexFound = t; ++ minSCC = scc; ++ } ++ } ++ } ++ if (minSCC == null) { ++ return null; ++ } ++ ++ // build a graph for the SCC found ++ MutableGraph<V> dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build(); ++ ++ for (V v : minSCC) { ++ for (V w : minSCC) { ++ if (graph.hasEdgeConnecting(v, w)) { ++ dependencyGraph.putEdge(v, w); ++ } ++ } ++ } ++ ++ Pair<Graph<V>, Integer> result = Pair.of(dependencyGraph, minIndexFound); ++ clearMinSCCState(); ++ return result; ++ } ++ ++ private List<Set<V>> findSCCS(int startIndex) ++ { ++ // Find SCCs in the subgraph induced ++ // by vertices startIndex and beyond. ++ // A call to StrongConnectivityAlgorithm ++ // would be too expensive because of the ++ // need to materialize the subgraph. ++ // So - do a local search by the Tarjan's ++ // algorithm and pretend that vertices ++ // with an index smaller than startIndex ++ // do not exist. ++ for (V v : graph.nodes()) { ++ int vI = toI(v); ++ if (vI < startIndex) { ++ continue; ++ } ++ if (!vIndex.containsKey(v)) { ++ getSCCs(startIndex, vI); ++ } ++ } ++ List<Set<V>> result = foundSCCs; ++ foundSCCs = null; ++ return result; ++ } ++ ++ private void getSCCs(int startIndex, int vertexIndex) ++ { ++ V vertex = toV(vertexIndex); ++ vIndex.put(vertex, index); ++ vLowlink.put(vertex, index); ++ index++; ++ path.push(vertex); ++ pathSet.add(vertex); ++ ++ Set<V> edges = graph.successors(vertex); ++ for (V successor : edges) { ++ int successorIndex = toI(successor); ++ if (successorIndex < startIndex) { ++ continue; ++ } ++ if (!vIndex.containsKey(successor)) { ++ getSCCs(startIndex, successorIndex); ++ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor))); ++ } else if (pathSet.contains(successor)) { ++ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor))); ++ } ++ } ++ if (vLowlink.get(vertex).equals(vIndex.get(vertex))) { ++ Set<V> result = new HashSet<>(); ++ V temp; ++ do { ++ temp = path.pop(); ++ pathSet.remove(temp); ++ result.add(temp); ++ } while (!vertex.equals(temp)); ++ if (result.size() == 1) { ++ V v = result.iterator().next(); ++ if (graph.edges().contains(vertex)) { ++ foundSCCs.add(result); ++ } ++ } else { ++ foundSCCs.add(result); ++ } ++ } ++ } ++ ++ private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph<V> scg) ++ { ++ /* ++ * Find cycles in a strongly connected graph per Johnson. ++ */ ++ boolean foundCycle = false; ++ V vertex = toV(vertexIndex); ++ stack.push(vertex); ++ blocked.add(vertex); ++ ++ for (V successor : scg.successors(vertex)) { ++ int successorIndex = toI(successor); ++ if (successorIndex == startIndex) { ++ List<V> cycle = new ArrayList<>(stack.size()); ++ stack.descendingIterator().forEachRemaining(cycle::add); ++ cycleConsumer.accept(cycle); ++ cycleVertexSuccessorConsumer.accept(vertex, successor); // Paper ++ //foundCycle = true; // Paper ++ } else if (!blocked.contains(successor)) { ++ boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg); ++ foundCycle = foundCycle || gotCycle; ++ } ++ } ++ if (foundCycle) { ++ unblock(vertex); ++ } else { ++ for (V w : scg.successors(vertex)) { ++ Set<V> bSet = getBSet(w); ++ bSet.add(vertex); ++ } ++ } ++ stack.pop(); ++ return foundCycle; ++ } ++ ++ private void unblock(V vertex) ++ { ++ blocked.remove(vertex); ++ Set<V> bSet = getBSet(vertex); ++ while (bSet.size() > 0) { ++ V w = bSet.iterator().next(); ++ bSet.remove(w); ++ if (blocked.contains(w)) { ++ unblock(w); ++ } ++ } ++ } ++ ++ @SuppressWarnings("unchecked") ++ private void initState(Consumer<List<V>> consumer) ++ { ++ cycleConsumer = consumer; ++ iToV = (V[]) graph.nodes().toArray(); ++ vToI = new HashMap<>(); ++ blocked = new HashSet<>(); ++ bSets = new HashMap<>(); ++ stack = new ArrayDeque<>(); ++ ++ for (int i = 0; i < iToV.length; i++) { ++ vToI.put(iToV[i], i); ++ } ++ } ++ ++ private void clearState() ++ { ++ cycleConsumer = null; ++ iToV = null; ++ vToI = null; ++ blocked = null; ++ bSets = null; ++ stack = null; ++ } ++ ++ private void initMinSCGState() ++ { ++ index = 0; ++ foundSCCs = new ArrayList<>(); ++ vIndex = new HashMap<>(); ++ vLowlink = new HashMap<>(); ++ path = new ArrayDeque<>(); ++ pathSet = new HashSet<>(); ++ } ++ ++ private void clearMinSCCState() ++ { ++ index = 0; ++ foundSCCs = null; ++ vIndex = null; ++ vLowlink = null; ++ path = null; ++ pathSet = null; ++ } ++ ++ private Integer toI(V vertex) ++ { ++ return vToI.get(vertex); ++ } ++ ++ private V toV(Integer i) ++ { ++ return iToV[i]; ++ } ++ ++ private Set<V> getBSet(V v) ++ { ++ // B sets typically not all needed, ++ // so instantiate lazily. ++ return bSets.computeIfAbsent(v, k -> new HashSet<>()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f59f48654eaa299bcac862991b1e2e622264639b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java +@@ -0,0 +1,271 @@ ++package io.papermc.paper.plugin.entrypoint.strategy; ++ ++import com.google.common.graph.GraphBuilder; ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import org.bukkit.plugin.UnknownDependencyException; ++ ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.LinkedList; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.logging.Level; ++import java.util.logging.Logger; ++ ++@SuppressWarnings("UnstableApiUsage") ++public class LegacyPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> { ++ ++ private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy"); ++ private final ProviderConfiguration<T> configuration; ++ ++ public LegacyPluginLoadingStrategy(ProviderConfiguration<T> onLoad) { ++ this.configuration = onLoad; ++ } ++ ++ @Override ++ public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> providers, MetaDependencyTree dependencyTree) { ++ List<ProviderPair<T>> javapluginsLoaded = new ArrayList<>(); ++ MutableGraph<String> dependencyGraph = dependencyTree.getGraph(); ++ ++ Map<String, PluginProvider<T>> providersToLoad = new HashMap<>(); ++ Set<String> loadedPlugins = new HashSet<>(); ++ Map<String, String> pluginsProvided = new HashMap<>(); ++ Map<String, Collection<String>> dependencies = new HashMap<>(); ++ Map<String, Collection<String>> softDependencies = new HashMap<>(); ++ ++ for (PluginProvider<T> provider : providers) { ++ PluginMeta configuration = provider.getMeta(); ++ ++ PluginProvider<T> replacedProvider = providersToLoad.put(configuration.getName(), provider); ++ dependencyTree.addDirectDependency(configuration.getName()); // add to dependency tree ++ if (replacedProvider != null) { ++ LOGGER.severe(String.format( ++ "Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'", ++ configuration.getName(), ++ provider.getSource(), ++ replacedProvider.getSource(), ++ replacedProvider.getParentSource() ++ )); ++ } ++ ++ String removedProvided = pluginsProvided.remove(configuration.getName()); ++ if (removedProvided != null) { ++ LOGGER.warning(String.format( ++ "Ambiguous plugin name `%s'. It is also provided by `%s'", ++ configuration.getName(), ++ removedProvided ++ )); ++ } ++ ++ for (String provided : configuration.getProvidedPlugins()) { ++ PluginProvider<T> pluginProvider = providersToLoad.get(provided); ++ ++ if (pluginProvider != null) { ++ LOGGER.warning(String.format( ++ "`%s provides `%s' while this is also the name of `%s' in `%s'", ++ provider.getSource(), ++ provided, ++ pluginProvider.getSource(), ++ provider.getParentSource() ++ )); ++ } else { ++ String replacedPlugin = pluginsProvided.put(provided, configuration.getName()); ++ dependencyTree.addDirectDependency(provided); // add to dependency tree ++ if (replacedPlugin != null) { ++ LOGGER.warning(String.format( ++ "`%s' is provided by both `%s' and `%s'", ++ provided, ++ configuration.getName(), ++ replacedPlugin ++ )); ++ } ++ } ++ } ++ ++ Collection<String> softDependencySet = provider.getMeta().getPluginSoftDependencies(); ++ if (softDependencySet != null && !softDependencySet.isEmpty()) { ++ if (softDependencies.containsKey(configuration.getName())) { ++ // Duplicates do not matter, they will be removed together if applicable ++ softDependencies.get(configuration.getName()).addAll(softDependencySet); ++ } else { ++ softDependencies.put(configuration.getName(), new LinkedList<String>(softDependencySet)); ++ } ++ ++ for (String depend : softDependencySet) { ++ dependencyGraph.putEdge(configuration.getName(), depend); ++ } ++ } ++ ++ Collection<String> dependencySet = provider.getMeta().getPluginDependencies(); ++ if (dependencySet != null && !dependencySet.isEmpty()) { ++ dependencies.put(configuration.getName(), new LinkedList<String>(dependencySet)); ++ ++ for (String depend : dependencySet) { ++ dependencyGraph.putEdge(configuration.getName(), depend); ++ } ++ } ++ ++ Collection<String> loadBeforeSet = provider.getMeta().getLoadBeforePlugins(); ++ if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) { ++ for (String loadBeforeTarget : loadBeforeSet) { ++ if (softDependencies.containsKey(loadBeforeTarget)) { ++ softDependencies.get(loadBeforeTarget).add(configuration.getName()); ++ } else { ++ // softDependencies is never iterated, so 'ghost' plugins aren't an issue ++ Collection<String> shortSoftDependency = new LinkedList<String>(); ++ shortSoftDependency.add(configuration.getName()); ++ softDependencies.put(loadBeforeTarget, shortSoftDependency); ++ } ++ ++ dependencyGraph.putEdge(loadBeforeTarget, configuration.getName()); ++ } ++ } ++ } ++ ++ while (!providersToLoad.isEmpty()) { ++ boolean missingDependency = true; ++ Iterator<Map.Entry<String, PluginProvider<T>>> providerIterator = providersToLoad.entrySet().iterator(); ++ ++ while (providerIterator.hasNext()) { ++ Map.Entry<String, PluginProvider<T>> entry = providerIterator.next(); ++ String providerIdentifier = entry.getKey(); ++ ++ if (dependencies.containsKey(providerIdentifier)) { ++ Iterator<String> dependencyIterator = dependencies.get(providerIdentifier).iterator(); ++ final Set<String> missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends ++ ++ while (dependencyIterator.hasNext()) { ++ String dependency = dependencyIterator.next(); ++ ++ // Dependency loaded ++ if (loadedPlugins.contains(dependency)) { ++ dependencyIterator.remove(); ++ ++ // We have a dependency not found ++ } else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) { ++ // Paper start ++ missingHardDependencies.add(dependency); ++ } ++ } ++ if (!missingHardDependencies.isEmpty()) { ++ // Paper end ++ missingDependency = false; ++ providerIterator.remove(); ++ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins ++ softDependencies.remove(providerIdentifier); ++ dependencies.remove(providerIdentifier); ++ ++ LOGGER.log( ++ Level.SEVERE, ++ "Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper ++ new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper ++ } ++ ++ if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) { ++ dependencies.remove(providerIdentifier); ++ } ++ } ++ if (softDependencies.containsKey(providerIdentifier)) { ++ Iterator<String> softDependencyIterator = softDependencies.get(providerIdentifier).iterator(); ++ ++ while (softDependencyIterator.hasNext()) { ++ String softDependency = softDependencyIterator.next(); ++ ++ // Soft depend is no longer around ++ if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) { ++ softDependencyIterator.remove(); ++ } ++ } ++ ++ if (softDependencies.get(providerIdentifier).isEmpty()) { ++ softDependencies.remove(providerIdentifier); ++ } ++ } ++ if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) { ++ // We're clear to load, no more soft or hard dependencies left ++ PluginProvider<T> file = providersToLoad.get(providerIdentifier); ++ providerIterator.remove(); ++ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins ++ missingDependency = false; ++ ++ try { ++ this.configuration.applyContext(file, dependencyTree); ++ T loadedPlugin = file.createInstance(); ++ this.warnIfPaperPlugin(file); ++ ++ if (this.configuration.load(file, loadedPlugin)) { ++ loadedPlugins.add(file.getMeta().getName()); ++ loadedPlugins.addAll(file.getMeta().getProvidedPlugins()); ++ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin)); ++ } ++ ++ } catch (Throwable ex) { ++ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper ++ } ++ } ++ } ++ ++ if (missingDependency) { ++ // We now iterate over plugins until something loads ++ // This loop will ignore soft dependencies ++ providerIterator = providersToLoad.entrySet().iterator(); ++ ++ while (providerIterator.hasNext()) { ++ Map.Entry<String, PluginProvider<T>> entry = providerIterator.next(); ++ String plugin = entry.getKey(); ++ ++ if (!dependencies.containsKey(plugin)) { ++ softDependencies.remove(plugin); ++ missingDependency = false; ++ PluginProvider<T> file = entry.getValue(); ++ providerIterator.remove(); ++ ++ try { ++ this.configuration.applyContext(file, dependencyTree); ++ T loadedPlugin = file.createInstance(); ++ this.warnIfPaperPlugin(file); ++ ++ if (this.configuration.load(file, loadedPlugin)) { ++ loadedPlugins.add(file.getMeta().getName()); ++ loadedPlugins.addAll(file.getMeta().getProvidedPlugins()); ++ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin)); ++ } ++ break; ++ } catch (Throwable ex) { ++ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper ++ } ++ } ++ } ++ // We have no plugins left without a depend ++ if (missingDependency) { ++ softDependencies.clear(); ++ dependencies.clear(); ++ Iterator<PluginProvider<T>> failedPluginIterator = providersToLoad.values().iterator(); ++ ++ while (failedPluginIterator.hasNext()) { ++ PluginProvider<T> file = failedPluginIterator.next(); ++ failedPluginIterator.remove(); ++ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper ++ } ++ } ++ } ++ } ++ ++ return javapluginsLoaded; ++ } ++ ++ private void warnIfPaperPlugin(PluginProvider<T> provider) { ++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { ++ provider.getLogger().warn("Loading Paper plugin in the legacy plugin loading logic. This is not recommended and may introduce some differences into load order. It's highly recommended you move away from this if you are wanting to use Paper plugins."); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2ea978ac957849260e7ca69c9ff56588d0ccc41b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java +@@ -0,0 +1,19 @@ ++package io.papermc.paper.plugin.entrypoint.strategy; ++ ++import java.util.List; ++ ++/** ++ * Indicates a dependency cycle within a provider loading sequence. ++ */ ++public class PluginGraphCycleException extends RuntimeException { ++ ++ private final List<List<String>> cycles; ++ ++ public PluginGraphCycleException(List<List<String>> cycles) { ++ this.cycles = cycles; ++ } ++ ++ public List<List<String>> getCycles() { ++ return this.cycles; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..67c4ef672ee509deba2b4bcaac42d9db24d4c89a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java +@@ -0,0 +1,25 @@ ++package io.papermc.paper.plugin.entrypoint.strategy; ++ ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++ ++/** ++ * Used to share code with the modern and legacy plugin load strategy. ++ * ++ * @param <T> ++ */ ++public interface ProviderConfiguration<T> { ++ ++ void applyContext(PluginProvider<T> provider, DependencyContext dependencyContext); ++ ++ boolean load(PluginProvider<T> provider, T provided); ++ ++ default boolean preloadProvider(PluginProvider<T> provider) { ++ return true; ++ } ++ ++ default void onGenericError(PluginProvider<T> provider) { ++ ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a79eb9e2c8c42ecf823aecbd859576415e9981dc +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java +@@ -0,0 +1,22 @@ ++package io.papermc.paper.plugin.entrypoint.strategy; ++ ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.provider.PluginProvider; ++ ++import java.util.List; ++ ++/** ++ * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order. ++ * <p> ++ * Returns providers loaded. ++ * ++ * @param <P> provider type ++ */ ++public interface ProviderLoadingStrategy<P> { ++ ++ List<ProviderPair<P>> loadProviders(List<PluginProvider<P>> providers, MetaDependencyTree dependencyTree); ++ ++ record ProviderPair<P>(PluginProvider<P> provider, P provided) { ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..52a110044611c8a0ace6d49549e8acc16cbbe83d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java +@@ -0,0 +1,58 @@ ++package io.papermc.paper.plugin.entrypoint.strategy; ++ ++import com.google.common.graph.Graph; ++import it.unimi.dsi.fastutil.objects.Object2IntMap; ++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Deque; ++import java.util.List; ++ ++public final class TopographicGraphSorter { ++ ++ // Topographically sort dependencies ++ public static <N> List<N> sortGraph(Graph<N> graph) throws PluginGraphCycleException { ++ List<N> sorted = new ArrayList<>(); ++ Deque<N> roots = new ArrayDeque<>(); ++ Object2IntMap<N> nonRoots = new Object2IntOpenHashMap<>(); ++ ++ for (N node : graph.nodes()) { ++ // Is a node being referred to by any other nodes? ++ int degree = graph.inDegree(node); ++ if (degree == 0) { ++ // Is a root ++ roots.add(node); ++ } else { ++ // Isn't a root, the number represents how many nodes connect to it. ++ nonRoots.put(node, degree); ++ } ++ } ++ ++ // Pick from nodes that aren't referred to anywhere else ++ N next; ++ while ((next = roots.poll()) != null) { ++ for (N successor : graph.successors(next)) { ++ // Traverse through, moving down a degree ++ int newInDegree = nonRoots.removeInt(successor) - 1; ++ ++ if (newInDegree == 0) { ++ roots.add(successor); ++ } else { ++ nonRoots.put(successor, newInDegree); ++ } ++ ++ } ++ sorted.add(next); ++ } ++ ++ if (!nonRoots.isEmpty()) { ++ throw new GraphCycleException(); ++ } ++ ++ return sorted; ++ } ++ ++ public static final class GraphCycleException extends RuntimeException { ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e3f01ec40a704acb46f7ac31d500e9d0185e3db9 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java +@@ -0,0 +1,121 @@ ++package io.papermc.paper.plugin.entrypoint.strategy.modern; ++ ++import com.google.common.collect.Lists; ++import com.google.common.graph.MutableGraph; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.entrypoint.strategy.JohnsonSimpleCycles; ++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; ++import io.papermc.paper.plugin.entrypoint.strategy.TopographicGraphSorter; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider; ++import org.slf4j.Logger; ++ ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++ ++class LoadOrderTree { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private final Map<String, PluginProvider<?>> providerMap; ++ private final MutableGraph<String> graph; ++ ++ public LoadOrderTree(Map<String, PluginProvider<?>> providerMapMirror, MutableGraph<String> graph) { ++ this.providerMap = providerMapMirror; ++ this.graph = graph; ++ } ++ ++ public void add(PluginProvider<?> provider) { ++ LoadOrderConfiguration configuration = provider.createConfiguration(this.providerMap); ++ ++ // Build a validated provider's load order changes ++ String identifier = configuration.getMeta().getName(); ++ for (String dependency : configuration.getLoadAfter()) { ++ if (this.providerMap.containsKey(dependency)) { ++ this.graph.putEdge(identifier, dependency); ++ } ++ } ++ ++ for (String loadBeforeTarget : configuration.getLoadBefore()) { ++ if (this.providerMap.containsKey(loadBeforeTarget)) { ++ this.graph.putEdge(loadBeforeTarget, identifier); ++ } ++ } ++ ++ this.graph.addNode(identifier); // Make sure load order has at least one node ++ } ++ ++ public List<String> getLoadOrder() throws PluginGraphCycleException { ++ List<String> reversedTopographicSort; ++ try { ++ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph)); ++ } catch (TopographicGraphSorter.GraphCycleException exception) { ++ List<List<String>> cycles = new JohnsonSimpleCycles<>(this.graph).findAndRemoveSimpleCycles(); ++ ++ // Only log an error if at least non-Spigot plugin is present in the cycle ++ // Due to Spigot plugin metadata making no distinction between load order and dependencies (= class loader access), cycles are an unfortunate reality we have to deal with ++ Set<String> cyclingPlugins = new HashSet<>(); ++ cycles.forEach(cyclingPlugins::addAll); ++ if (cyclingPlugins.stream().anyMatch(plugin -> { ++ PluginProvider<?> pluginProvider = this.providerMap.get(plugin); ++ return pluginProvider != null && !(pluginProvider instanceof SpigotPluginProvider); ++ })) { ++ logCycleError(cycles, this.providerMap); ++ } ++ ++ // Try again after hopefully having removed all cycles ++ try { ++ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph)); ++ } catch (TopographicGraphSorter.GraphCycleException e) { ++ throw new PluginGraphCycleException(cycles); ++ } ++ } ++ ++ return reversedTopographicSort; ++ } ++ ++ private void logCycleError(List<List<String>> cycles, Map<String, PluginProvider<?>> providerMapMirror) { ++ LOGGER.error("================================="); ++ LOGGER.error("Circular plugin loading detected:"); ++ for (int i = 0; i < cycles.size(); i++) { ++ List<String> cycle = cycles.get(i); ++ LOGGER.error("{}) {} -> {}", i + 1, String.join(" -> ", cycle), cycle.get(0)); ++ for (String pluginName : cycle) { ++ PluginProvider<?> pluginProvider = providerMapMirror.get(pluginName); ++ if (pluginProvider == null) { ++ return; ++ } ++ ++ logPluginInfo(pluginProvider.getMeta()); ++ } ++ } ++ ++ LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help."); ++ LOGGER.error("================================="); ++ } ++ ++ private void logPluginInfo(PluginMeta meta) { ++ if (!meta.getLoadBeforePlugins().isEmpty()) { ++ LOGGER.error(" {} loadbefore: {}", meta.getName(), meta.getLoadBeforePlugins()); ++ } ++ ++ if (meta instanceof PaperPluginMeta paperPluginMeta) { ++ if (!paperPluginMeta.getLoadAfterPlugins().isEmpty()) { ++ LOGGER.error(" {} loadafter: {}", meta.getName(), paperPluginMeta.getLoadAfterPlugins()); ++ } ++ } else { ++ List<String> dependencies = new ArrayList<>(); ++ dependencies.addAll(meta.getPluginDependencies()); ++ dependencies.addAll(meta.getPluginSoftDependencies()); ++ if (!dependencies.isEmpty()) { ++ LOGGER.error(" {} depend/softdepend: {}", meta.getName(), dependencies); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9af388a8e56806ab44f8c3ef4f97086ce38ef3b4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java +@@ -0,0 +1,138 @@ ++package io.papermc.paper.plugin.entrypoint.strategy.modern; ++ ++import com.google.common.collect.Maps; ++import com.google.common.graph.GraphBuilder; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import org.bukkit.plugin.UnknownDependencyException; ++import org.slf4j.Logger; ++ ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++ ++@SuppressWarnings("UnstableApiUsage") ++public class ModernPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private final ProviderConfiguration<T> configuration; ++ ++ public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) { ++ this.configuration = onLoad; ++ } ++ ++ @Override ++ public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> pluginProviders, MetaDependencyTree dependencyTree) { ++ Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>(); ++ Map<String, PluginProvider<?>> providerMapMirror = Maps.transformValues(providerMap, (entry) -> entry.provider); ++ List<PluginProvider<T>> validatedProviders = new ArrayList<>(); ++ ++ // Populate provider map ++ for (PluginProvider<T> provider : pluginProviders) { ++ PluginMeta providerConfig = provider.getMeta(); ++ PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider); ++ ++ PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry); ++ if (replacedProvider != null) { ++ LOGGER.error(String.format( ++ "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'", ++ providerConfig.getName(), ++ provider.getSource(), ++ replacedProvider.provider.getSource(), ++ replacedProvider.provider.getParentSource() ++ )); ++ this.configuration.onGenericError(replacedProvider.provider); ++ } ++ ++ for (String extra : providerConfig.getProvidedPlugins()) { ++ PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry); ++ if (replacedExtraProvider != null) { ++ LOGGER.warn(String.format( ++ "`%s' is provided by both `%s' and `%s'", ++ extra, ++ providerConfig.getName(), ++ replacedExtraProvider.provider.getMeta().getName() ++ )); ++ } ++ } ++ } ++ ++ // Populate dependency tree ++ for (PluginProvider<?> validated : pluginProviders) { ++ dependencyTree.add(validated); ++ } ++ ++ // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid ++ for (PluginProvider<T> provider : pluginProviders) { ++ PluginMeta configuration = provider.getMeta(); ++ ++ // Populate missing dependencies to capture if there are multiple missing ones. ++ List<String> missingDependencies = provider.validateDependencies(dependencyTree); ++ ++ if (missingDependencies.isEmpty()) { ++ validatedProviders.add(provider); ++ } else { ++ LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper ++ // Because the validator is invalid, remove it from the provider map ++ providerMap.remove(configuration.getName()); ++ // Cleanup plugins that failed to load ++ dependencyTree.remove(provider); ++ this.configuration.onGenericError(provider); ++ } ++ } ++ ++ LoadOrderTree loadOrderTree = new LoadOrderTree(providerMapMirror, GraphBuilder.directed().build()); ++ // Populate load order tree ++ for (PluginProvider<?> validated : validatedProviders) { ++ loadOrderTree.add(validated); ++ } ++ ++ // Reverse the topographic search to let us see which providers we can load first. ++ List<String> reversedTopographicSort = loadOrderTree.getLoadOrder(); ++ List<ProviderPair<T>> loadedPlugins = new ArrayList<>(); ++ for (String providerIdentifier : reversedTopographicSort) { ++ // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist. ++ // The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just ++ // nicer to do it here TBH. ++ PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier); ++ if (retrievedProviderEntry == null || retrievedProviderEntry.provided) { ++ // OR if this was already provided (most likely from a plugin that already "provides" that dependency) ++ // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways ++ continue; // Skip provider that doesn't exist.... ++ } ++ retrievedProviderEntry.provided = true; ++ PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider; ++ try { ++ this.configuration.applyContext(retrievedProvider, dependencyTree); ++ ++ if (this.configuration.preloadProvider(retrievedProvider)) { ++ T instance = retrievedProvider.createInstance(); ++ if (this.configuration.load(retrievedProvider, instance)) { ++ loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance)); ++ } ++ } ++ } catch (Throwable ex) { ++ LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper ++ } ++ } ++ ++ return loadedPlugins; ++ } ++ ++ private static class PluginProviderEntry<T> { ++ ++ private final PluginProvider<T> provider; ++ private boolean provided; ++ ++ private PluginProviderEntry(PluginProvider<T> provider) { ++ this.provider = provider; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f38ecd7f65dc24e4a3f0bc675e3730287ac353f1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +@@ -0,0 +1,64 @@ ++package io.papermc.paper.plugin.loader; ++ ++import io.papermc.paper.plugin.bootstrap.PluginProviderContext; ++import io.papermc.paper.plugin.loader.library.ClassPathLibrary; ++import io.papermc.paper.plugin.loader.library.PaperLibraryStore; ++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import org.jetbrains.annotations.NotNull; ++ ++import java.io.IOException; ++import java.net.MalformedURLException; ++import java.net.URL; ++import java.net.URLClassLoader; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.jar.JarFile; ++import java.util.logging.Logger; ++ ++public class PaperClasspathBuilder implements PluginClasspathBuilder { ++ ++ private final List<ClassPathLibrary> libraries = new ArrayList<>(); ++ ++ private final PluginProviderContext context; ++ ++ public PaperClasspathBuilder(PluginProviderContext context) { ++ this.context = context; ++ } ++ ++ @Override ++ public @NotNull PluginProviderContext getContext() { ++ return this.context; ++ } ++ ++ @Override ++ public @NotNull PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary) { ++ this.libraries.add(classPathLibrary); ++ return this; ++ } ++ ++ public PaperPluginClassLoader buildClassLoader(Logger logger, Path source, JarFile jarFile, PaperPluginMeta configuration) { ++ PaperLibraryStore paperLibraryStore = new PaperLibraryStore(); ++ for (ClassPathLibrary library : this.libraries) { ++ library.register(paperLibraryStore); ++ } ++ ++ List<Path> paths = paperLibraryStore.getPaths(); ++ URL[] urls = new URL[paths.size()]; ++ for (int i = 0; i < paths.size(); i++) { ++ Path path = paperLibraryStore.getPaths().get(i); ++ try { ++ urls[i] = path.toUri().toURL(); ++ } catch (MalformedURLException e) { ++ throw new AssertionError(e); ++ } ++ } ++ ++ try { ++ return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader())); ++ } catch (IOException exception) { ++ throw new RuntimeException(exception); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5fcce65009f715d46dd3013f1f92ec8393d66e15 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java +@@ -0,0 +1,21 @@ ++package io.papermc.paper.plugin.loader.library; ++ ++import org.jetbrains.annotations.NotNull; ++ ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++ ++public class PaperLibraryStore implements LibraryStore { ++ ++ private final List<Path> paths = new ArrayList<>(); ++ ++ @Override ++ public void addLibrary(@NotNull Path library) { ++ this.paths.add(library); ++ } ++ ++ public List<Path> getPaths() { ++ return this.paths; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aef19b44075a3b2e8696315baa89117dd8ebb513 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java +@@ -0,0 +1,82 @@ ++package io.papermc.paper.plugin.manager; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.type.PluginFileType; ++import org.bukkit.Bukkit; ++import org.bukkit.event.Event; ++import org.bukkit.event.Listener; ++import org.bukkit.plugin.InvalidDescriptionException; ++import org.bukkit.plugin.InvalidPluginException; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.PluginLoader; ++import org.bukkit.plugin.RegisteredListener; ++import org.bukkit.plugin.UnknownDependencyException; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.NotNull; ++ ++import java.io.File; ++import java.io.FileNotFoundException; ++import java.io.IOException; ++import java.util.Map; ++import java.util.Set; ++import java.util.jar.JarFile; ++import java.util.regex.Pattern; ++ ++/** ++ * A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation ++ * of papers new plugin system. ++ */ ++public class DummyBukkitPluginLoader implements PluginLoader { ++ ++ private static final Pattern[] PATTERNS = new Pattern[0]; ++ ++ @Override ++ public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException { ++ try { ++ return PaperPluginManagerImpl.getInstance().loadPlugin(file); ++ } catch (InvalidDescriptionException e) { ++ throw new InvalidPluginException(e); ++ } ++ } ++ ++ @Override ++ public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException { ++ try (JarFile jar = new JarFile(file)) { ++ PluginFileType<?, ?> type = PluginFileType.guessType(jar); ++ if (type == null) { ++ throw new InvalidDescriptionException(new FileNotFoundException("Jar does not contain plugin.yml")); ++ } ++ ++ PluginMeta meta = type.getConfig(jar); ++ if (meta instanceof PluginDescriptionFile pluginDescriptionFile) { ++ return pluginDescriptionFile; ++ } else { ++ throw new InvalidDescriptionException("Plugin type does not use plugin.yml. Cannot read file description."); ++ } ++ } catch (Exception e) { ++ throw new InvalidDescriptionException(e); ++ } ++ } ++ ++ @Override ++ public @NotNull Pattern[] getPluginFileFilters() { ++ return PATTERNS; ++ } ++ ++ @Override ++ public @NotNull Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) { ++ return PaperPluginManagerImpl.getInstance().paperEventManager.createRegisteredListeners(listener, plugin); ++ } ++ ++ @Override ++ public void enablePlugin(@NotNull Plugin plugin) { ++ Bukkit.getPluginManager().enablePlugin(plugin); ++ } ++ ++ @Override ++ public void disablePlugin(@NotNull Plugin plugin) { ++ Bukkit.getPluginManager().disablePlugin(plugin); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d681222f355af5c4c26f35aaba484a393aee41c6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java +@@ -0,0 +1,61 @@ ++package io.papermc.paper.plugin.manager; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.slf4j.Logger; ++ ++import java.util.ArrayList; ++import java.util.List; ++ ++public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private final List<JavaPlugin> provided = new ArrayList<>(); ++ ++ private final MetaDependencyTree dependencyTree; ++ ++ MultiRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) { ++ this.dependencyTree = dependencyTree; ++ } ++ ++ @Override ++ public void register(PluginProvider<JavaPlugin> provider) { ++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { ++ LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager."); ++ return; ++ } ++ super.register(provider); ++ /* ++ Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage, ++ as putting it into the entrypoint handlers doesn't make much sense. ++ */ ++ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider); ++ } ++ ++ @Override ++ public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) { ++ super.processProvided(provider, provided); ++ this.provided.add(provided); ++ } ++ ++ @Override ++ public boolean throwOnCycle() { ++ return false; ++ } ++ ++ public List<JavaPlugin> getLoaded() { ++ return this.provided; ++ } ++ ++ @Override ++ public MetaDependencyTree createDependencyTree() { ++ return this.dependencyTree; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6f6aaab295018017565ba27d6958a1f5c7b69bc8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.plugin.manager; ++ ++import org.bukkit.permissions.Permissible; ++import org.bukkit.permissions.Permission; ++ ++import java.util.HashMap; ++import java.util.LinkedHashMap; ++import java.util.LinkedHashSet; ++import java.util.Map; ++import java.util.Set; ++ ++class NormalPaperPermissionManager extends PaperPermissionManager { ++ ++ private final Map<String, Permission> permissions = new HashMap<>(); ++ private final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<>(); ++ private final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<>(); ++ private final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<>(); ++ ++ public NormalPaperPermissionManager() { ++ this.defaultPerms().put(true, new LinkedHashSet<>()); ++ this.defaultPerms().put(false, new LinkedHashSet<>()); ++ } ++ ++ @Override ++ public Map<String, Permission> permissions() { ++ return this.permissions; ++ } ++ ++ @Override ++ public Map<Boolean, Set<Permission>> defaultPerms() { ++ return this.defaultPerms; ++ } ++ ++ @Override ++ public Map<String, Map<Permissible, Boolean>> permSubs() { ++ return this.permSubs; ++ } ++ ++ @Override ++ public Map<Boolean, Map<Permissible, Boolean>> defSubs() { ++ return this.defSubs; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +@@ -0,0 +1,194 @@ ++package io.papermc.paper.plugin.manager; ++ ++import co.aikar.timings.TimedEventExecutor; ++import com.destroystokyo.paper.event.server.ServerExceptionEvent; ++import com.destroystokyo.paper.exception.ServerEventException; ++import com.google.common.collect.Sets; ++import org.bukkit.Server; ++import org.bukkit.Warning; ++import org.bukkit.event.Event; ++import org.bukkit.event.EventHandler; ++import org.bukkit.event.EventPriority; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.Listener; ++import org.bukkit.plugin.AuthorNagException; ++import org.bukkit.plugin.EventExecutor; ++import org.bukkit.plugin.IllegalPluginAccessException; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.RegisteredListener; ++import org.jetbrains.annotations.NotNull; ++ ++import java.lang.reflect.Method; ++import java.util.Arrays; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++import java.util.logging.Level; ++ ++class PaperEventManager { ++ ++ private final Server server; ++ ++ public PaperEventManager(Server server) { ++ this.server = server; ++ } ++ ++ // SimplePluginManager ++ public void callEvent(@NotNull Event event) { ++ if (event.isAsynchronous() && this.server.isPrimaryThread()) { ++ throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously."); ++ } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) { ++ throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously."); ++ } ++ ++ HandlerList handlers = event.getHandlers(); ++ RegisteredListener[] listeners = handlers.getRegisteredListeners(); ++ ++ for (RegisteredListener registration : listeners) { ++ if (!registration.getPlugin().isEnabled()) { ++ continue; ++ } ++ ++ try { ++ registration.callEvent(event); ++ } catch (AuthorNagException ex) { ++ Plugin plugin = registration.getPlugin(); ++ ++ if (plugin.isNaggable()) { ++ plugin.setNaggable(false); ++ ++ this.server.getLogger().log(Level.SEVERE, String.format( ++ "Nag author(s): '%s' of '%s' about the following: %s", ++ plugin.getPluginMeta().getAuthors(), ++ plugin.getPluginMeta().getDisplayName(), ++ ex.getMessage() ++ )); ++ } ++ } catch (Throwable ex) { ++ String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getPluginMeta().getDisplayName(); ++ this.server.getLogger().log(Level.SEVERE, msg, ex); ++ if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop ++ this.callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event))); ++ } ++ } ++ } ++ } ++ ++ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) { ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled"); ++ } ++ ++ for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) { ++ this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue()); ++ } ++ ++ } ++ ++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) { ++ this.registerEvent(event, listener, priority, executor, plugin, false); ++ } ++ ++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) { ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled"); ++ } ++ ++ executor = new TimedEventExecutor(executor, plugin, null, event); ++ this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); ++ } ++ ++ @NotNull ++ private HandlerList getEventListeners(@NotNull Class<? extends Event> type) { ++ try { ++ Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList"); ++ method.setAccessible(true); ++ return (HandlerList) method.invoke(null); ++ } catch (Exception e) { ++ throw new IllegalPluginAccessException(e.toString()); ++ } ++ } ++ ++ @NotNull ++ private Class<? extends Event> getRegistrationClass(@NotNull Class<? extends Event> clazz) { ++ try { ++ clazz.getDeclaredMethod("getHandlerList"); ++ return clazz; ++ } catch (NoSuchMethodException e) { ++ if (clazz.getSuperclass() != null ++ && !clazz.getSuperclass().equals(Event.class) ++ && Event.class.isAssignableFrom(clazz.getSuperclass())) { ++ return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class)); ++ } else { ++ throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!"); ++ } ++ } ++ } ++ ++ // JavaPluginLoader ++ @NotNull ++ public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) { ++ Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<>(); ++ ++ Set<Method> methods; ++ try { ++ Class<?> listenerClazz = listener.getClass(); ++ methods = Sets.union( ++ Set.of(listenerClazz.getMethods()), ++ Set.of(listenerClazz.getDeclaredMethods()) ++ ); ++ } catch (NoClassDefFoundError e) { ++ plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist."); ++ return ret; ++ } ++ ++ for (final Method method : methods) { ++ final EventHandler eh = method.getAnnotation(EventHandler.class); ++ if (eh == null) continue; ++ // Do not register bridge or synthetic methods to avoid event duplication ++ // Fixes SPIGOT-893 ++ if (method.isBridge() || method.isSynthetic()) { ++ continue; ++ } ++ final Class<?> checkClass; ++ if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) { ++ plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass()); ++ continue; ++ } ++ final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class); ++ method.setAccessible(true); ++ Set<RegisteredListener> eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>()); ++ ++ for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) { ++ // This loop checks for extending deprecated events ++ if (clazz.getAnnotation(Deprecated.class) != null) { ++ Warning warning = clazz.getAnnotation(Warning.class); ++ Warning.WarningState warningState = this.server.getWarningState(); ++ if (!warningState.printFor(warning)) { ++ break; ++ } ++ plugin.getLogger().log( ++ Level.WARNING, ++ String.format( ++ "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.", ++ plugin.getPluginMeta().getDisplayName(), ++ clazz.getName(), ++ method.toGenericString(), ++ (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected", ++ Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())), ++ warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null); ++ break; ++ } ++ } ++ ++ EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); ++ eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); ++ } ++ return ret; ++ } ++ ++ public void clearEvents() { ++ HandlerList.unregisterAll(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..afe793c35f05a80058e80bcaee76ac45a40b04a2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java +@@ -0,0 +1,201 @@ ++package io.papermc.paper.plugin.manager; ++ ++import com.google.common.collect.ImmutableSet; ++import io.papermc.paper.plugin.PermissionManager; ++import org.bukkit.permissions.Permissible; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.HashSet; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.Set; ++import java.util.WeakHashMap; ++ ++/** ++ * See ++ * {@link StupidSPMPermissionManagerWrapper} ++ */ ++abstract class PaperPermissionManager implements PermissionManager { ++ ++ public abstract Map<String, Permission> permissions(); ++ ++ public abstract Map<Boolean, Set<Permission>> defaultPerms(); ++ ++ public abstract Map<String, Map<Permissible, Boolean>> permSubs(); ++ ++ public abstract Map<Boolean, Map<Permissible, Boolean>> defSubs(); ++ ++ @Override ++ @Nullable ++ public Permission getPermission(@NotNull String name) { ++ return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH)); ++ } ++ ++ @Override ++ public void addPermission(@NotNull Permission perm) { ++ this.addPermission(perm, true); ++ } ++ ++ @Override ++ public void addPermissions(@NotNull List<Permission> permissions) { ++ for (Permission permission : permissions) { ++ this.addPermission(permission, false); ++ } ++ this.dirtyPermissibles(); ++ } ++ ++ // Allow suppressing permission default calculations ++ private void addPermission(@NotNull Permission perm, boolean dirty) { ++ String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH); ++ ++ if (this.permissions().containsKey(name)) { ++ throw new IllegalArgumentException("The permission " + name + " is already defined!"); ++ } ++ ++ this.permissions().put(name, perm); ++ this.calculatePermissionDefault(perm, dirty); ++ } ++ ++ @Override ++ @NotNull ++ public Set<Permission> getDefaultPermissions(boolean op) { ++ return ImmutableSet.copyOf(this.defaultPerms().get(op)); ++ } ++ ++ ++ @Override ++ public void removePermission(@NotNull Permission perm) { ++ this.removePermission(perm.getName()); ++ } ++ ++ ++ @Override ++ public void removePermission(@NotNull String name) { ++ this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH)); ++ } ++ ++ @Override ++ public void recalculatePermissionDefaults(@NotNull Permission perm) { ++ // we need a null check here because some plugins for some unknown reason pass null into this? ++ if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ROOT))) { ++ this.defaultPerms().get(true).remove(perm); ++ this.defaultPerms().get(false).remove(perm); ++ ++ this.calculatePermissionDefault(perm, true); ++ } ++ } ++ ++ private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) { ++ if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) { ++ this.defaultPerms().get(true).add(perm); ++ if (dirty) { ++ this.dirtyPermissibles(true); ++ } ++ } ++ if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) { ++ this.defaultPerms().get(false).add(perm); ++ if (dirty) { ++ this.dirtyPermissibles(false); ++ } ++ } ++ } ++ ++ ++ @Override ++ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) { ++ String name = permission.toLowerCase(java.util.Locale.ENGLISH); ++ Map<Permissible, Boolean> map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>()); ++ ++ map.put(permissible, true); ++ } ++ ++ @Override ++ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) { ++ String name = permission.toLowerCase(java.util.Locale.ENGLISH); ++ Map<Permissible, Boolean> map = this.permSubs().get(name); ++ ++ if (map != null) { ++ map.remove(permissible); ++ ++ if (map.isEmpty()) { ++ this.permSubs().remove(name); ++ } ++ } ++ } ++ ++ @Override ++ @NotNull ++ public Set<Permissible> getPermissionSubscriptions(@NotNull String permission) { ++ String name = permission.toLowerCase(java.util.Locale.ENGLISH); ++ Map<Permissible, Boolean> map = this.permSubs().get(name); ++ ++ if (map == null) { ++ return ImmutableSet.of(); ++ } else { ++ return ImmutableSet.copyOf(map.keySet()); ++ } ++ } ++ ++ @Override ++ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) { ++ Map<Permissible, Boolean> map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>()); ++ ++ map.put(permissible, true); ++ } ++ ++ @Override ++ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) { ++ Map<Permissible, Boolean> map = this.defSubs().get(op); ++ ++ if (map != null) { ++ map.remove(permissible); ++ ++ if (map.isEmpty()) { ++ this.defSubs().remove(op); ++ } ++ } ++ } ++ ++ @Override ++ @NotNull ++ public Set<Permissible> getDefaultPermSubscriptions(boolean op) { ++ Map<Permissible, Boolean> map = this.defSubs().get(op); ++ ++ if (map == null) { ++ return ImmutableSet.of(); ++ } else { ++ return ImmutableSet.copyOf(map.keySet()); ++ } ++ } ++ ++ @Override ++ @NotNull ++ public Set<Permission> getPermissions() { ++ return new HashSet<>(this.permissions().values()); ++ } ++ ++ @Override ++ public void clearPermissions() { ++ this.permissions().clear(); ++ this.defaultPerms().get(true).clear(); ++ this.defaultPerms().get(false).clear(); ++ } ++ ++ ++ void dirtyPermissibles(boolean op) { ++ Set<Permissible> permissibles = this.getDefaultPermSubscriptions(op); ++ ++ for (Permissible p : permissibles) { ++ p.recalculatePermissions(); ++ } ++ } ++ ++ void dirtyPermissibles() { ++ this.dirtyPermissibles(true); ++ this.dirtyPermissibles(false); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d2dee700f2c5cc7d6a272e751a933901fe7a55b6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +@@ -0,0 +1,317 @@ ++package io.papermc.paper.plugin.manager; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.graph.GraphBuilder; ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; ++import io.papermc.paper.plugin.provider.source.DirectoryProviderSource; ++import io.papermc.paper.plugin.provider.source.FileArrayProviderSource; ++import io.papermc.paper.plugin.provider.source.FileProviderSource; ++import java.io.File; ++import org.bukkit.Bukkit; ++import org.bukkit.Server; ++import org.bukkit.World; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandMap; ++import org.bukkit.command.PluginCommandYamlParser; ++import org.bukkit.craftbukkit.util.CraftMagicNumbers; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.server.PluginDisableEvent; ++import org.bukkit.event.server.PluginEnableEvent; ++import org.bukkit.plugin.InvalidPluginException; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.PluginManager; ++import org.bukkit.plugin.UnknownDependencyException; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.io.IOException; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.logging.Level; ++ ++@SuppressWarnings("UnstableApiUsage") ++class PaperPluginInstanceManager { ++ ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted); ++ ++ private final List<Plugin> plugins = new ArrayList<>(); ++ private final Map<String, Plugin> lookupNames = new HashMap<>(); ++ ++ private final PluginManager pluginManager; ++ private final CommandMap commandMap; ++ private final Server server; ++ ++ private final MetaDependencyTree dependencyTree = new SimpleMetaDependencyTree(GraphBuilder.directed().build()); ++ ++ public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) { ++ this.commandMap = commandMap; ++ this.server = server; ++ this.pluginManager = pluginManager; ++ } ++ ++ public @Nullable Plugin getPlugin(@NotNull String name) { ++ return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper ++ } ++ ++ public @NotNull Plugin[] getPlugins() { ++ return this.plugins.toArray(new Plugin[0]); ++ } ++ ++ public boolean isPluginEnabled(@NotNull String name) { ++ Plugin plugin = this.getPlugin(name); ++ ++ return this.isPluginEnabled(plugin); ++ } ++ ++ public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) { ++ if ((plugin != null) && (this.plugins.contains(plugin))) { ++ return plugin.isEnabled(); ++ } else { ++ return false; ++ } ++ } ++ ++ public void loadPlugin(Plugin provided) { ++ PluginMeta configuration = provided.getPluginMeta(); ++ ++ this.plugins.add(provided); ++ this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided); ++ for (String providedPlugin : configuration.getProvidedPlugins()) { ++ this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided); ++ } ++ ++ this.dependencyTree.add(configuration); ++ } ++ ++ // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception. ++ public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException { ++ RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage(this.dependencyTree)); ++ ++ try { ++ path = FILE_PROVIDER_SOURCE.prepareContext(path); ++ FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path); ++ } catch (IllegalArgumentException exception) { ++ return null; // Return null when the plugin file is not valid / plugin type is unknown ++ } catch (PluginGraphCycleException exception) { ++ throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!"); ++ } catch (Exception e) { ++ throw new InvalidPluginException(e); ++ } ++ ++ try { ++ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN); ++ } catch (Throwable e) { ++ throw new InvalidPluginException(e); ++ } ++ ++ return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded() ++ .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?")); ++ } ++ ++ public @NotNull Plugin[] loadPlugins(@NotNull File[] files) { ++ RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree)); ++ try { ++ List<Path> paths = FileArrayProviderSource.INSTANCE.prepareContext(files); ++ DirectoryProviderSource.INSTANCE.registerProviders(runtimePluginEntrypointHandler, paths); ++ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN); ++ } catch (Exception e) { ++ // This should never happen, any errors that occur in this provider should instead be logged. ++ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e); ++ } ++ ++ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]); ++ } ++ ++ // The behavior of this is that all errors are logged instead of being thrown ++ public @NotNull Plugin[] loadPlugins(@NotNull Path directory) { ++ Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist ++ ++ RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree)); ++ try { ++ List<Path> files = DirectoryProviderSource.INSTANCE.prepareContext(directory); ++ DirectoryProviderSource.INSTANCE.registerProviders(runtimePluginEntrypointHandler, files); ++ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN); ++ } catch (Exception e) { ++ // This should never happen, any errors that occur in this provider should instead be logged. ++ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e); ++ } ++ ++ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]); ++ } ++ ++ // Plugins are disabled in order like this inorder to "rougly" prevent ++ // their dependencies unloading first. But, eh. ++ public void disablePlugins() { ++ Plugin[] plugins = this.getPlugins(); ++ for (int i = plugins.length - 1; i >= 0; i--) { ++ this.disablePlugin(plugins[i]); ++ } ++ } ++ ++ public void clearPlugins() { ++ synchronized (this) { ++ this.disablePlugins(); ++ this.plugins.clear(); ++ this.lookupNames.clear(); ++ } ++ } ++ ++ public synchronized void enablePlugin(@NotNull Plugin plugin) { ++ if (plugin.isEnabled()) { ++ return; ++ } ++ ++ if (plugin.getPluginMeta() instanceof PluginDescriptionFile) { ++ List<Command> bukkitCommands = PluginCommandYamlParser.parse(plugin); ++ ++ if (!bukkitCommands.isEmpty()) { ++ this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands); ++ } ++ } ++ ++ try { ++ String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName(); ++ if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) { ++ enableMsg += "*"; ++ } ++ plugin.getLogger().info(enableMsg); ++ ++ JavaPlugin jPlugin = (JavaPlugin) plugin; ++ ++ if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper ++ if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) { ++ this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName()); ++ } ++ } // Paper ++ ++ try { ++ jPlugin.setEnabled(true); ++ } catch (Throwable ex) { ++ this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex); ++ // Paper start - Disable plugins that fail to load ++ this.server.getPluginManager().disablePlugin(jPlugin); ++ return; ++ // Paper end ++ } ++ ++ // Perhaps abort here, rather than continue going, but as it stands, ++ // an abort is not possible the way it's currently written ++ this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin)); ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while enabling " ++ + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin); ++ } ++ ++ HandlerList.bakeAll(); ++ } ++ ++ public synchronized void disablePlugin(@NotNull Plugin plugin) { ++ if (!(plugin instanceof JavaPlugin javaPlugin)) { ++ throw new IllegalArgumentException("Only expects java plugins."); ++ } ++ if (!plugin.isEnabled()) { ++ return; ++ } ++ ++ String pluginName = plugin.getPluginMeta().getDisplayName(); ++ ++ try { ++ plugin.getLogger().info("Disabling %s".formatted(pluginName)); ++ ++ this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin)); ++ try { ++ javaPlugin.setEnabled(false); ++ } catch (Throwable ex) { ++ this.server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + pluginName, ex); ++ } ++ ++ ClassLoader classLoader = plugin.getClass().getClassLoader(); ++ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ try { ++ configuredPluginClassLoader.close(); ++ } catch (IOException ex) { ++ this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception ++ } ++ // Remove from the classloader pool inorder to prevent plugins from trying ++ // to access classes ++ PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader); ++ } ++ ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while disabling " ++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper ++ } ++ ++ try { ++ this.server.getScheduler().cancelTasks(plugin); ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " ++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper ++ } ++ ++ try { ++ this.server.getServicesManager().unregisterAll(plugin); ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for " ++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper ++ } ++ ++ try { ++ HandlerList.unregisterAll(plugin); ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for " ++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper ++ } ++ ++ try { ++ this.server.getMessenger().unregisterIncomingPluginChannel(plugin); ++ this.server.getMessenger().unregisterOutgoingPluginChannel(plugin); ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for " ++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper ++ } ++ ++ try { ++ for (World world : this.server.getWorlds()) { ++ world.removePluginChunkTickets(plugin); ++ } ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper ++ } ++ ++ } ++ ++ // TODO: Implement event part in future patch (paper patch move up, this patch is lower) ++ private void handlePluginException(String msg, Throwable ex, Plugin plugin) { ++ Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex); ++ this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin))); ++ } ++ ++ public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) { ++ return this.dependencyTree.isTransitiveDependency(plugin, depend); ++ } ++ ++ public boolean hasDependency(String pluginIdentifier) { ++ return this.getPlugin(pluginIdentifier) != null; ++ } ++ ++ // Debug only ++ @ApiStatus.Internal ++ public MutableGraph<String> getDependencyGraph() { ++ return this.dependencyTree.getGraph(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..097500a59336db1bbfffcd1aa4cff7a8586e46ec +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java +@@ -0,0 +1,246 @@ ++package io.papermc.paper.plugin.manager; ++ ++import com.google.common.graph.MutableGraph; ++import io.papermc.paper.plugin.PermissionManager; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import org.bukkit.Bukkit; ++import org.bukkit.Server; ++import org.bukkit.command.CommandMap; ++import org.bukkit.craftbukkit.CraftServer; ++import org.bukkit.event.Event; ++import org.bukkit.event.EventPriority; ++import org.bukkit.event.Listener; ++import org.bukkit.permissions.Permissible; ++import org.bukkit.permissions.Permission; ++import org.bukkit.plugin.EventExecutor; ++import org.bukkit.plugin.InvalidDescriptionException; ++import org.bukkit.plugin.InvalidPluginException; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.PluginLoader; ++import org.bukkit.plugin.PluginManager; ++import org.bukkit.plugin.SimplePluginManager; ++import org.bukkit.plugin.UnknownDependencyException; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.io.File; ++import java.util.List; ++import java.util.Set; ++ ++public class PaperPluginManagerImpl implements PluginManager, DependencyContext { ++ ++ final PaperPluginInstanceManager instanceManager; ++ final PaperEventManager paperEventManager; ++ PermissionManager permissionManager; ++ ++ public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) { ++ this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server); ++ this.paperEventManager = new PaperEventManager(server); ++ ++ if (permissionManager == null) { ++ this.permissionManager = new NormalPaperPermissionManager(); ++ } else { ++ this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed ++ } ++ } ++ ++ // REMOVE THIS WHEN SimplePluginManager is removed. ++ // Just cast and use Bukkit.getServer().getPluginManager() ++ public static PaperPluginManagerImpl getInstance() { ++ return ((CraftServer) (Bukkit.getServer())).paperPluginManager; ++ } ++ ++ // Plugin Manipulation ++ ++ @Override ++ public @Nullable Plugin getPlugin(@NotNull String name) { ++ return this.instanceManager.getPlugin(name); ++ } ++ ++ @Override ++ public @NotNull Plugin[] getPlugins() { ++ return this.instanceManager.getPlugins(); ++ } ++ ++ @Override ++ public boolean isPluginEnabled(@NotNull String name) { ++ return this.instanceManager.isPluginEnabled(name); ++ } ++ ++ @Override ++ public boolean isPluginEnabled(@Nullable Plugin plugin) { ++ return this.instanceManager.isPluginEnabled(plugin); ++ } ++ ++ public void loadPlugin(Plugin plugin) { ++ this.instanceManager.loadPlugin(plugin); ++ } ++ ++ @Override ++ public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException { ++ return this.instanceManager.loadPlugin(file.toPath()); ++ } ++ ++ @Override ++ public @NotNull Plugin[] loadPlugins(@NotNull File directory) { ++ return this.instanceManager.loadPlugins(directory.toPath()); ++ } ++ ++ @Override ++ public @NotNull Plugin[] loadPlugins(final @NotNull File[] files) { ++ return this.instanceManager.loadPlugins(files); ++ } ++ ++ @Override ++ public void disablePlugins() { ++ this.instanceManager.disablePlugins(); ++ } ++ ++ @Override ++ public synchronized void clearPlugins() { ++ this.instanceManager.clearPlugins(); ++ this.permissionManager.clearPermissions(); ++ this.paperEventManager.clearEvents(); ++ } ++ ++ @Override ++ public void enablePlugin(@NotNull Plugin plugin) { ++ this.instanceManager.enablePlugin(plugin); ++ } ++ ++ @Override ++ public void disablePlugin(@NotNull Plugin plugin) { ++ this.instanceManager.disablePlugin(plugin); ++ } ++ ++ @Override ++ public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) { ++ return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig); ++ } ++ ++ @Override ++ public boolean hasDependency(String pluginIdentifier) { ++ return this.instanceManager.hasDependency(pluginIdentifier); ++ } ++ ++ // Event manipulation ++ ++ @Override ++ public void callEvent(@NotNull Event event) throws IllegalStateException { ++ this.paperEventManager.callEvent(event); ++ } ++ ++ @Override ++ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) { ++ this.paperEventManager.registerEvents(listener, plugin); ++ } ++ ++ @Override ++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) { ++ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin); ++ } ++ ++ @Override ++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) { ++ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled); ++ } ++ ++ // Permission manipulation ++ ++ @Override ++ public @Nullable Permission getPermission(@NotNull String name) { ++ return this.permissionManager.getPermission(name); ++ } ++ ++ @Override ++ public void addPermission(@NotNull Permission perm) { ++ this.permissionManager.addPermission(perm); ++ } ++ ++ @Override ++ public void removePermission(@NotNull Permission perm) { ++ this.permissionManager.removePermission(perm); ++ } ++ ++ @Override ++ public void removePermission(@NotNull String name) { ++ this.permissionManager.removePermission(name); ++ } ++ ++ @Override ++ public @NotNull Set<Permission> getDefaultPermissions(boolean op) { ++ return this.permissionManager.getDefaultPermissions(op); ++ } ++ ++ @Override ++ public void recalculatePermissionDefaults(@NotNull Permission perm) { ++ this.permissionManager.recalculatePermissionDefaults(perm); ++ } ++ ++ @Override ++ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) { ++ this.permissionManager.subscribeToPermission(permission, permissible); ++ } ++ ++ @Override ++ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) { ++ this.permissionManager.unsubscribeFromPermission(permission, permissible); ++ } ++ ++ @Override ++ public @NotNull Set<Permissible> getPermissionSubscriptions(@NotNull String permission) { ++ return this.permissionManager.getPermissionSubscriptions(permission); ++ } ++ ++ @Override ++ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) { ++ this.permissionManager.subscribeToDefaultPerms(op, permissible); ++ } ++ ++ @Override ++ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) { ++ this.permissionManager.unsubscribeFromDefaultPerms(op, permissible); ++ } ++ ++ @Override ++ public @NotNull Set<Permissible> getDefaultPermSubscriptions(boolean op) { ++ return this.permissionManager.getDefaultPermSubscriptions(op); ++ } ++ ++ @Override ++ public @NotNull Set<Permission> getPermissions() { ++ return this.permissionManager.getPermissions(); ++ } ++ ++ @Override ++ public void addPermissions(@NotNull List<Permission> perm) { ++ this.permissionManager.addPermissions(perm); ++ } ++ ++ @Override ++ public void clearPermissions() { ++ this.permissionManager.clearPermissions(); ++ } ++ ++ @Override ++ public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) { ++ this.permissionManager = permissionManager; ++ } ++ ++ // Etc ++ ++ @Override ++ public boolean useTimings() { ++ return co.aikar.timings.Timings.isTimingsEnabled(); ++ } ++ ++ @Override ++ public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException { ++ throw new UnsupportedOperationException(); ++ } ++ ++ public MutableGraph<String> getInstanceManagerGraph() { ++ return instanceManager.getDependencyGraph(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5d50d1d312388e979c0e1cd53a6bf5977ca6e549 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java +@@ -0,0 +1,47 @@ ++package io.papermc.paper.plugin.manager; ++ ++import com.destroystokyo.paper.util.SneakyThrow; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.storage.ProviderStorage; ++import org.bukkit.plugin.InvalidPluginException; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Used for loading plugins during runtime, only supporting providers that are plugins. ++ * This is only used for the plugin manager, as it only allows plugins to be ++ * registered to a provider storage. ++ */ ++class RuntimePluginEntrypointHandler<T extends ProviderStorage<JavaPlugin>> implements EntrypointHandler { ++ ++ private final T providerStorage; ++ ++ RuntimePluginEntrypointHandler(T providerStorage) { ++ this.providerStorage = providerStorage; ++ } ++ ++ @Override ++ public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) { ++ if (!entrypoint.equals(Entrypoint.PLUGIN)) { ++ SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint))); ++ // We have to throw an invalid plugin exception for legacy reasons ++ } ++ ++ this.providerStorage.register((PluginProvider<JavaPlugin>) provider); ++ } ++ ++ @Override ++ public void enter(Entrypoint<?> entrypoint) { ++ if (entrypoint != Entrypoint.PLUGIN) { ++ throw new IllegalArgumentException("Only plugin entrypoint supported"); ++ } ++ this.providerStorage.enter(); ++ } ++ ++ @NotNull ++ public T getPluginProviderStorage() { ++ return this.providerStorage; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b0e723bcda9b1fc01e6aa5e53e57c09ea4f1a1c8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java +@@ -0,0 +1,79 @@ ++package io.papermc.paper.plugin.manager; ++ ++import com.destroystokyo.paper.util.SneakyThrow; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage; ++import org.bukkit.plugin.InvalidPluginException; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.UnknownDependencyException; ++import org.bukkit.plugin.java.JavaPlugin; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Optional; ++ ++/** ++ * Used for registering a single plugin provider. ++ * This has special behavior in that some errors are thrown instead of logged. ++ */ ++class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage { ++ ++ private final MetaDependencyTree dependencyTree; ++ private PluginProvider<JavaPlugin> lastProvider; ++ private JavaPlugin singleLoaded; ++ ++ SingularRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) { ++ this.dependencyTree = dependencyTree; ++ } ++ ++ @Override ++ public void register(PluginProvider<JavaPlugin> provider) { ++ super.register(provider); ++ if (this.lastProvider != null) { ++ SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins")); ++ } ++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { ++ throw new IllegalStateException("Cannot register paper plugins during runtime!"); ++ } ++ this.lastProvider = provider; ++ // Register the provider into the server entrypoint, this allows it to show in /plugins correctly. ++ // Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense. ++ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider); ++ } ++ ++ @Override ++ public void enter() { ++ PluginProvider<JavaPlugin> provider = this.lastProvider; ++ if (provider == null) { ++ return; ++ } ++ ++ // Go through normal plugin loading logic ++ super.enter(); ++ } ++ ++ @Override ++ public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) { ++ super.processProvided(provider, provided); ++ this.singleLoaded = provided; ++ } ++ ++ @Override ++ public boolean throwOnCycle() { ++ return false; ++ } ++ ++ public Optional<JavaPlugin> getSingleLoaded() { ++ return Optional.ofNullable(this.singleLoaded); ++ } ++ ++ @Override ++ public MetaDependencyTree createDependencyTree() { ++ return this.dependencyTree; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ea8cf22c35242eb9f3914b95df00e20504aef5c1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java +@@ -0,0 +1,42 @@ ++package io.papermc.paper.plugin.manager; ++ ++import org.bukkit.permissions.Permissible; ++import org.bukkit.permissions.Permission; ++import org.bukkit.plugin.SimplePluginManager; ++ ++import java.util.Map; ++import java.util.Set; ++ ++/* ++This is actually so cursed I hate it. ++We need to wrap these in fields as people override the fields, so we need to access them lazily at all times. ++// TODO: When SimplePluginManager is GONE remove this and cleanup the PaperPermissionManager to use actual fields. ++ */ ++class StupidSPMPermissionManagerWrapper extends PaperPermissionManager { ++ ++ private final SimplePluginManager simplePluginManager; ++ ++ public StupidSPMPermissionManagerWrapper(SimplePluginManager simplePluginManager) { ++ this.simplePluginManager = simplePluginManager; ++ } ++ ++ @Override ++ public Map<String, Permission> permissions() { ++ return this.simplePluginManager.permissions; ++ } ++ ++ @Override ++ public Map<Boolean, Set<Permission>> defaultPerms() { ++ return this.simplePluginManager.defaultPerms; ++ } ++ ++ @Override ++ public Map<String, Map<Permissible, Boolean>> permSubs() { ++ return this.simplePluginManager.permSubs; ++ } ++ ++ @Override ++ public Map<Boolean, Map<Permissible, Boolean>> defSubs() { ++ return this.simplePluginManager.defSubs; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fd9748c693a029c0d75f6ad813ea46a2b528d140 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java +@@ -0,0 +1,56 @@ ++package io.papermc.paper.plugin.provider; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.NotNull; ++ ++import java.nio.file.Path; ++import java.util.List; ++import java.util.Map; ++import java.util.jar.JarFile; ++ ++/** ++ * PluginProviders are created by a {@link io.papermc.paper.plugin.provider.source.ProviderSource}, ++ * which is loaded into an {@link io.papermc.paper.plugin.entrypoint.EntrypointHandler}. ++ * <p> ++ * A PluginProvider is responsible for providing part of a plugin, whether it's a Bootstrapper or Server Plugin. ++ * The point of this class is to be able to create the actual instance later, as at the time this is created the server ++ * may be missing some key parts. For example, the Bukkit singleton will not be initialized yet, therefor we need to ++ * have a PluginServerProvider load the server plugin later. ++ * <p> ++ * Plugin providers are currently not exposed in any way of the api. It is preferred that this stays this way, ++ * as providers are only needed for initialization. ++ * ++ * @param <T> provider type ++ */ ++public interface PluginProvider<T> { ++ ++ @NotNull ++ Path getSource(); ++ ++ default Path getFileName() { ++ return this.getSource().getFileName(); ++ } ++ ++ default Path getParentSource() { ++ return this.getSource().getParent(); ++ } ++ ++ JarFile file(); ++ ++ T createInstance(); ++ ++ PluginMeta getMeta(); ++ ++ ComponentLogger getLogger(); ++ ++ LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad); ++ ++ // Returns a list of missing dependencies ++ List<String> validateDependencies(@NotNull DependencyContext context); ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6154e864b0ff01cb70acaaeee5ca8c9f4a90a90e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java +@@ -0,0 +1,13 @@ ++package io.papermc.paper.plugin.provider; ++ ++import org.jetbrains.annotations.ApiStatus; ++ ++/** ++ * This is used for the /plugins command, where it will look in the {@link io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler} and ++ * use the provider statuses to determine the color. ++ */ ++public enum ProviderStatus { ++ INITIALIZED, ++ ERRORED, ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..17284d0f61c459dff765c0adae4ad2c641e054c1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java +@@ -0,0 +1,11 @@ ++package io.papermc.paper.plugin.provider; ++ ++/** ++ * This is used to mark that a plugin provider is able to hold a status for the /plugins command. ++ */ ++public interface ProviderStatusHolder { ++ ++ ProviderStatus getLastProvidedStatus(); ++ ++ void setStatus(ProviderStatus status); ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6ba3bcc468c0a60c76d6d0f0243bda661c737f2f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java +@@ -0,0 +1,29 @@ ++package io.papermc.paper.plugin.provider.configuration; ++ ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.meta.NodeResolver; ++ ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.AnnotatedElement; ++ ++@Retention(RetentionPolicy.RUNTIME) ++@Target(ElementType.FIELD) ++public @interface FlattenedResolver { ++ ++ final class Factory implements NodeResolver.Factory { ++ ++ @Override ++ public @Nullable NodeResolver make(String name, AnnotatedElement element) { ++ if (element.isAnnotationPresent(FlattenedResolver.class)) { ++ return (node) -> node; ++ } else { ++ return null; ++ } ++ } ++ } ++ ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8cd649c977172f6b757d68565fcbb9eb8ae100a3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java +@@ -0,0 +1,147 @@ ++package io.papermc.paper.plugin.provider.configuration; ++ ++import com.google.gson.reflect.TypeToken; ++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration; ++import io.papermc.paper.plugin.provider.configuration.type.PluginDependencyLifeCycle; ++import org.spongepowered.configurate.CommentedConfigurationNode; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.meta.Required; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++ ++import java.util.EnumMap; ++import java.util.EnumSet; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++ ++class LegacyPaperMeta { ++ ++ ++ private static final TypeToken<Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>>> TYPE_TOKEN = new TypeToken<>() { ++ }; ++ ++ public static void migrate(CommentedConfigurationNode node) throws ConfigurateException { ++ ConfigurationTransformation.chain(notVersioned()).apply(node); ++ } ++ ++ private static ConfigurationTransformation notVersioned() { ++ return ConfigurationTransformation.builder() ++ .addAction(NodePath.path(), (path, value) -> { ++ boolean bootstrapSubSection = value.hasChild("bootstrap"); ++ boolean serverSubSection = value.hasChild("server"); ++ ++ // Ignore if using newer format ++ if (bootstrapSubSection || serverSubSection) { ++ return null; ++ } ++ ++ // First collect all load before elements ++ LegacyConfiguration legacyConfiguration; ++ try { ++ legacyConfiguration = value.require(LegacyConfiguration.class); ++ } catch (SerializationException exception) { ++ // Ignore if not present ++ return null; ++ } ++ ++ Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>> dependencies = new EnumMap<>(PluginDependencyLifeCycle.class); ++ dependencies.put(PluginDependencyLifeCycle.BOOTSTRAP, new HashMap<>()); ++ dependencies.put(PluginDependencyLifeCycle.SERVER, new HashMap<>()); ++ ++ Map<PluginDependencyLifeCycle, Map<String, Set<DependencyFlag>>> dependencyConfigurationMap = new HashMap<>(); ++ dependencyConfigurationMap.put(PluginDependencyLifeCycle.BOOTSTRAP, new HashMap<>()); ++ dependencyConfigurationMap.put(PluginDependencyLifeCycle.SERVER, new HashMap<>()); ++ ++ // Migrate loadafter ++ for (LegacyLoadConfiguration legacyConfig : legacyConfiguration.loadAfter) { ++ Set<DependencyFlag> dependencyFlags = dependencyConfigurationMap ++ .get(legacyConfig.bootstrap ? PluginDependencyLifeCycle.BOOTSTRAP : PluginDependencyLifeCycle.SERVER) ++ .computeIfAbsent(legacyConfig.name, s -> EnumSet.noneOf(DependencyFlag.class)); ++ ++ dependencyFlags.add(DependencyFlag.LOAD_AFTER); ++ } ++ ++ // Migrate loadbefore ++ for (LegacyLoadConfiguration legacyConfig : legacyConfiguration.loadBefore) { ++ Set<DependencyFlag> dependencyFlags = dependencyConfigurationMap ++ .get(legacyConfig.bootstrap ? PluginDependencyLifeCycle.BOOTSTRAP : PluginDependencyLifeCycle.SERVER) ++ .computeIfAbsent(legacyConfig.name, s -> EnumSet.noneOf(DependencyFlag.class)); ++ ++ dependencyFlags.add(DependencyFlag.LOAD_BEFORE); ++ } ++ ++ // Migrate dependencies ++ for (LegacyDependencyConfiguration legacyConfig : legacyConfiguration.dependencies) { ++ Set<DependencyFlag> dependencyFlags = dependencyConfigurationMap ++ .get(legacyConfig.bootstrap ? PluginDependencyLifeCycle.BOOTSTRAP : PluginDependencyLifeCycle.SERVER) ++ .computeIfAbsent(legacyConfig.name, s -> EnumSet.noneOf(DependencyFlag.class)); ++ ++ dependencyFlags.add(DependencyFlag.DEPENDENCY); ++ if (legacyConfig.required) { ++ dependencyFlags.add(DependencyFlag.REQUIRED); ++ } ++ } ++ for (Map.Entry<PluginDependencyLifeCycle, Map<String, Set<DependencyFlag>>> legacyTypes : dependencyConfigurationMap.entrySet()) { ++ Map<String, DependencyConfiguration> flagMap = dependencies.get(legacyTypes.getKey()); ++ for (Map.Entry<String, Set<DependencyFlag>> entry : legacyTypes.getValue().entrySet()) { ++ Set<DependencyFlag> flags = entry.getValue(); ++ ++ ++ DependencyConfiguration.LoadOrder loadOrder = DependencyConfiguration.LoadOrder.OMIT; ++ // These meanings are now swapped ++ if (flags.contains(DependencyFlag.LOAD_BEFORE)) { ++ loadOrder = DependencyConfiguration.LoadOrder.AFTER; ++ } else if (flags.contains(DependencyFlag.LOAD_AFTER)) { ++ loadOrder = DependencyConfiguration.LoadOrder.BEFORE; ++ } ++ ++ flagMap.put(entry.getKey(), new DependencyConfiguration( ++ loadOrder, ++ flags.contains(DependencyFlag.REQUIRED), ++ flags.contains(DependencyFlag.DEPENDENCY) ++ )); ++ } ++ } ++ ++ value.node("dependencies").set(TYPE_TOKEN.getType(), dependencies); ++ return null; ++ }) ++ .build(); ++ } ++ ++ @ConfigSerializable ++ record LegacyLoadConfiguration( ++ @Required String name, ++ boolean bootstrap ++ ) { ++ } ++ ++ @ConfigSerializable ++ private static class LegacyConfiguration { ++ ++ private List<LegacyLoadConfiguration> loadAfter = List.of(); ++ private List<LegacyLoadConfiguration> loadBefore = List.of(); ++ private List<LegacyDependencyConfiguration> dependencies = List.of(); ++ } ++ ++ ++ @ConfigSerializable ++ public record LegacyDependencyConfiguration( ++ @Required String name, ++ boolean required, ++ boolean bootstrap ++ ) { ++ } ++ ++ enum DependencyFlag { ++ LOAD_AFTER, ++ LOAD_BEFORE, ++ REQUIRED, ++ DEPENDENCY ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/LoadOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/LoadOrderConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e3430f535e8e9c3b8b44bf2daece8c47e8b14db7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/LoadOrderConfiguration.java +@@ -0,0 +1,38 @@ ++package io.papermc.paper.plugin.provider.configuration; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.List; ++ ++/** ++ * This is used for plugins to configure the load order of strategies. ++ */ ++public interface LoadOrderConfiguration { ++ ++ /** ++ * Provides a list of plugins that THIS configuration should load ++ * before. ++ * ++ * @return list of plugins ++ */ ++ @NotNull ++ List<String> getLoadBefore(); ++ ++ /** ++ * Provides a list of plugins that THIS configuration should load ++ * before. ++ * ++ * @return list of plugins ++ */ ++ @NotNull ++ List<String> getLoadAfter(); ++ ++ /** ++ * Gets the responsible plugin provider's meta. ++ * ++ * @return meta ++ */ ++ @NotNull ++ PluginMeta getMeta(); ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c685871155c8dff1d57ff151d7a5ec70350e5390 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java +@@ -0,0 +1,271 @@ ++package io.papermc.paper.plugin.provider.configuration; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.collect.ImmutableList; ++import io.papermc.paper.configuration.constraint.Constraint; ++import io.papermc.paper.configuration.serializer.ComponentSerializer; ++import io.papermc.paper.configuration.serializer.EnumValueSerializer; ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer; ++import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; ++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration; ++import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration; ++import io.papermc.paper.plugin.provider.configuration.type.PluginDependencyLifeCycle; ++import java.lang.reflect.Type; ++import java.util.function.Predicate; ++import org.bukkit.craftbukkit.util.ApiVersion; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.bukkit.plugin.PluginLoadOrder; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import org.jetbrains.annotations.TestOnly; ++import org.spongepowered.configurate.CommentedConfigurationNode; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.loader.HeaderMode; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.ObjectMapper; ++import org.spongepowered.configurate.objectmapping.meta.Required; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.yaml.NodeStyle; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++import java.io.BufferedReader; ++import java.util.EnumMap; ++import java.util.List; ++import java.util.Map; ++ ++@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) ++@ConfigSerializable ++public class PaperPluginMeta implements PluginMeta { ++ ++ @PluginConfigConstraints.PluginName ++ @Required ++ private String name; ++ @Required ++ @PluginConfigConstraints.PluginNameSpace ++ private String main; ++ @PluginConfigConstraints.PluginNameSpace ++ private String bootstrapper; ++ @PluginConfigConstraints.PluginNameSpace ++ private String loader; ++ private List<String> provides = List.of(); ++ private boolean hasOpenClassloader = false; ++ @Required ++ private String version; ++ private String description; ++ private List<String> authors = List.of(); ++ private List<String> contributors = List.of(); ++ private String website; ++ private String prefix; ++ private PluginLoadOrder load = PluginLoadOrder.POSTWORLD; ++ @FlattenedResolver ++ private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of()); ++ @Required ++ private ApiVersion apiVersion; ++ ++ private Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>> dependencies = new EnumMap<>(PluginDependencyLifeCycle.class); ++ ++ public PaperPluginMeta() { ++ } ++ ++ static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.19"); ++ public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException { ++ YamlConfigurationLoader loader = YamlConfigurationLoader.builder() ++ .indent(2) ++ .nodeStyle(NodeStyle.BLOCK) ++ .headerMode(HeaderMode.NONE) ++ .source(() -> reader) ++ .defaultOptions((options) -> { ++ ++ return options.serializers((serializers) -> { ++ serializers ++ .register(new ScalarSerializer<>(ApiVersion.class) { ++ @Override ++ public ApiVersion deserialize(final Type type, final Object obj) throws SerializationException { ++ try { ++ final ApiVersion version = ApiVersion.getOrCreateVersion(obj.toString()); ++ if (version.isOlderThan(MINIMUM)) { ++ throw new SerializationException(version + " is too old for a paper plugin!"); ++ } ++ return version; ++ } catch (final IllegalArgumentException e) { ++ throw new SerializationException(e); ++ } ++ } ++ ++ @Override ++ protected Object serialize(final ApiVersion item, final Predicate<Class<?>> typeSupported) { ++ return item.getVersionString(); ++ } ++ }) ++ .register(new EnumValueSerializer()) ++ .register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER) ++ .register(new ComponentSerializer()) ++ .registerAnnotatedObjects( ++ ObjectMapper.factoryBuilder() ++ .addConstraint(Constraint.class, new Constraint.Factory()) ++ .addConstraint(PluginConfigConstraints.PluginName.class, String.class, new PluginConfigConstraints.PluginName.Factory()) ++ .addConstraint(PluginConfigConstraints.PluginNameSpace.class, String.class, new PluginConfigConstraints.PluginNameSpace.Factory()) ++ .addNodeResolver(new FlattenedResolver.Factory()) ++ .build() ++ ); ++ ++ }); ++ }) ++ .build(); ++ CommentedConfigurationNode node = loader.load(); ++ LegacyPaperMeta.migrate(node); ++ PaperPluginMeta pluginConfiguration = node.require(PaperPluginMeta.class); ++ ++ if (!node.node("author").virtual()) { ++ pluginConfiguration.authors = ImmutableList.<String>builder() ++ .addAll(pluginConfiguration.authors) ++ .add(node.node("author").getString()) ++ .build(); ++ } ++ ++ return pluginConfiguration; ++ } ++ ++ @Override ++ public @NotNull String getName() { ++ return this.name; ++ } ++ ++ @TestOnly ++ public void setName(@NotNull String name) { ++ Preconditions.checkNotNull(name, "name"); ++ this.name = name; ++ } ++ ++ @Override ++ public @NotNull String getMainClass() { ++ return this.main; ++ } ++ ++ @Override ++ public @NotNull String getVersion() { ++ return this.version; ++ } ++ ++ @TestOnly ++ public void setVersion(@NotNull String version) { ++ Preconditions.checkNotNull(version, "version"); ++ this.version = version; ++ } ++ ++ @Override ++ public @Nullable String getLoggerPrefix() { ++ return this.prefix; ++ } ++ ++ @Override ++ public @NotNull List<String> getPluginDependencies() { ++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of()) ++ .entrySet() ++ .stream() ++ .filter((entry) -> entry.getValue().required() && entry.getValue().joinClasspath()) ++ .map(Map.Entry::getKey) ++ .toList(); ++ } ++ ++ @Override ++ public @NotNull List<String> getPluginSoftDependencies() { ++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of()) ++ .entrySet() ++ .stream() ++ .filter((entry) -> !entry.getValue().required() && entry.getValue().joinClasspath()) ++ .map(Map.Entry::getKey) ++ .toList(); ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadBeforePlugins() { ++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of()) ++ .entrySet() ++ .stream() ++ // This plugin will load BEFORE all dependencies (so dependencies will load AFTER plugin) ++ .filter((entry) -> entry.getValue().load() == DependencyConfiguration.LoadOrder.AFTER) ++ .map(Map.Entry::getKey) ++ .toList(); ++ } ++ ++ public @NotNull List<String> getLoadAfterPlugins() { ++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of()) ++ .entrySet() ++ .stream() ++ // This plugin will load AFTER all dependencies (so dependencies will load BEFORE plugin) ++ .filter((entry) -> entry.getValue().load() == DependencyConfiguration.LoadOrder.BEFORE) ++ .map(Map.Entry::getKey) ++ .toList(); ++ } ++ ++ ++ public Map<String, DependencyConfiguration> getServerDependencies() { ++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of()); ++ } ++ ++ public Map<String, DependencyConfiguration> getBoostrapDependencies() { ++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.BOOTSTRAP, Map.of()); ++ } ++ ++ @Override ++ public @NotNull PluginLoadOrder getLoadOrder() { ++ return this.load; ++ } ++ ++ @Override ++ public @NotNull String getDescription() { ++ return this.description; ++ } ++ ++ @Override ++ public @NotNull List<String> getAuthors() { ++ return this.authors; ++ } ++ ++ @Override ++ public @NotNull List<String> getContributors() { ++ return this.contributors; ++ } ++ ++ @Override ++ public String getWebsite() { ++ return this.website; ++ } ++ ++ @Override ++ public @NotNull List<Permission> getPermissions() { ++ return this.permissionConfiguration.permissions(); ++ } ++ ++ @Override ++ public @NotNull PermissionDefault getPermissionDefault() { ++ return this.permissionConfiguration.defaultPerm(); ++ } ++ ++ @Override ++ public @NotNull String getAPIVersion() { ++ return this.apiVersion.getVersionString(); ++ } ++ ++ @Override ++ public @NotNull List<String> getProvidedPlugins() { ++ return this.provides; ++ } ++ ++ public String getBootstrapper() { ++ return this.bootstrapper; ++ } ++ ++ public String getLoader() { ++ return this.loader; ++ } ++ ++ public boolean hasOpenClassloader() { ++ return this.hasOpenClassloader; ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f0cdb1bab30faaa438aa3e6de6125ade3fae98c2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java +@@ -0,0 +1,90 @@ ++package io.papermc.paper.plugin.provider.configuration.serializer; ++ ++import com.google.common.collect.ImmutableCollection; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.serialize.TypeSerializer; ++import org.spongepowered.configurate.util.CheckedConsumer; ++ ++import java.lang.reflect.Type; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.List; ++ ++@SuppressWarnings("unchecked") ++public abstract class ImmutableCollectionSerializer<B extends ImmutableCollection.Builder<?>, T extends Collection<?>> implements TypeSerializer<T> { ++ ++ protected ImmutableCollectionSerializer() { ++ } ++ ++ @Override ++ public final T deserialize(final Type type, final ConfigurationNode node) throws SerializationException { ++ final Type entryType = this.elementType(type); ++ final @Nullable TypeSerializer<?> entrySerial = node.options().serializers().get(entryType); ++ if (entrySerial == null) { ++ throw new SerializationException(node, entryType, "No applicable type serializer for type"); ++ } ++ ++ if (node.isList()) { ++ final List<? extends ConfigurationNode> values = node.childrenList(); ++ final B builder = this.createNew(values.size()); ++ for (ConfigurationNode value : values) { ++ try { ++ this.deserializeSingle(builder, entrySerial.deserialize(entryType, value)); ++ } catch (final SerializationException ex) { ++ ex.initPath(value::path); ++ throw ex; ++ } ++ } ++ return (T) builder.build(); ++ } else { ++ final @Nullable Object unwrappedVal = node.raw(); ++ if (unwrappedVal != null) { ++ final B builder = this.createNew(1); ++ this.deserializeSingle(builder, entrySerial.deserialize(entryType, node)); ++ return (T) builder.build(); ++ } ++ } ++ return this.emptyValue(type, null); ++ } ++ ++ @SuppressWarnings({"unchecked", "rawtypes"}) ++ @Override ++ public final void serialize(final Type type, final @Nullable T obj, final ConfigurationNode node) throws SerializationException { ++ final Type entryType = this.elementType(type); ++ final @Nullable TypeSerializer entrySerial = node.options().serializers().get(entryType); ++ if (entrySerial == null) { ++ throw new SerializationException(node, entryType, "No applicable type serializer for type"); ++ } ++ ++ node.raw(Collections.emptyList()); ++ if (obj != null) { ++ this.forEachElement(obj, el -> { ++ final ConfigurationNode child = node.appendListNode(); ++ try { ++ entrySerial.serialize(entryType, el, child); ++ } catch (final SerializationException ex) { ++ ex.initPath(child::path); ++ throw ex; ++ } ++ }); ++ } ++ } ++ ++ @SuppressWarnings({"unchecked"}) ++ @Override ++ public @Nullable T emptyValue(final Type specificType, final ConfigurationOptions options) { ++ return (T) this.createNew(0).build(); ++ } ++ ++ protected abstract Type elementType(Type containerType) throws SerializationException; ++ ++ protected abstract B createNew(int size); ++ ++ protected abstract void forEachElement(T collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException; ++ ++ protected abstract void deserializeSingle(B builder, @Nullable Object deserialized) throws SerializationException; ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7757d7df70e39a6fe4d92d02b6f905a22f80dbf3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.plugin.provider.configuration.serializer; ++ ++import com.google.common.collect.ImmutableList; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.CheckedConsumer; ++ ++import java.lang.reflect.ParameterizedType; ++import java.lang.reflect.Type; ++import java.util.List; ++ ++public class ImmutableListSerializer extends ImmutableCollectionSerializer<ImmutableList.Builder<?>, List<?>> { ++ ++ @Override ++ protected Type elementType(Type containerType) throws SerializationException { ++ if (!(containerType instanceof ParameterizedType)) { ++ throw new SerializationException(containerType, "Raw types are not supported for collections"); ++ } ++ return ((ParameterizedType) containerType).getActualTypeArguments()[0]; ++ } ++ ++ @Override ++ protected ImmutableList.Builder<?> createNew(int size) { ++ return ImmutableList.builderWithExpectedSize(size); ++ } ++ ++ @Override ++ protected void forEachElement(List<?> collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException { ++ for (Object obj : collection) { ++ action.accept(obj); ++ } ++ } ++ ++ @SuppressWarnings({"unchecked", "rawtypes"}) ++ @Override ++ protected void deserializeSingle(ImmutableList.Builder<?> builder, @Nullable Object deserialized) throws SerializationException { ++ if (deserialized == null) { ++ return; ++ } ++ ++ ((ImmutableList.Builder) builder).add(deserialized); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f951f4024745503e9cdfa7ff17b9313ac6d7b4c4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java +@@ -0,0 +1,56 @@ ++package io.papermc.paper.plugin.provider.configuration.serializer; ++ ++import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.serialize.TypeSerializer; ++ ++import java.lang.reflect.Type; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++ ++public class PermissionConfigurationSerializer { ++ ++ public static final Serializer SERIALIZER = new Serializer(); ++ ++ private static final class Serializer implements TypeSerializer<PermissionConfiguration> { ++ private Serializer() { ++ super(); ++ } ++ ++ @Override ++ public PermissionConfiguration deserialize(Type type, ConfigurationNode node) throws SerializationException { ++ Map<?, ?> map = (Map<?, ?>) node.node("permissions").raw(); ++ ++ PermissionDefault permissionDefault; ++ ConfigurationNode permNode = node.node("defaultPerm"); ++ if (permNode.virtual()) { ++ permissionDefault = PermissionDefault.OP; ++ } else { ++ permissionDefault = PermissionDefault.getByName(permNode.getString()); ++ } ++ ++ List<Permission> result = new ArrayList<>(); ++ if (map != null) { ++ for (Map.Entry<?, ?> entry : map.entrySet()) { ++ try { ++ result.add(Permission.loadPermission(entry.getKey().toString(), (Map<?, ?>) entry.getValue(), permissionDefault, result)); ++ } catch (Throwable ex) { ++ throw new SerializationException((Type) null, "Error loading permission %s".formatted(entry.getKey()), ex); ++ } ++ } ++ } ++ ++ return new PermissionConfiguration(permissionDefault, List.copyOf(result)); ++ } ++ ++ @Override ++ public void serialize(Type type, @org.checkerframework.checker.nullness.qual.Nullable PermissionConfiguration obj, ConfigurationNode node) throws SerializationException { ++ ++ } ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3043a4216ec13c3de0cb931f11492ded1e6dc8de +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java +@@ -0,0 +1,67 @@ ++package io.papermc.paper.plugin.provider.configuration.serializer.constraints; ++ ++import io.papermc.paper.plugin.util.NamespaceChecker; ++import org.spongepowered.configurate.objectmapping.meta.Constraint; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.Type; ++import java.util.Locale; ++import java.util.Set; ++import java.util.regex.Pattern; ++ ++public final class PluginConfigConstraints { ++ ++ public static final Set<String> RESERVED_KEYS = Set.of("bukkit", "minecraft", "mojang", "spigot", "paper"); ++ ++ @Documented ++ @Retention(RetentionPolicy.RUNTIME) ++ @Target(ElementType.FIELD) ++ public @interface PluginName { ++ ++ final class Factory implements Constraint.Factory<PluginName, String> { ++ ++ private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z\\d _.-]+$"); ++ ++ @Override ++ public Constraint<String> make(PluginName data, Type type) { ++ return value -> { ++ if (value != null) { ++ if (RESERVED_KEYS.contains(value.toLowerCase(Locale.ROOT))) { ++ throw new SerializationException("Restricted name, cannot use '%s' as a plugin name.".formatted(data)); ++ } else if (value.indexOf(' ') != -1) { ++ // For legacy reasons, the space condition has a separate exception message. ++ throw new SerializationException("Restricted name, cannot use 0x20 (space character) in a plugin name."); ++ } ++ ++ if (!VALID_NAME.matcher(value).matches()) { ++ throw new SerializationException("name '" + value + "' contains invalid characters."); ++ } ++ } ++ }; ++ } ++ } ++ } ++ ++ @Documented ++ @Retention(RetentionPolicy.RUNTIME) ++ @Target(ElementType.FIELD) ++ public @interface PluginNameSpace { ++ ++ final class Factory implements Constraint.Factory<PluginNameSpace, String> { ++ ++ @Override ++ public Constraint<String> make(PluginNameSpace data, Type type) { ++ return value -> { ++ if (value != null && !NamespaceChecker.isValidNameSpace(value)) { ++ throw new SerializationException("provided class '%s' is in an invalid namespace.".formatted(value)); ++ } ++ }; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..957fb5e60ba6bd8760e8f6016d7bb6e8a405e163 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.plugin.provider.configuration.type; ++ ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++ ++import static java.util.Objects.requireNonNullElse; ++ ++@DefaultQualifier(NonNull.class) ++@ConfigSerializable ++public record DependencyConfiguration( ++ LoadOrder load, ++ Boolean required, ++ Boolean joinClasspath ++) { ++ @SuppressWarnings("DataFlowIssue") // incorrect intellij inspections ++ public DependencyConfiguration { ++ required = requireNonNullElse(required, true); ++ joinClasspath = requireNonNullElse(joinClasspath, true); ++ } ++ ++ @ConfigSerializable ++ public enum LoadOrder { ++ // dependency will now load BEFORE your plugin ++ BEFORE, ++ // the dependency will now load AFTER your plugin ++ AFTER, ++ OMIT ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/LoadConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/LoadConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4184e4232c59f15ef8bbc98f82f501fc524f37c7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/LoadConfiguration.java +@@ -0,0 +1,11 @@ ++package io.papermc.paper.plugin.provider.configuration.type; ++ ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.meta.Required; ++ ++@ConfigSerializable ++public record LoadConfiguration( ++ @Required String name, ++ boolean bootstrap ++) { ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a180612a1ec395202dbae1ca5b97ec01382097e4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java +@@ -0,0 +1,14 @@ ++package io.papermc.paper.plugin.provider.configuration.type; ++ ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++ ++import java.util.List; ++ ++// Record components used for deserialization!!!! ++@ConfigSerializable ++public record PermissionConfiguration( ++ PermissionDefault defaultPerm, ++ List<Permission> permissions) { ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PluginDependencyLifeCycle.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PluginDependencyLifeCycle.java +new file mode 100644 +index 0000000000000000000000000000000000000000..49a087381307eab263f7dad43aaa25980db33cc2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PluginDependencyLifeCycle.java +@@ -0,0 +1,6 @@ ++package io.papermc.paper.plugin.provider.configuration.type; ++ ++public enum PluginDependencyLifeCycle { ++ BOOTSTRAP, ++ SERVER ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..226f457db6c1461c943c157b2b91e7450abc9dc6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java +@@ -0,0 +1,66 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import java.io.IOException; ++import java.nio.file.FileVisitOption; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Consumer; ++import org.slf4j.Logger; ++ ++/** ++ * Loads all plugin providers in the given directory. ++ */ ++public class DirectoryProviderSource implements ProviderSource<Path, List<Path>> { ++ ++ public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource(); ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("Directory '%s'"::formatted); ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ @Override ++ public List<Path> prepareContext(Path context) throws IOException { ++ // Symlink happy, create file if missing. ++ if (!Files.isDirectory(context)) { ++ Files.createDirectories(context); ++ } ++ ++ final List<Path> files = new ArrayList<>(); ++ this.walkFiles(context, path -> { ++ try { ++ files.add(FILE_PROVIDER_SOURCE.prepareContext(path)); ++ } catch (IllegalArgumentException ignored) { ++ // Ignore illegal argument exceptions from jar checking ++ } catch (final Exception e) { ++ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e); ++ } ++ }); ++ return files; ++ } ++ ++ @Override ++ public void registerProviders(EntrypointHandler entrypointHandler, List<Path> context) { ++ for (Path path : context) { ++ try { ++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, path); ++ } catch (IllegalArgumentException ignored) { ++ // Ignore illegal argument exceptions from jar checking ++ } catch (Exception e) { ++ LOGGER.error("Error loading plugin: " + e.getMessage(), e); ++ } ++ } ++ } ++ ++ private void walkFiles(Path context, Consumer<Path> consumer) throws IOException { ++ Files.walk(context, 1, FileVisitOption.FOLLOW_LINKS) ++ .filter(this::isValidFile) ++ .forEach(consumer); ++ } ++ ++ public boolean isValidFile(Path path) { ++ // Avoid loading plugins that start with a dot ++ return Files.isRegularFile(path) && !path.startsWith("."); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileArrayProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileArrayProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b2ec31fdb2ba8f18c29f2014c03c96a15ec995ad +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileArrayProviderSource.java +@@ -0,0 +1,44 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import java.io.File; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import org.slf4j.Logger; ++ ++public class FileArrayProviderSource implements ProviderSource<File[], List<Path>> { ++ ++ public static final FileArrayProviderSource INSTANCE = new FileArrayProviderSource(); ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted); ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ @Override ++ public List<Path> prepareContext(File[] context) { ++ final List<Path> files = new ArrayList<>(); ++ for (File file : context) { ++ try { ++ files.add(FILE_PROVIDER_SOURCE.prepareContext(file.toPath())); ++ } catch (IllegalArgumentException ignored) { ++ // Ignore illegal argument exceptions from jar checking ++ } catch (final Exception e) { ++ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e); ++ } ++ } ++ return files; ++ } ++ ++ @Override ++ public void registerProviders(EntrypointHandler entrypointHandler, List<Path> context) { ++ for (Path path : context) { ++ try { ++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, path); ++ } catch (IllegalArgumentException ignored) { ++ // Ignore illegal argument exceptions from jar checking ++ } catch (Exception e) { ++ LOGGER.error("Error loading plugin: " + e.getMessage(), e); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5b58df8df7efca0f67e3a14dd71051dfd7a26079 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +@@ -0,0 +1,171 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import io.papermc.paper.plugin.PluginInitializerManager; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import io.papermc.paper.plugin.provider.type.PluginFileType; ++import org.bukkit.plugin.InvalidPluginException; ++import org.jetbrains.annotations.Nullable; ++ ++import java.io.File; ++import java.io.IOException; ++import java.nio.file.FileVisitResult; ++import java.nio.file.FileVisitor; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.nio.file.attribute.BasicFileAttributes; ++import java.util.Set; ++import java.util.function.Function; ++import java.util.jar.JarFile; ++ ++/** ++ * Loads a plugin provider at the given plugin jar file path. ++ */ ++public class FileProviderSource implements ProviderSource<Path, Path> { ++ ++ private final Function<Path, String> contextChecker; ++ ++ public FileProviderSource(Function<Path, String> contextChecker) { ++ this.contextChecker = contextChecker; ++ } ++ ++ @Override ++ public Path prepareContext(Path context) throws IOException { ++ String source = this.contextChecker.apply(context); ++ ++ if (Files.notExists(context)) { ++ throw new IllegalArgumentException(source + " does not exist, cannot load a plugin from it!"); ++ } ++ ++ if (!Files.isRegularFile(context)) { ++ throw new IllegalArgumentException(source + " is not a file, cannot load a plugin from it!"); ++ } ++ ++ if (!context.getFileName().toString().endsWith(".jar")) { ++ throw new IllegalArgumentException(source + " is not a jar file, cannot load a plugin from it!"); ++ } ++ ++ try { ++ context = this.checkUpdate(context); ++ } catch (Exception exception) { ++ throw new RuntimeException(source + " failed to update!", exception); ++ } ++ return context; ++ } ++ ++ @Override ++ public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception { ++ String source = this.contextChecker.apply(context); ++ ++ JarFile file = new JarFile(context.toFile(), true, JarFile.OPEN_READ, JarFile.runtimeVersion()); ++ PluginFileType<?, ?> type = PluginFileType.guessType(file); ++ if (type == null) { ++ // Throw IAE wrapped in RE to prevent callers from considering this a "invalid parameter" as caller ignores IAE. ++ // TODO: This needs some heavy rework, using illegal argument exception to signal an actual failure is less than ideal. ++ if (file.getEntry("META-INF/versions.list") != null) { ++ throw new RuntimeException(new IllegalArgumentException(context + " appears to be a server jar! Server jars do not belong in the plugin folder.")); ++ } ++ ++ throw new RuntimeException( ++ new IllegalArgumentException(source + " does not contain a " + String.join(" or ", PluginFileType.getConfigTypes()) + "! Could not determine plugin type, cannot load a plugin from it!") ++ ); ++ } ++ ++ type.register(entrypointHandler, file, context); ++ } ++ ++ /** ++ * Replaces a plugin with a plugin of the same plugin name in the update folder. ++ * ++ * @param file ++ */ ++ private Path checkUpdate(Path file) throws InvalidPluginException { ++ PluginInitializerManager pluginSystem = PluginInitializerManager.instance(); ++ Path updateDirectory = pluginSystem.pluginUpdatePath(); ++ if (updateDirectory == null || !Files.isDirectory(updateDirectory)) { ++ return file; ++ } ++ ++ try { ++ String pluginName = this.getPluginName(file); ++ UpdateFileVisitor visitor = new UpdateFileVisitor(pluginName); ++ Files.walkFileTree(updateDirectory, Set.of(), 1, visitor); ++ if (visitor.getValidPlugin() != null) { ++ Path updateLocation = visitor.getValidPlugin(); ++ ++ try { ++ Files.copy(updateLocation, file, StandardCopyOption.REPLACE_EXISTING); ++ } catch (IOException exception) { ++ throw new RuntimeException("Could not copy '" + updateLocation + "' to '" + file + "' in update plugin process", exception); ++ } ++ ++ // Idk what this is about, TODO ++ File newName = new File(file.toFile().getParentFile(), updateLocation.toFile().getName()); ++ file.toFile().renameTo(newName); ++ updateLocation.toFile().delete(); ++ return newName.toPath(); ++ } ++ } catch (Exception e) { ++ throw new InvalidPluginException(e); ++ } ++ return file; ++ } ++ ++ private String getPluginName(Path path) throws Exception { ++ try (JarFile file = new JarFile(path.toFile())) { ++ PluginFileType<?, ?> type = PluginFileType.guessType(file); ++ if (type == null) { ++ throw new IllegalArgumentException(path + " does not contain a " + String.join(" or ", PluginFileType.getConfigTypes()) + "! Could not determine plugin type, cannot load a plugin from it!"); ++ } ++ ++ return type.getConfig(file).getName(); ++ } ++ } ++ ++ private class UpdateFileVisitor implements FileVisitor<Path> { ++ ++ private final String targetName; ++ @Nullable ++ private Path validPlugin; ++ ++ private UpdateFileVisitor(String targetName) { ++ this.targetName = targetName; ++ } ++ ++ @Override ++ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { ++ return FileVisitResult.CONTINUE; ++ } ++ ++ @Override ++ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { ++ try { ++ String updatePluginName = FileProviderSource.this.getPluginName(file); ++ if (this.targetName.equals(updatePluginName)) { ++ this.validPlugin = file; ++ return FileVisitResult.TERMINATE; ++ } ++ } catch (Exception e) { ++ // We failed to load this data for some reason, so, we'll skip over this ++ } ++ ++ ++ return FileVisitResult.CONTINUE; ++ } ++ ++ @Override ++ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { ++ return FileVisitResult.CONTINUE; ++ } ++ ++ @Override ++ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { ++ return FileVisitResult.CONTINUE; ++ } ++ ++ @Nullable ++ public Path getValidPlugin() { ++ return validPlugin; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ac55ae0e30119556f01e2e36c20fc63a111fae5f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import org.slf4j.Logger; ++ ++import java.util.List; ++ ++/** ++ * Registers providers at the provided files in the add-plugin argument. ++ */ ++public class PluginFlagProviderSource implements ProviderSource<List<Path>, List<Path>> { ++ ++ public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource(); ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted); ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ @Override ++ public List<Path> prepareContext(List<Path> context) { ++ final List<Path> files = new ArrayList<>(); ++ for (Path path : context) { ++ try { ++ files.add(FILE_PROVIDER_SOURCE.prepareContext(path)); ++ } catch (Exception e) { ++ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e); ++ } ++ } ++ return files; ++ } ++ ++ @Override ++ public void registerProviders(EntrypointHandler entrypointHandler, List<Path> context) { ++ for (Path path : context) { ++ try { ++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, path); ++ } catch (Exception e) { ++ LOGGER.error("Error loading plugin: " + e.getMessage(), e); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6b09813c75fad02fe9b8deb0bf86ad0b148fa770 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import java.io.IOException; ++ ++/** ++ * A provider source is responsible for giving PluginTypes an EntrypointHandler for ++ * registering providers at. ++ * ++ * @param <I> input context ++ * @param <C> context ++ */ ++public interface ProviderSource<I, C> { ++ ++ /** ++ * Prepares the context for use in {@link #registerProviders(EntrypointHandler, Object)}. ++ * ++ * @param context the context to prepare ++ * @return the prepared context, ready for use in {@link #registerProviders(EntrypointHandler, Object)} ++ * @throws IOException if an error occurs while preparing the context ++ */ ++ C prepareContext(I context) throws IOException; ++ ++ /** ++ * Uses the prepared context to register providers at the given entrypoint handler. ++ * ++ * @param entrypointHandler the entrypoint handler to register providers at ++ * @param context the context to register providers at ++ * @throws Exception if an error occurs while registering providers ++ */ ++ void registerProviders(EntrypointHandler entrypointHandler, C context) throws Exception; ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..87128685015d550440a798028f50be24bc755f6c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java +@@ -0,0 +1,85 @@ ++package io.papermc.paper.plugin.provider.type; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.jetbrains.annotations.Nullable; ++ ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.jar.JarEntry; ++import java.util.jar.JarFile; ++ ++/** ++ * This is where spigot/paper plugins are registered. ++ * This will get the jar and find a certain config file, create an object ++ * then registering it into a {@link EntrypointHandler} at a certain {@link Entrypoint}. ++ */ ++public abstract class PluginFileType<T, C extends PluginMeta> { ++ ++ private static final List<String> CONFIG_TYPES = new ArrayList<>(); ++ ++ public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) { ++ @Override ++ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) { ++ PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null; ++ if (parent.shouldCreateBootstrap()) { ++ bootstrapPluginProvider = parent.createBootstrapProvider(); ++ entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider); ++ } ++ ++ entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider)); ++ } ++ }; ++ public static final PluginFileType<SpigotPluginProvider, PluginDescriptionFile> SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) { ++ @Override ++ protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) { ++ entrypointHandler.register(Entrypoint.PLUGIN, provider); ++ } ++ }; ++ ++ private static final List<PluginFileType<?, ?>> VALUES = List.of(PAPER, SPIGOT); ++ ++ private final String config; ++ private final PluginTypeFactory<T, C> factory; ++ ++ PluginFileType(String config, PluginTypeFactory<T, C> factory) { ++ this.config = config; ++ this.factory = factory; ++ CONFIG_TYPES.add(config); ++ } ++ ++ @Nullable ++ public static PluginFileType<?, ?> guessType(JarFile file) { ++ for (PluginFileType<?, ?> type : VALUES) { ++ JarEntry entry = file.getJarEntry(type.config); ++ if (entry != null) { ++ return type; ++ } ++ } ++ ++ return null; ++ } ++ ++ public T register(EntrypointHandler entrypointHandler, JarFile file, Path context) throws Exception { ++ C config = this.getConfig(file); ++ T provider = this.factory.build(file, config, context); ++ this.register(entrypointHandler, provider); ++ return provider; ++ } ++ ++ public C getConfig(JarFile file) throws Exception { ++ return this.factory.create(file, file.getJarEntry(this.config)); ++ } ++ ++ protected abstract void register(EntrypointHandler entrypointHandler, T provider); ++ ++ public static List<String> getConfigTypes() { ++ return CONFIG_TYPES; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java +new file mode 100644 +index 0000000000000000000000000000000000000000..32f230d66f6953520b59ccbf3079c5a6242ca92c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java +@@ -0,0 +1,21 @@ ++package io.papermc.paper.plugin.provider.type; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++ ++import java.nio.file.Path; ++import java.util.jar.JarEntry; ++import java.util.jar.JarFile; ++ ++/** ++ * A plugin type factory is responsible for building an object ++ * and config for a certain plugin type. ++ * ++ * @param <T> plugin provider type (may not be a plugin provider) ++ * @param <C> config type ++ */ ++public interface PluginTypeFactory<T, C extends PluginMeta> { ++ ++ T build(JarFile file, C configuration, Path source) throws Exception; ++ ++ C create(JarFile file, JarEntry config) throws Exception; ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperBootstrapOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperBootstrapOrderConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e34656fb0573ff6d826eb4d4dcfd517e01589206 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperBootstrapOrderConfiguration.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.plugin.provider.type.paper; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++ ++public class PaperBootstrapOrderConfiguration implements LoadOrderConfiguration { ++ ++ private final PaperPluginMeta paperPluginMeta; ++ private final List<String> loadBefore = new ArrayList<>(); ++ private final List<String> loadAfter = new ArrayList<>(); ++ ++ public PaperBootstrapOrderConfiguration(PaperPluginMeta paperPluginMeta) { ++ this.paperPluginMeta = paperPluginMeta; ++ ++ for (Map.Entry<String, DependencyConfiguration> configuration : paperPluginMeta.getBoostrapDependencies().entrySet()) { ++ String name = configuration.getKey(); ++ DependencyConfiguration dependencyConfiguration = configuration.getValue(); ++ ++ if (dependencyConfiguration.load() == DependencyConfiguration.LoadOrder.AFTER) { ++ // This plugin will load BEFORE all dependencies (so dependencies will load AFTER plugin) ++ this.loadBefore.add(name); ++ } else if (dependencyConfiguration.load() == DependencyConfiguration.LoadOrder.BEFORE) { ++ // This plugin will load AFTER all dependencies (so dependencies will load BEFORE plugin) ++ this.loadAfter.add(name); ++ } ++ } ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadBefore() { ++ return this.loadBefore; ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadAfter() { ++ return this.loadAfter; ++ } ++ ++ @Override ++ public @NotNull PluginMeta getMeta() { ++ return this.paperPluginMeta; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperLoadOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperLoadOrderConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7e8a5ba375a558e0442aa9facf96954a9bb135f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperLoadOrderConfiguration.java +@@ -0,0 +1,44 @@ ++package io.papermc.paper.plugin.provider.type.paper; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++ ++public class PaperLoadOrderConfiguration implements LoadOrderConfiguration { ++ ++ private final PaperPluginMeta meta; ++ private final List<String> loadBefore; ++ private final List<String> loadAfter; ++ ++ public PaperLoadOrderConfiguration(PaperPluginMeta meta) { ++ this.meta = meta; ++ ++ this.loadBefore = this.meta.getLoadBeforePlugins(); ++ this.loadAfter = this.meta.getLoadAfterPlugins(); ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadBefore() { ++ return this.loadBefore; ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadAfter() { ++ return this.loadAfter; ++ } ++ ++ @Override ++ public @NotNull PluginMeta getMeta() { ++ return this.meta; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f2bc4d0b55d4c9877a442529e0b144656497dae6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java +@@ -0,0 +1,264 @@ ++package io.papermc.paper.plugin.provider.type.paper; ++ ++import com.destroystokyo.paper.util.SneakyThrow; ++import io.papermc.paper.plugin.bootstrap.PluginProviderContext; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; ++import io.papermc.paper.plugin.bootstrap.PluginBootstrap; ++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.ProviderStatus; ++import io.papermc.paper.plugin.provider.ProviderStatusHolder; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import io.papermc.paper.plugin.provider.type.PluginTypeFactory; ++import io.papermc.paper.plugin.provider.util.ProviderUtil; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.NotNull; ++ ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.jar.JarFile; ++ ++public class PaperPluginParent { ++ ++ public static final PluginTypeFactory<PaperPluginParent, PaperPluginMeta> FACTORY = new PaperPluginProviderFactory(); ++ private final Path path; ++ private final JarFile jarFile; ++ private final PaperPluginMeta description; ++ private final PaperPluginClassLoader classLoader; ++ private final PluginProviderContext context; ++ private final ComponentLogger logger; ++ ++ public PaperPluginParent(Path path, JarFile jarFile, PaperPluginMeta description, PaperPluginClassLoader classLoader, PluginProviderContext context) { ++ this.path = path; ++ this.jarFile = jarFile; ++ this.description = description; ++ this.classLoader = classLoader; ++ this.context = context; ++ this.logger = context.getLogger(); ++ } ++ ++ public boolean shouldCreateBootstrap() { ++ return this.description.getBootstrapper() != null; ++ } ++ ++ public PaperBootstrapProvider createBootstrapProvider() { ++ return new PaperBootstrapProvider(); ++ } ++ ++ public PaperServerPluginProvider createPluginProvider(PaperBootstrapProvider provider) { ++ return new PaperServerPluginProvider(provider); ++ } ++ ++ public class PaperBootstrapProvider implements PluginProvider<PluginBootstrap>, ProviderStatusHolder, DependencyContextHolder { ++ ++ private ProviderStatus status; ++ private PluginBootstrap lastProvided; ++ ++ @Override ++ public @NotNull Path getSource() { ++ return PaperPluginParent.this.path; ++ } ++ ++ @Override ++ public JarFile file() { ++ return PaperPluginParent.this.jarFile; ++ } ++ ++ @Override ++ public PluginBootstrap createInstance() { ++ PluginBootstrap bootstrap = ProviderUtil.loadClass(PaperPluginParent.this.description.getBootstrapper(), ++ PluginBootstrap.class, PaperPluginParent.this.classLoader, () -> this.status = ProviderStatus.ERRORED); ++ this.status = ProviderStatus.INITIALIZED; ++ this.lastProvided = bootstrap; ++ return bootstrap; ++ } ++ ++ @Override ++ public PaperPluginMeta getMeta() { ++ return PaperPluginParent.this.description; ++ } ++ ++ @Override ++ public ComponentLogger getLogger() { ++ return PaperPluginParent.this.logger; ++ } ++ ++ @Override ++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) { ++ return new PaperBootstrapOrderConfiguration(PaperPluginParent.this.description); ++ } ++ ++ @Override ++ public List<String> validateDependencies(@NotNull DependencyContext context) { ++ List<String> missingDependencies = new ArrayList<>(); ++ for (Map.Entry<String, DependencyConfiguration> configuration : this.getMeta().getBoostrapDependencies().entrySet()) { ++ String dependency = configuration.getKey(); ++ if (configuration.getValue().required() && !context.hasDependency(dependency)) { ++ missingDependencies.add(dependency); ++ } ++ } ++ ++ return missingDependencies; ++ } ++ ++ @Override ++ public ProviderStatus getLastProvidedStatus() { ++ return this.status; ++ } ++ ++ @Override ++ public void setStatus(ProviderStatus status) { ++ this.status = status; ++ } ++ ++ public PluginBootstrap getLastProvided() { ++ return this.lastProvided; ++ } ++ ++ @Override ++ public void setContext(DependencyContext context) { ++ PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context); ++ } ++ ++ @Override ++ public String toString() { ++ return "PaperBootstrapProvider{" + ++ "parent=" + PaperPluginParent.this + ++ "status=" + status + ++ ", lastProvided=" + lastProvided + ++ '}'; ++ } ++ } ++ ++ public class PaperServerPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder { ++ ++ private final PaperBootstrapProvider bootstrapProvider; ++ ++ private ProviderStatus status; ++ ++ PaperServerPluginProvider(PaperBootstrapProvider bootstrapProvider) { ++ this.bootstrapProvider = bootstrapProvider; ++ } ++ ++ @Override ++ public @NotNull Path getSource() { ++ return PaperPluginParent.this.path; ++ } ++ ++ @Override ++ public JarFile file() { ++ return PaperPluginParent.this.jarFile; ++ } ++ ++ @Override ++ public JavaPlugin createInstance() { ++ PluginBootstrap bootstrap = null; ++ if (this.bootstrapProvider != null && this.bootstrapProvider.getLastProvided() != null) { ++ bootstrap = this.bootstrapProvider.getLastProvided(); ++ } ++ ++ try { ++ JavaPlugin plugin; ++ if (bootstrap == null) { ++ plugin = ProviderUtil.loadClass(PaperPluginParent.this.description.getMainClass(), JavaPlugin.class, PaperPluginParent.this.classLoader); ++ } else { ++ plugin = bootstrap.createPlugin(PaperPluginParent.this.context); ++ } ++ ++ if (!plugin.getClass().isAssignableFrom(Class.forName(PaperPluginParent.this.description.getMainClass(), true, plugin.getClass().getClassLoader()))) { ++ logger.info("Bootstrap of plugin " + PaperPluginParent.this.description.getName() + " provided a plugin instance of class " + plugin.getClass().getName() + " which does not match the plugin declared main class"); ++ } ++ ++ this.status = ProviderStatus.INITIALIZED; ++ return plugin; ++ } catch (Throwable throwable) { ++ this.status = ProviderStatus.ERRORED; ++ SneakyThrow.sneaky(throwable); ++ } ++ ++ throw new AssertionError(); // Impossible ++ } ++ ++ @Override ++ public PaperPluginMeta getMeta() { ++ return PaperPluginParent.this.description; ++ } ++ ++ @Override ++ public ComponentLogger getLogger() { ++ return PaperPluginParent.this.logger; ++ } ++ ++ @Override ++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) { ++ return new PaperLoadOrderConfiguration(PaperPluginParent.this.description); ++ } ++ ++ @Override ++ public List<String> validateDependencies(@NotNull DependencyContext context) { ++ List<String> missingDependencies = new ArrayList<>(); ++ for (Map.Entry<String, DependencyConfiguration> dependency : this.getMeta().getServerDependencies().entrySet()) { ++ String name = dependency.getKey(); ++ if (dependency.getValue().required() && !context.hasDependency(name)) { ++ missingDependencies.add(name); ++ } ++ } ++ ++ return missingDependencies; ++ } ++ ++ @Override ++ public ProviderStatus getLastProvidedStatus() { ++ return this.status; ++ } ++ ++ @Override ++ public void setStatus(ProviderStatus status) { ++ this.status = status; ++ } ++ ++ public boolean shouldSkipCreation() { ++ if (this.bootstrapProvider == null) { ++ return false; ++ } ++ ++ return this.bootstrapProvider.getLastProvidedStatus() == ProviderStatus.ERRORED; ++ } ++ ++ /* ++ The plugin has to reuse the classloader in order to share the bootstrapper. ++ However, a plugin may have totally separate dependencies during bootstrapping. ++ This is a bit yuck, but in general we have to treat bootstrapping and normal game as connected. ++ */ ++ @Override ++ public void setContext(DependencyContext context) { ++ PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context); ++ } ++ ++ @Override ++ public String toString() { ++ return "PaperServerPluginProvider{" + ++ "parent=" + PaperPluginParent.this + ++ "bootstrapProvider=" + bootstrapProvider + ++ ", status=" + status + ++ '}'; ++ } ++ } ++ ++ ++ @Override ++ public String toString() { ++ return "PaperPluginParent{" + ++ "path=" + path + ++ ", jarFile=" + jarFile + ++ ", description=" + description + ++ ", classLoader=" + classLoader + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0a27b468560ccf4b9588cd12d50c02e442f3024f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java +@@ -0,0 +1,56 @@ ++package io.papermc.paper.plugin.provider.type.paper; ++ ++import com.destroystokyo.paper.utils.PaperPluginLogger; ++import io.papermc.paper.plugin.bootstrap.PluginProviderContext; ++import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl; ++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; ++import io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader; ++import io.papermc.paper.plugin.loader.PaperClasspathBuilder; ++import io.papermc.paper.plugin.loader.PluginLoader; ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import io.papermc.paper.plugin.provider.type.PluginTypeFactory; ++import io.papermc.paper.plugin.provider.util.ProviderUtil; ++ ++import java.io.BufferedReader; ++import java.io.IOException; ++import java.io.InputStreamReader; ++import java.nio.file.Path; ++import java.util.jar.JarEntry; ++import java.util.jar.JarFile; ++import java.util.logging.Logger; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++ ++class PaperPluginProviderFactory implements PluginTypeFactory<PaperPluginParent, PaperPluginMeta> { ++ ++ @Override ++ public PaperPluginParent build(JarFile file, PaperPluginMeta configuration, Path source) { ++ Logger jul = PaperPluginLogger.getLogger(configuration); ++ ComponentLogger logger = ComponentLogger.logger(jul.getName()); ++ PluginProviderContext context = PluginProviderContextImpl.create(configuration, logger, source); ++ ++ PaperClasspathBuilder builder = new PaperClasspathBuilder(context); ++ ++ if (configuration.getLoader() != null) { ++ try ( ++ PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(source, file, configuration, this.getClass().getClassLoader()) ++ ) { ++ PluginLoader loader = ProviderUtil.loadClass(configuration.getLoader(), PluginLoader.class, simplePluginClassLoader); ++ loader.classloader(builder); ++ } catch (IOException e) { ++ throw new RuntimeException(e); ++ } ++ } ++ ++ PaperPluginClassLoader classLoader = builder.buildClassLoader(jul, source, file, configuration); ++ return new PaperPluginParent(source, file, configuration, classLoader, context); ++ } ++ ++ @Override ++ public PaperPluginMeta create(JarFile file, JarEntry config) throws IOException { ++ PaperPluginMeta configuration; ++ try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(config)))) { ++ configuration = PaperPluginMeta.create(bufferedReader); ++ } ++ return configuration; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotLoadOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotLoadOrderConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b2a6544e321fa61c58bdf5684231de1020884fcc +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotLoadOrderConfiguration.java +@@ -0,0 +1,72 @@ ++package io.papermc.paper.plugin.provider.type.spigot; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++ ++public class SpigotLoadOrderConfiguration implements LoadOrderConfiguration { ++ ++ private final PluginDescriptionFile meta; ++ private final List<String> loadBefore; ++ private final List<String> loadAfter; ++ ++ public SpigotLoadOrderConfiguration(SpigotPluginProvider spigotPluginProvider, Map<String, PluginProvider<?>> toLoad) { ++ this.meta = spigotPluginProvider.getMeta(); ++ ++ this.loadBefore = meta.getLoadBeforePlugins(); ++ this.loadAfter = new ArrayList<>(); ++ this.loadAfter.addAll(meta.getDepend()); ++ this.loadAfter.addAll(meta.getSoftDepend()); ++ ++ // First: Remove as load after IF already in loadbefore ++ // Some plugins would put a plugin both in depends and in loadbefore, ++ // so in this case, we just ignore the effects of depend. ++ for (String loadBefore : this.loadBefore) { ++ this.loadAfter.remove(loadBefore); ++ } ++ ++ // Second: Do a basic check to see if any other dependencies refer back to this plugin. ++ Iterator<String> iterators = this.loadAfter.iterator(); ++ while (iterators.hasNext()) { ++ String loadAfter = iterators.next(); ++ PluginProvider<?> provider = toLoad.get(loadAfter); ++ if (provider != null) { ++ PluginMeta configuration = provider.getMeta(); ++ // Does a configuration refer back to this plugin? ++ Set<String> dependencies = new HashSet<>(); ++ dependencies.addAll(configuration.getPluginDependencies()); ++ dependencies.addAll(configuration.getPluginSoftDependencies()); ++ ++ if (configuration.getName().equals(this.meta.getName()) || dependencies.contains(this.meta.getName())) { ++ iterators.remove(); // Let the other config deal with it ++ } ++ } ++ } ++ ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadBefore() { ++ return this.loadBefore; ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadAfter() { ++ return this.loadAfter; ++ } ++ ++ @Override ++ public @NotNull PluginMeta getMeta() { ++ return this.meta; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..75a2b687d58d76b94f8bec111df8613f120ff74b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java +@@ -0,0 +1,197 @@ ++package io.papermc.paper.plugin.provider.type.spigot; ++ ++import com.destroystokyo.paper.util.SneakyThrow; ++import com.destroystokyo.paper.utils.PaperPluginLogger; ++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.ProviderStatus; ++import io.papermc.paper.plugin.provider.ProviderStatusHolder; ++import io.papermc.paper.plugin.provider.type.PluginTypeFactory; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import org.bukkit.Bukkit; ++import org.bukkit.Server; ++import org.bukkit.plugin.InvalidPluginException; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.UnknownDependencyException; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.bukkit.plugin.java.LibraryLoader; ++import org.bukkit.plugin.java.PluginClassLoader; ++import org.jetbrains.annotations.NotNull; ++ ++import java.io.File; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.jar.JarFile; ++import java.util.logging.Level; ++import java.util.logging.Logger; ++ ++public class SpigotPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder { ++ ++ public static final PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> FACTORY = new SpigotPluginProviderFactory(); ++ private static final LibraryLoader LIBRARY_LOADER = new LibraryLoader(Logger.getLogger("SpigotLibraryLoader")); ++ private final Path path; ++ private final PluginDescriptionFile description; ++ private final JarFile jarFile; ++ private final Logger logger; ++ private final ComponentLogger componentLogger; ++ private ProviderStatus status; ++ private DependencyContext dependencyContext; ++ ++ SpigotPluginProvider(Path path, JarFile file, PluginDescriptionFile description) { ++ this.path = path; ++ this.jarFile = file; ++ this.description = description; ++ this.logger = PaperPluginLogger.getLogger(description); ++ this.componentLogger = ComponentLogger.logger(this.logger.getName()); ++ } ++ ++ @Override ++ public @NotNull Path getSource() { ++ return this.path; ++ } ++ ++ @Override ++ public JarFile file() { ++ return this.jarFile; ++ } ++ ++ @Override ++ public JavaPlugin createInstance() { ++ Server server = Bukkit.getServer(); ++ try { ++ ++ final File parentFile = server.getPluginsFolder(); // Paper ++ final File dataFolder = new File(parentFile, this.description.getName()); ++ @SuppressWarnings("deprecation") final File oldDataFolder = new File(parentFile, this.description.getRawName()); ++ ++ // Found old data folder ++ if (dataFolder.equals(oldDataFolder)) { ++ // They are equal -- nothing needs to be done! ++ } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) { ++ server.getLogger().warning(String.format( ++ "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'", ++ this.description.getFullName(), ++ this.path, ++ oldDataFolder, ++ dataFolder ++ )); ++ } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) { ++ if (!oldDataFolder.renameTo(dataFolder)) { ++ throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'"); ++ } ++ server.getLogger().log(Level.INFO, String.format( ++ "While loading %s (%s) renamed data folder: `%s' to `%s'", ++ this.description.getFullName(), ++ this.path, ++ oldDataFolder, ++ dataFolder ++ )); ++ } ++ ++ if (dataFolder.exists() && !dataFolder.isDirectory()) { ++ throw new InvalidPluginException(String.format( ++ "Projected datafolder: `%s' for %s (%s) exists and is not a directory", ++ dataFolder, ++ this.description.getFullName(), ++ this.path ++ )); ++ } ++ ++ Set<String> missingHardDependencies = new HashSet<>(this.description.getDepend().size()); // Paper - list all missing hard depends ++ for (final String pluginName : this.description.getDepend()) { ++ if (!this.dependencyContext.hasDependency(pluginName)) { ++ missingHardDependencies.add(pluginName); // Paper - list all missing hard depends ++ } ++ } ++ // Paper start - list all missing hard depends ++ if (!missingHardDependencies.isEmpty()) { ++ throw new UnknownDependencyException(missingHardDependencies, this.description.getFullName()); ++ } ++ // Paper end ++ ++ server.getUnsafe().checkSupported(this.description); ++ ++ final PluginClassLoader loader; ++ try { ++ loader = new PluginClassLoader(this.getClass().getClassLoader(), this.description, dataFolder, this.path.toFile(), LIBRARY_LOADER.createLoader(this.description), this.jarFile, this.dependencyContext); // Paper ++ } catch (InvalidPluginException ex) { ++ throw ex; ++ } catch (Throwable ex) { ++ throw new InvalidPluginException(ex); ++ } ++ ++ // Override dependency context. ++ // We must provide a temporary context in order to properly handle dependencies on the plugin classloader constructor. ++ loader.dependencyContext = PaperPluginManagerImpl.getInstance(); ++ ++ ++ this.status = ProviderStatus.INITIALIZED; ++ return loader.getPlugin(); ++ } catch (Throwable ex) { ++ this.status = ProviderStatus.ERRORED; ++ SneakyThrow.sneaky(ex); ++ } ++ ++ throw new AssertionError(); // Shouldn't happen ++ } ++ ++ @Override ++ public PluginDescriptionFile getMeta() { ++ return this.description; ++ } ++ ++ @Override ++ public ComponentLogger getLogger() { ++ return this.componentLogger; ++ } ++ ++ @Override ++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) { ++ return new SpigotLoadOrderConfiguration(this, toLoad); ++ } ++ ++ @Override ++ public List<String> validateDependencies(@NotNull DependencyContext context) { ++ List<String> missingDependencies = new ArrayList<>(); ++ for (String hardDependency : this.getMeta().getPluginDependencies()) { ++ if (!context.hasDependency(hardDependency)) { ++ missingDependencies.add(hardDependency); ++ } ++ } ++ ++ return missingDependencies; ++ } ++ ++ @Override ++ public ProviderStatus getLastProvidedStatus() { ++ return this.status; ++ } ++ ++ @Override ++ public void setStatus(ProviderStatus status) { ++ this.status = status; ++ } ++ ++ @Override ++ public void setContext(DependencyContext context) { ++ this.dependencyContext = context; ++ } ++ ++ @Override ++ public String toString() { ++ return "SpigotPluginProvider{" + ++ "path=" + path + ++ ", description=" + description + ++ ", jarFile=" + jarFile + ++ ", status=" + status + ++ ", dependencyContext=" + dependencyContext + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bdd9bc8a414719b9f1d6f01f90539ddb8603a878 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java +@@ -0,0 +1,45 @@ ++package io.papermc.paper.plugin.provider.type.spigot; ++ ++import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; ++import io.papermc.paper.plugin.provider.type.PluginTypeFactory; ++import org.bukkit.plugin.InvalidDescriptionException; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.yaml.snakeyaml.error.YAMLException; ++ ++import java.io.IOException; ++import java.io.InputStream; ++import java.nio.file.Path; ++import java.util.Locale; ++import java.util.jar.JarEntry; ++import java.util.jar.JarFile; ++ ++class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> { ++ ++ @Override ++ public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException { ++ // Copied from SimplePluginManager#loadPlugins ++ // Spigot doesn't validate the name when the config is created, and instead when the plugin is loaded. ++ // Paper plugin configuration will do these checks in config serializer instead of when this is created. ++ String name = configuration.getRawName(); ++ if (PluginConfigConstraints.RESERVED_KEYS.contains(name.toLowerCase(Locale.ROOT))) { ++ throw new InvalidDescriptionException("Restricted name, cannot use %s as a plugin name.".formatted(name)); ++ } else if (name.indexOf(' ') != -1) { ++ throw new InvalidDescriptionException("Restricted name, cannot use 0x20 (space character) in a plugin name."); ++ } ++ ++ return new SpigotPluginProvider(source, file, configuration); ++ } ++ ++ @Override ++ public PluginDescriptionFile create(JarFile file, JarEntry config) throws InvalidDescriptionException { ++ PluginDescriptionFile descriptionFile; ++ try (InputStream inputStream = file.getInputStream(config)) { ++ descriptionFile = new PluginDescriptionFile(inputStream); ++ } catch (IOException | YAMLException ex) { ++ throw new InvalidDescriptionException(ex); ++ } ++ ++ return descriptionFile; ++ } ++} ++ +diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2e96308696e131f3f013469a395e5ddda2c5d529 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.plugin.storage; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.PluginInitializerManager; ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.bootstrap.PluginBootstrap; ++import io.papermc.paper.plugin.bootstrap.PluginBootstrapContextImpl; ++import io.papermc.paper.plugin.entrypoint.dependency.BootstrapMetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; ++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.ProviderStatus; ++import io.papermc.paper.plugin.provider.ProviderStatusHolder; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import org.slf4j.Logger; ++ ++public class BootstrapProviderStorage extends SimpleProviderStorage<PluginBootstrap> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public BootstrapProviderStorage() { ++ super(new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() { ++ @Override ++ public void applyContext(PluginProvider<PluginBootstrap> provider, DependencyContext dependencyContext) { ++ if (provider instanceof DependencyContextHolder contextHolder) { ++ contextHolder.setContext(dependencyContext); ++ } ++ } ++ ++ @Override ++ public boolean load(PluginProvider<PluginBootstrap> provider, PluginBootstrap provided) { ++ try { ++ BootstrapContext context = PluginBootstrapContextImpl.create(provider, PluginInitializerManager.instance().pluginDirectoryPath()); ++ provided.bootstrap(context); ++ return true; ++ } catch (Throwable e) { ++ LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e); ++ if (provider instanceof ProviderStatusHolder statusHolder) { ++ statusHolder.setStatus(ProviderStatus.ERRORED); ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public void onGenericError(PluginProvider<PluginBootstrap> provider) { ++ if (provider instanceof ProviderStatusHolder statusHolder) { ++ statusHolder.setStatus(ProviderStatus.ERRORED); ++ } ++ } ++ })); ++ } ++ ++ @Override ++ public MetaDependencyTree createDependencyTree() { ++ return new BootstrapMetaDependencyTree(); ++ } ++ ++ @Override ++ public String toString() { ++ return "BOOTSTRAP:" + super.toString(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8ef4806cadabe56264dd861f1a1854b2354b3b5c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java +@@ -0,0 +1,17 @@ ++package io.papermc.paper.plugin.storage; ++ ++import io.papermc.paper.plugin.entrypoint.strategy.LegacyPluginLoadingStrategy; ++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; ++ ++public abstract class ConfiguredProviderStorage<T> extends SimpleProviderStorage<T> { ++ ++ public static final boolean LEGACY_PLUGIN_LOADING = Boolean.getBoolean("paper.useLegacyPluginLoading"); ++ ++ protected ConfiguredProviderStorage(ProviderConfiguration<T> onLoad) { ++ // This doesn't work with reloading. ++ // Should we care? ++ super(LEGACY_PLUGIN_LOADING ? new LegacyPluginLoadingStrategy<>(onLoad) : new ModernPluginLoadingStrategy<>(onLoad)); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..39cd3acd3f76b3b0d065e0efb04a3eacea1c2e6b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java +@@ -0,0 +1,21 @@ ++package io.papermc.paper.plugin.storage; ++ ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.provider.PluginProvider; ++ ++/** ++ * A provider storage is meant to be a singleton that stores providers. ++ * ++ * @param <T> provider type ++ */ ++public interface ProviderStorage<T> { ++ ++ void register(PluginProvider<T> provider); ++ ++ MetaDependencyTree createDependencyTree(); ++ ++ void enter(); ++ ++ Iterable<PluginProvider<T>> getRegisteredProviders(); ++ ++} +diff --git a/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cb9b13522a976b82bcb71cef486f11f4172e3e99 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java +@@ -0,0 +1,70 @@ ++package io.papermc.paper.plugin.storage; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; ++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.ProviderStatus; ++import io.papermc.paper.plugin.provider.ProviderStatusHolder; ++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.slf4j.Logger; ++ ++import java.util.List; ++ ++public class ServerPluginProviderStorage extends ConfiguredProviderStorage<JavaPlugin> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public ServerPluginProviderStorage() { ++ super(new ProviderConfiguration<>() { ++ @Override ++ public void applyContext(PluginProvider<JavaPlugin> provider, DependencyContext dependencyContext) { ++ Plugin alreadyLoadedPlugin = PaperPluginManagerImpl.getInstance().getPlugin(provider.getMeta().getName()); ++ if (alreadyLoadedPlugin != null) { ++ throw new IllegalStateException("Provider " + provider + " attempted to add duplicate plugin identifier " + alreadyLoadedPlugin + " THIS WILL CREATE BUGS!!!"); ++ } ++ ++ if (provider instanceof DependencyContextHolder contextHolder) { ++ contextHolder.setContext(dependencyContext); ++ } ++ } ++ ++ @Override ++ public boolean load(PluginProvider<JavaPlugin> provider, JavaPlugin provided) { ++ // Add it to the map here, we have to run the actual loading logic later. ++ PaperPluginManagerImpl.getInstance().loadPlugin(provided); ++ return true; ++ } ++ }); ++ } ++ ++ @Override ++ protected void filterLoadingProviders(List<PluginProvider<JavaPlugin>> pluginProviders) { ++ /* ++ Have to do this to prevent loading plugin providers that have failed initializers. ++ This is a hack and a better solution here would be to store failed plugin providers elsewhere. ++ */ ++ pluginProviders.removeIf((provider) -> (provider instanceof PaperPluginParent.PaperServerPluginProvider pluginProvider && pluginProvider.shouldSkipCreation())); ++ } ++ ++ // We need to call the load methods AFTER all plugins are constructed ++ @Override ++ public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) { ++ try { ++ provided.getLogger().info(String.format("Loading server plugin %s", provided.getPluginMeta().getDisplayName())); ++ provided.onLoad(); ++ } catch (Throwable ex) { ++ // Don't mark that provider as ERRORED, as this apparently still needs to run the onEnable logic. ++ provided.getSLF4JLogger().error("Error initializing plugin '%s' in folder '%s' (Is it up to date?)".formatted(provider.getFileName(), provider.getParentSource()), ex); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "PLUGIN:" + super.toString(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..26422904751647a061397ce978bba752149003cd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java +@@ -0,0 +1,93 @@ ++package io.papermc.paper.plugin.storage; ++ ++import com.google.common.graph.GraphBuilder; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import org.slf4j.Logger; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.stream.Collectors; ++ ++public abstract class SimpleProviderStorage<T> implements ProviderStorage<T> { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ protected final List<PluginProvider<T>> providers = new ArrayList<>(); ++ protected ProviderLoadingStrategy<T> strategy; ++ ++ protected SimpleProviderStorage(ProviderLoadingStrategy<T> strategy) { ++ this.strategy = strategy; ++ } ++ ++ @Override ++ public void register(PluginProvider<T> provider) { ++ this.providers.add(provider); ++ } ++ ++ @Override ++ public void enter() { ++ List<PluginProvider<T>> providerList = new ArrayList<>(this.providers); ++ this.filterLoadingProviders(providerList); ++ ++ try { ++ for (ProviderLoadingStrategy.ProviderPair<T> providerPair : this.strategy.loadProviders(providerList, this.createDependencyTree())) { ++ this.processProvided(providerPair.provider(), providerPair.provided()); ++ } ++ } catch (PluginGraphCycleException exception) { ++ this.handleCycle(exception); ++ } ++ } ++ ++ @Override ++ public MetaDependencyTree createDependencyTree() { ++ return new SimpleMetaDependencyTree(GraphBuilder.directed().build()); ++ } ++ ++ @Override ++ public Iterable<PluginProvider<T>> getRegisteredProviders() { ++ return this.providers; ++ } ++ ++ public void processProvided(PluginProvider<T> provider, T provided) { ++ } ++ ++ // Mutable enter ++ protected void filterLoadingProviders(List<PluginProvider<T>> providers) { ++ } ++ ++ protected void handleCycle(PluginGraphCycleException exception) { ++ List<String> logMessages = new ArrayList<>(); ++ for (List<String> list : exception.getCycles()) { ++ logMessages.add(String.join(" -> ", list) + " -> " + list.get(0)); ++ } ++ ++ LOGGER.error("Circular plugin loading detected!"); ++ LOGGER.error("Circular load order:"); ++ for (String logMessage : logMessages) { ++ LOGGER.error(" {}", logMessage); ++ } ++ LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help."); ++ LOGGER.error("If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true"); ++ ++ if (this.throwOnCycle()) { ++ throw new IllegalStateException("Circular plugin loading from plugins " + exception.getCycles().stream().map(cycle -> cycle.get(0)).collect(Collectors.joining(", "))); ++ } ++ } ++ ++ public boolean throwOnCycle() { ++ return true; ++ } ++ ++ @Override ++ public String toString() { ++ return "SimpleProviderStorage{" + ++ "providers=" + this.providers + ++ ", strategy=" + this.strategy + ++ '}'; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/storage/package-info.java b/src/main/java/io/papermc/paper/plugin/storage/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c1114675137e862ac9682b635bfdbfbc1d7c6e67 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/storage/package-info.java +@@ -0,0 +1,5 @@ ++/** ++ * Classes in this package are supposed to connect components of {@link io.papermc.paper.plugin.entrypoint} and {@link io.papermc.paper.plugin.provider} packages. ++ * @see io.papermc.paper.plugin.entrypoint.Entrypoint ++ */ ++package io.papermc.paper.plugin.storage; +diff --git a/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..01c88a23755618b98c1a1cdeb8e404e79401940b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.plugin.util; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.provider.source.ProviderSource; ++import org.slf4j.Logger; ++ ++public final class EntrypointUtil { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public static <I, C> void registerProvidersFromSource(ProviderSource<I, C> source, I contextInput) { ++ try { ++ C context = source.prepareContext(contextInput); ++ source.registerProviders(LaunchEntryPointHandler.INSTANCE, context); ++ } catch (Throwable e) { ++ LOGGER.error(e.getMessage(), e); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fd55fd1d6518ebd1bc2513dd331f072018fd4782 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java +@@ -0,0 +1,37 @@ ++package io.papermc.paper.plugin.util; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.NotNull; ++ ++public class NamespaceChecker { ++ ++ private static final String[] QUICK_INVALID_NAMESPACES = { ++ "net.minecraft.", ++ "org.bukkit.", ++ "io.papermc.paper.", ++ "com.destroystokoyo.paper." ++ }; ++ ++ /** ++ * Used for a variety of namespaces that shouldn't be resolved and should instead be moved to ++ * other classloaders. We can assume this because only plugins should be using this classloader. ++ * ++ * @param name namespace ++ */ ++ public static void validateNameSpaceForClassloading(@NotNull String name) throws ClassNotFoundException { ++ if (!isValidNameSpace(name)) { ++ throw new ClassNotFoundException(name); ++ } ++ } ++ ++ public static boolean isValidNameSpace(@NotNull String name) { ++ for (String string : QUICK_INVALID_NAMESPACES) { ++ if (name.startsWith(string)) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java +index f7114d5b8f2f93f62883e24da29afaf9f74ee1a6..8bf0630c0e06950cd99b7ae9898137f70c22063f 100644 +--- a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java ++++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java +@@ -1,9 +1,10 @@ + package io.papermc.paper.util; + ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; + import org.bukkit.plugin.java.JavaPlugin; +-import org.bukkit.plugin.java.PluginClassLoader; + import org.jetbrains.annotations.Nullable; + ++import java.util.Objects; + import java.util.Optional; + + public class StackWalkerUtil { +@@ -12,11 +13,18 @@ public class StackWalkerUtil { + public static JavaPlugin getFirstPluginCaller() { + Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(stream -> stream +- .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader) + .map((frame) -> { +- PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader(); +- return classLoader.getPlugin(); ++ ClassLoader classLoader = frame.getDeclaringClass().getClassLoader(); ++ JavaPlugin plugin; ++ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) { ++ plugin = configuredPluginClassLoader.getPlugin(); ++ } else { ++ plugin = null; ++ } ++ ++ return plugin; + }) ++ .filter(Objects::nonNull) + .findFirst()); + + return foundFrame.orElse(null); +diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +index 34b3b3251da21bce616870d312fd42fd58ba7881..cbc1658e0df4070605a6b2fbe99167b3bc001223 100644 +--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java ++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +@@ -322,7 +322,13 @@ public class BuiltInRegistries { + } + + public static void bootStrap() { ++ // Paper start ++ bootStrap(() -> {}); ++ } ++ public static void bootStrap(Runnable runnable) { ++ // Paper end + createContents(); ++ runnable.run(); // Paper + freeze(); + validate(REGISTRY); + } +diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java +index 394792291e3b89e5fd757907eecd85ccc71183e2..8f1992188f7fd9e735569e099b36a7eafed47aae 100644 +--- a/src/main/java/net/minecraft/server/Bootstrap.java ++++ b/src/main/java/net/minecraft/server/Bootstrap.java +@@ -62,6 +62,7 @@ public class Bootstrap { + Bootstrap.isBootstrapped = true; + Instant instant = Instant.now(); + ++ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping + if (BuiltInRegistries.REGISTRY.keySet().isEmpty()) { + throw new IllegalStateException("Unable to load registries"); + } else { +@@ -73,7 +74,10 @@ public class Bootstrap { + EntitySelectorOptions.bootStrap(); + DispenseItemBehavior.bootStrap(); + CauldronInteraction.bootStrap(); +- BuiltInRegistries.bootStrap(); ++ // Paper start ++ BuiltInRegistries.bootStrap(() -> { ++ }); ++ // Paper end + CreativeModeTabs.validate(); + Bootstrap.wrapStreams(); + Bootstrap.bootstrapDuration.set(Duration.between(instant, Instant.now()).toMillis()); +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index 5b4ac7b4fd0077e900e9f788963f1613bbc9a5d0..6afede80c10503a261d0f735c351d943597be9ff 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -119,6 +119,7 @@ public class Main { + JvmProfiler.INSTANCE.start(Environment.SERVER); + } + ++ io.papermc.paper.plugin.PluginInitializerManager.load(optionset); // Paper + Bootstrap.bootStrap(); + Bootstrap.validate(); + Util.startTimerHackThread(); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index e7301f38be4fae26404fd8dd33798ef0764565ac..3f748a8f067d8ce8c39272cb3decaf4ec4879da5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -277,7 +277,8 @@ public final class CraftServer implements Server { + private final CraftCommandMap commandMap = new CraftCommandMap(this); + private final SimpleHelpMap helpMap = new SimpleHelpMap(this); + private final StandardMessenger messenger = new StandardMessenger(); +- private final SimplePluginManager pluginManager = new SimplePluginManager(this, this.commandMap); ++ private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap); ++ public final io.papermc.paper.plugin.manager.PaperPluginManagerImpl paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); {this.pluginManager.paperPluginManager = this.paperPluginManager;} // Paper + private final StructureManager structureManager; + protected final DedicatedServer console; + protected final DedicatedPlayerList playerList; +@@ -446,24 +447,7 @@ public final class CraftServer implements Server { + } + + public void loadPlugins() { +- this.pluginManager.registerInterface(JavaPluginLoader.class); +- +- File pluginFolder = (File) this.console.options.valueOf("plugins"); +- +- if (pluginFolder.exists()) { +- Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder); +- for (Plugin plugin : plugins) { +- try { +- String message = String.format("Loading %s", plugin.getDescription().getFullName()); +- plugin.getLogger().info(message); +- plugin.onLoad(); +- } catch (Throwable ex) { +- Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); +- } +- } +- } else { +- pluginFolder.mkdir(); +- } ++ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation + } + + public void enablePlugins(PluginLoadOrder type) { +@@ -552,15 +536,17 @@ public final class CraftServer implements Server { + private void enablePlugin(Plugin plugin) { + try { + List<Permission> perms = plugin.getDescription().getPermissions(); +- ++ List<Permission> permsToLoad = new ArrayList<>(); // Paper + for (Permission perm : perms) { +- try { +- this.pluginManager.addPermission(perm, false); +- } catch (IllegalArgumentException ex) { +- this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex); ++ // Paper start ++ if (this.paperPluginManager.getPermission(perm.getName()) == null) { ++ permsToLoad.add(perm); ++ } else { ++ this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered"); ++ // Paper end + } + } +- this.pluginManager.dirtyPermissibles(); ++ this.paperPluginManager.addPermissions(permsToLoad); // Paper + + this.pluginManager.enablePlugin(plugin); + } catch (Throwable ex) { +@@ -1002,6 +988,7 @@ public final class CraftServer implements Server { + "This plugin is not properly shutting down its async tasks when it is being reloaded. This may cause conflicts with the newly loaded version of the plugin" + )); + } ++ io.papermc.paper.plugin.PluginInitializerManager.reload(this.console); // Paper + this.loadPlugins(); + this.enablePlugins(PluginLoadOrder.STARTUP); + this.enablePlugins(PluginLoadOrder.POSTWORLD); +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +index 909b2c98e7a9117d2f737245e4661792ffafb744..d96399e9bf1a58db5a4a22e58abb99e7660e0694 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +@@ -42,6 +42,12 @@ public class MinecraftInternalPlugin extends PluginBase { + public PluginDescriptionFile getDescription() { + return pdf; + } ++ // Paper start ++ @Override ++ public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { ++ return pdf; ++ } ++ // Paper end + + @Override + public FileConfiguration getConfig() { +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index fb22e04a4a9f039f0e942ff837124b00ca1b12be..297767f64c1a7c01cffd6d78e71b60746622f22e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -426,6 +426,16 @@ public final class CraftMagicNumbers implements UnsafeValues { + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + return nmsItemStack.getItem().getDescriptionId(nmsItemStack); + } ++ // Paper start ++ @Override ++ public boolean isSupportedApiVersion(String apiVersion) { ++ if (apiVersion == null) return false; ++ final ApiVersion toCheck = ApiVersion.getOrCreateVersion(apiVersion); ++ final ApiVersion minimumVersion = MinecraftServer.getServer().server.minimumAPI; ++ ++ return !toCheck.isNewerThan(ApiVersion.CURRENT) && !toCheck.isOlderThan(minimumVersion); ++ } ++ // Paper end + + @Override + public String getTranslationKey(final Attribute attribute) { +diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier +new file mode 100644 +index 0000000000000000000000000000000000000000..20dbe2775951bfcdb85c5d679ac86c77a93e0847 +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier +@@ -0,0 +1 @@ ++io.papermc.paper.plugin.entrypoint.classloader.PaperClassloaderBytecodeModifier +diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage +new file mode 100644 +index 0000000000000000000000000000000000000000..a22647244037cd92262b3b5a6582f0a11172fdc8 +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage +@@ -0,0 +1 @@ ++io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage +diff --git a/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader b/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader +new file mode 100644 +index 0000000000000000000000000000000000000000..4f78bc62d03460463b9694de933e5b73da8df6e3 +--- /dev/null ++++ b/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader +@@ -0,0 +1 @@ ++io.papermc.paper.plugin.manager.DummyBukkitPluginLoader +diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1d14f530ef888102e47eeeaf0d1a6076e51871c4 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java +@@ -0,0 +1,146 @@ ++package io.papermc.paper.plugin; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import org.bukkit.Server; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.configuration.file.FileConfiguration; ++import org.bukkit.generator.BiomeProvider; ++import org.bukkit.generator.ChunkGenerator; ++import org.bukkit.plugin.PluginBase; ++import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.PluginLoader; ++import org.bukkit.plugin.PluginLogger; ++ ++import java.io.File; ++import java.io.InputStream; ++import java.util.List; ++ ++public class PaperTestPlugin extends PluginBase { ++ private final String pluginName; ++ private boolean enabled = true; ++ private final PluginMeta configuration; ++ ++ public PaperTestPlugin(String pluginName) { ++ this.pluginName = pluginName; ++ this.configuration = new TestPluginMeta(pluginName); ++ } ++ ++ public PaperTestPlugin(PluginMeta configuration) { ++ this.configuration = configuration; ++ this.pluginName = configuration.getName(); ++ } ++ ++ @Override ++ public File getDataFolder() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginDescriptionFile getDescription() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginMeta getPluginMeta() { ++ return this.configuration; ++ } ++ ++ @Override ++ public FileConfiguration getConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public InputStream getResource(String filename) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void saveConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void saveDefaultConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void saveResource(String resourcePath, boolean replace) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void reloadConfig() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginLogger getLogger() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public PluginLoader getPluginLoader() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public Server getServer() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public boolean isEnabled() { ++ return enabled; ++ } ++ ++ public void setEnabled(boolean enabled) { ++ this.enabled = enabled; ++ } ++ ++ @Override ++ public void onDisable() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void onLoad() { ++ } ++ ++ @Override ++ public void onEnable() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public boolean isNaggable() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public void setNaggable(boolean canNag) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public BiomeProvider getDefaultBiomeProvider(String worldName, String id) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ @Override ++ public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java b/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..83b1274ba56f03bec6cb69a35f33dc04f008cc1e +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java +@@ -0,0 +1,60 @@ ++package io.papermc.paper.plugin; ++ ++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree; ++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree; ++import org.junit.jupiter.api.Test; ++ ++import java.util.List; ++ ++import static org.hamcrest.MatcherAssert.assertThat; ++ ++public class PluginDependencyValidationTest { ++ ++ private static final TestPluginMeta MAIN; ++ private static final TestPluginMeta HARD_DEPENDENCY_1; ++ private static final TestPluginMeta SOFT_DEPENDENCY_1; ++ ++ public static final String ROOT_NAME = "main"; ++ ++ public static final String REGISTERED_HARD_DEPEND = "hard1"; ++ public static final String REGISTERED_SOFT_DEPEND = "soft1"; ++ public static final String UNREGISTERED_HARD_DEPEND = "hard2"; ++ public static final String UNREGISTERED_SOFT_DEPEND = "soft2"; ++ ++ static { ++ MAIN = new TestPluginMeta(ROOT_NAME); ++ MAIN.setSoftDependencies(List.of(REGISTERED_SOFT_DEPEND, UNREGISTERED_SOFT_DEPEND)); ++ MAIN.setHardDependencies(List.of(REGISTERED_HARD_DEPEND, UNREGISTERED_HARD_DEPEND)); ++ ++ HARD_DEPENDENCY_1 = new TestPluginMeta(REGISTERED_HARD_DEPEND); ++ SOFT_DEPENDENCY_1 = new TestPluginMeta(REGISTERED_SOFT_DEPEND); ++ } ++ ++ @Test ++ public void testDependencyTree() { ++ MetaDependencyTree tree = new SimpleMetaDependencyTree(); ++ tree.add(MAIN); ++ tree.add(HARD_DEPENDENCY_1); ++ tree.add(SOFT_DEPENDENCY_1); ++ ++ // Test simple transitive dependencies ++ assertThat("%s was not a transitive dependency of %s".formatted(ROOT_NAME, REGISTERED_SOFT_DEPEND), tree.isTransitiveDependency(MAIN, SOFT_DEPENDENCY_1)); ++ assertThat("%s was not a transitive dependency of %s".formatted(ROOT_NAME, REGISTERED_HARD_DEPEND), tree.isTransitiveDependency(MAIN, HARD_DEPENDENCY_1)); ++ ++ assertThat("%s was a transitive dependency of %s".formatted(REGISTERED_SOFT_DEPEND, ROOT_NAME), !tree.isTransitiveDependency(SOFT_DEPENDENCY_1, MAIN)); ++ assertThat("%s was a transitive dependency of %s".formatted(REGISTERED_HARD_DEPEND, ROOT_NAME), !tree.isTransitiveDependency(HARD_DEPENDENCY_1, MAIN)); ++ ++ // Test to ensure that registered dependencies exist ++ assertThat("tree did not contain dependency %s".formatted(ROOT_NAME), tree.hasDependency(ROOT_NAME)); ++ assertThat("tree did not contain dependency %s".formatted(REGISTERED_HARD_DEPEND), tree.hasDependency(REGISTERED_HARD_DEPEND)); ++ assertThat("tree did not contain dependency %s".formatted(REGISTERED_SOFT_DEPEND), tree.hasDependency(REGISTERED_SOFT_DEPEND)); ++ ++ // Test to ensure unregistered dependencies don't exist ++ assertThat("tree contained dependency %s".formatted(UNREGISTERED_HARD_DEPEND), !tree.hasDependency(UNREGISTERED_HARD_DEPEND)); ++ assertThat("tree contained dependency %s".formatted(UNREGISTERED_SOFT_DEPEND), !tree.hasDependency(UNREGISTERED_SOFT_DEPEND)); ++ ++ // Test removal ++ tree.remove(HARD_DEPENDENCY_1); ++ assertThat("tree contained dependency %s".formatted(REGISTERED_HARD_DEPEND), !tree.hasDependency(REGISTERED_HARD_DEPEND)); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java b/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c2c3c2f24ea802628bc4a36ef180fc08f4e5d288 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java +@@ -0,0 +1,148 @@ ++package io.papermc.paper.plugin; ++ ++import com.google.common.graph.GraphBuilder; ++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy; ++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.BeforeEach; ++import org.junit.jupiter.api.Test; ++ ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++public class PluginLoadOrderTest { ++ ++ private static List<PluginProvider<PaperTestPlugin>> REGISTERED_PROVIDERS = new ArrayList<>(); ++ private static Map<String, Integer> LOAD_ORDER = new HashMap<>(); ++ private static final String[] EMPTY = {}; ++ ++ static { ++ setup(); ++ } ++ ++ private static TestJavaPluginProvider setup(String identifier, String[] loadAfter, String[] loadAfterSoft, String[] before) { ++ TestPluginMeta configuration = new TestPluginMeta(identifier); ++ configuration.setHardDependencies(List.of(loadAfter)); ++ configuration.setSoftDependencies(List.of(loadAfterSoft)); ++ configuration.setLoadBefore(List.of(before)); ++ ++ TestJavaPluginProvider provider = new TestJavaPluginProvider(configuration); ++ REGISTERED_PROVIDERS.add(provider); ++ return provider; ++ } ++ ++ /** ++ * Obfuscated plugin names, this uses a real dependency tree... ++ */ ++ private static void setup() { ++ setup("RedAir", EMPTY, new String[]{"NightShovel", "EmeraldFire"}, new String[]{"GreenShovel", "IronSpork", "BrightBlueShovel", "WireDoor"}); ++ setup("BigGrass", EMPTY, new String[]{"IronEarth", "RedAir"}, new String[]{"BlueFire"}); ++ setup("BlueFire", EMPTY, EMPTY, EMPTY); ++ setup("BigPaper", EMPTY, new String[]{"BlueFire"}, EMPTY); ++ setup("EmeraldSpork", EMPTY, EMPTY, new String[]{"GoldPaper", "YellowSnow"}); ++ setup("GreenShovel", EMPTY, EMPTY, EMPTY); ++ setup("BrightBlueGrass", new String[]{"BigPaper"}, new String[]{"DarkSpork"}, EMPTY); ++ setup("GoldPaper", EMPTY, new String[]{"BlueFire"}, EMPTY); ++ setup("GreenGlass", EMPTY, EMPTY, EMPTY); ++ setup("GoldNeptune", EMPTY, new String[]{"GreenShovel", "GoldNeptuneVersioning"}, EMPTY); ++ setup("RedPaper", EMPTY, new String[]{"GoldPaper", "GoldFire", "EmeraldGrass", "BlueFire", "CopperSpork", "YellowDoor", "OrangeClam", "BlueSponge", "GoldNeptune", "BrightBlueGrass", "DarkSpoon", "BigShovel", "GreenGlass", "IronGlass"}, new String[]{"IronPaper", "YellowFire"}); ++ setup("YellowGrass", EMPTY, new String[]{"RedAir"}, EMPTY); ++ setup("WireFire", EMPTY, new String[]{"RedPaper", "WireGrass", "YellowSpork", "NightAir"}, EMPTY); ++ setup("OrangeNeptune", EMPTY, EMPTY, EMPTY); ++ setup("BigSpoon", new String[]{"YellowGrass", "GreenShovel"}, new String[]{"RedAir", "GoldNeptune", "BrightBlueGrass", "LightDoor", "LightSpork", "LightEarth", "NightDoor", "OrangeSpoon", "GoldSponge", "GoldDoor", "DarkPaper", "RedPaper", "GreenGlass", "IronGlass", "NightGlass", "BigGrass", "BlueFire", "YellowSpoon", "DiamondGrass", "DiamondShovel", "DarkSnow", "EmeraldGlass", "EmeraldSpoon", "LightFire", "WireGrass", "RedEarth", "WireFire"}, EMPTY); ++ setup("CopperSnow", EMPTY, new String[]{"RedSnow", "OrangeFire", "WireAir", "GreenGlass", "NightSpork", "EmeraldPaper"}, new String[]{"BlueGrass"}); ++ setup("BrightBluePaper", EMPTY, new String[]{"GoldEarth", "BrightBlueSpoon", "CopperGlass", "LightSporkChat", "DarkAir", "LightEarth", "DiamondDoor", "YellowShovel", "BlueAir", "DarkShovel", "GoldPaper", "BlueFire", "GreenGlass", "YellowSpork", "BigGrass", "OrangePaper", "DarkPaper"}, new String[]{"WireShovel"}); ++ setup("LightSponge", EMPTY, EMPTY, EMPTY); ++ setup("OrangeShovel", EMPTY, EMPTY, EMPTY); ++ setup("GoldGrass", EMPTY, new String[]{"GreenGlass", "BlueFire"}, EMPTY); ++ setup("IronSponge", EMPTY, new String[]{"DiamondEarth"}, EMPTY); ++ setup("EmeraldSnow", EMPTY, EMPTY, EMPTY); ++ setup("BlueSpoon", new String[]{"BigGrass"}, new String[]{"GreenGlass", "GoldPaper", "GreenShovel", "YellowClam"}, EMPTY); ++ setup("BigSpork", EMPTY, new String[]{"BigPaper"}, EMPTY); ++ setup("BluePaper", EMPTY, new String[]{"BigClam", "RedSpoon", "GreenFire", "WireSnow", "OrangeSnow", "BlueFire", "BrightBlueGrass", "YellowSpork", "GreenGlass"}, EMPTY); ++ setup("OrangeSpork", EMPTY, EMPTY, EMPTY); ++ setup("DiamondNeptune", EMPTY, new String[]{"GreenGlass", "GreenShovel", "YellowNeptune"}, EMPTY); ++ setup("BigFire", EMPTY, new String[]{"BlueFire", "BrightBlueDoor", "GreenGlass"}, EMPTY); ++ setup("NightNeptune", EMPTY, new String[]{"BlueFire", "DarkGlass", "GoldPaper", "YellowNeptune", "BlueShovel"}, EMPTY); ++ setup("YellowEarth", new String[]{"RedAir"}, EMPTY, EMPTY); ++ setup("DiamondClam", EMPTY, EMPTY, EMPTY); ++ setup("CopperAir", EMPTY, new String[]{"BigPaper"}, EMPTY); ++ setup("NightSpoon", new String[]{"OrangeNeptune"}, new String[]{"BlueFire", "GreenGlass", "RedSpork", "GoldPaper", "BigShovel", "YellowSponge", "EmeraldSpork"}, EMPTY); ++ setup("GreenClam", EMPTY, new String[]{"GreenShovel", "BrightBlueEarth", "BigSpoon", "RedPaper", "BlueFire", "GreenGlass", "WireFire", "GreenSnow"}, EMPTY); ++ setup("YellowPaper", EMPTY, EMPTY, EMPTY); ++ setup("WireGlass", new String[]{"YellowGrass"}, new String[]{"YellowGlass", "BigSpoon", "CopperSnow", "GreenGlass", "BlueEarth"}, EMPTY); ++ setup("BlueSpork", EMPTY, new String[]{"BrightBlueGrass"}, EMPTY); ++ setup("CopperShovel", EMPTY, new String[]{"GreenGlass"}, EMPTY); ++ setup("RedClam", EMPTY, EMPTY, EMPTY); ++ setup("EmeraldClam", EMPTY, new String[]{"BlueFire"}, EMPTY); ++ setup("DarkClam", EMPTY, new String[]{"GoldAir", "LightGlass"}, EMPTY); ++ setup("WireSpoon", EMPTY, new String[]{"GoldPaper", "LightSnow"}, EMPTY); ++ setup("CopperNeptune", EMPTY, new String[]{"GreenGlass", "BigGrass"}, EMPTY); ++ setup("RedNeptune", EMPTY, EMPTY, EMPTY); ++ setup("GreenAir", EMPTY, EMPTY, EMPTY); ++ setup("RedFire", new String[]{"BrightBlueGrass", "BigPaper"}, new String[]{"BlueFire", "GreenGlass", "BigGrass"}, EMPTY); ++ } ++ ++ @BeforeEach ++ public void loadProviders() { ++ AtomicInteger currentLoad = new AtomicInteger(); ++ ModernPluginLoadingStrategy<PaperTestPlugin> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() { ++ @Override ++ public void applyContext(PluginProvider<PaperTestPlugin> provider, DependencyContext dependencyContext) { ++ } ++ ++ @Override ++ public boolean load(PluginProvider<PaperTestPlugin> provider, PaperTestPlugin provided) { ++ LOAD_ORDER.put(provider.getMeta().getName(), currentLoad.getAndIncrement()); ++ return false; ++ } ++ ++ }); ++ ++ modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS, new SimpleMetaDependencyTree(GraphBuilder.directed().build())); ++ } ++ ++ @Test ++ public void testDependencies() { ++ for (PluginProvider<PaperTestPlugin> provider : REGISTERED_PROVIDERS) { ++ TestPluginMeta pluginMeta = (TestPluginMeta) provider.getMeta(); ++ String identifier = pluginMeta.getName(); ++ Assertions.assertTrue(LOAD_ORDER.containsKey(identifier), "Provider wasn't loaded! (%s)".formatted(identifier)); ++ ++ int index = LOAD_ORDER.get(identifier); ++ ++ // Hard dependencies should be loaded BEFORE ++ for (String hardDependency : pluginMeta.getPluginDependencies()) { ++ Assertions.assertTrue(LOAD_ORDER.containsKey(hardDependency), "Plugin (%s) is missing hard dependency (%s)".formatted(identifier, hardDependency)); ++ ++ int dependencyIndex = LOAD_ORDER.get(hardDependency); ++ Assertions.assertTrue(index > dependencyIndex, "Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, hardDependency)); ++ } ++ ++ for (String softDependency : pluginMeta.getPluginSoftDependencies()) { ++ if (!LOAD_ORDER.containsKey(softDependency)) { ++ continue; ++ } ++ ++ int dependencyIndex = LOAD_ORDER.get(softDependency); ++ ++ Assertions.assertTrue(index > dependencyIndex, "Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, softDependency)); ++ } ++ ++ for (String loadBefore : pluginMeta.getLoadBeforePlugins()) { ++ if (!LOAD_ORDER.containsKey(loadBefore)) { ++ continue; ++ } ++ ++ int dependencyIndex = LOAD_ORDER.get(loadBefore); ++ Assertions.assertTrue(index < dependencyIndex, "Plugin (%s) was NOT loaded BEFORE loadbefore dependency. (%s)".formatted(identifier, loadBefore)); ++ } ++ } ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7d69dda2b88218221a3cca6db4445cb58aa0179 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java +@@ -0,0 +1,75 @@ ++package io.papermc.paper.plugin; ++ ++import org.bukkit.Bukkit; ++import org.bukkit.event.Event; ++import org.bukkit.permissions.Permission; ++import org.bukkit.plugin.PluginManager; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.api.AfterEach; ++import org.junit.jupiter.api.Test; ++ ++import static org.hamcrest.MatcherAssert.assertThat; ++import static org.hamcrest.Matchers.*; ++ ++public class PluginManagerTest extends AbstractTestingBase { ++ ++ private static final PluginManager pm = Bukkit.getPluginManager(); ++ ++ @Test ++ public void testSyncSameThread() { ++ final Event event = new TestEvent(false); ++ pm.callEvent(event); ++ } ++ ++ @Test ++ public void testRemovePermissionByNameLower() { ++ this.testRemovePermissionByName("lower"); ++ } ++ ++ @Test ++ public void testRemovePermissionByNameUpper() { ++ this.testRemovePermissionByName("UPPER"); ++ } ++ ++ @Test ++ public void testRemovePermissionByNameCamel() { ++ this.testRemovePermissionByName("CaMeL"); ++ } ++ ++ @Test ++ public void testRemovePermissionByPermissionLower() { ++ this.testRemovePermissionByPermission("lower"); ++ } ++ ++ @Test ++ public void testRemovePermissionByPermissionUpper() { ++ this.testRemovePermissionByPermission("UPPER"); ++ } ++ ++ @Test ++ public void testRemovePermissionByPermissionCamel() { ++ this.testRemovePermissionByPermission("CaMeL"); ++ } ++ ++ private void testRemovePermissionByName(final String name) { ++ final Permission perm = new Permission(name); ++ pm.addPermission(perm); ++ assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm)); ++ pm.removePermission(name); ++ assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue())); ++ } ++ ++ private void testRemovePermissionByPermission(final String name) { ++ final Permission perm = new Permission(name); ++ pm.addPermission(perm); ++ assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm)); ++ pm.removePermission(perm); ++ assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue())); ++ } ++ ++ @AfterEach ++ public void tearDown() { ++ pm.clearPlugins(); ++ assertThat(pm.getPermissions(), is(empty())); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/PluginNamingTest.java b/src/test/java/io/papermc/paper/plugin/PluginNamingTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6c3be5b84ae245652261668a52ce49934ef200a9 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/PluginNamingTest.java +@@ -0,0 +1,28 @@ ++package io.papermc.paper.plugin; ++ ++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.Test; ++ ++public class PluginNamingTest { ++ private static final String TEST_NAME = "Test_Plugin"; ++ private static final String TEST_VERSION = "1.0"; ++ ++ private final PaperPluginMeta pluginMeta; ++ ++ public PluginNamingTest() { ++ this.pluginMeta = new PaperPluginMeta(); ++ this.pluginMeta.setName(TEST_NAME); ++ this.pluginMeta.setVersion(TEST_VERSION); ++ } ++ ++ @Test ++ public void testName() { ++ Assertions.assertEquals(TEST_NAME, this.pluginMeta.getName()); ++ } ++ ++ @Test ++ public void testDisplayName() { ++ Assertions.assertEquals(TEST_NAME + " v" + TEST_VERSION, this.pluginMeta.getDisplayName()); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2a7408ed5d9a415333212cadd7fefcd03785afbf +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java +@@ -0,0 +1,42 @@ ++package io.papermc.paper.plugin; ++ ++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; ++import org.bukkit.Bukkit; ++import org.bukkit.event.Event; ++import org.bukkit.event.EventHandler; ++import org.bukkit.event.Listener; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.Test; ++ ++public class SyntheticEventTest { ++ ++ @Test ++ public void test() { ++ PaperTestPlugin paperTestPlugin = new PaperTestPlugin("synthetictest"); ++ PaperPluginManagerImpl paperPluginManager = new PaperPluginManagerImpl(Bukkit.getServer(), null, null); ++ ++ TestEvent event = new TestEvent(false); ++ Impl impl = new Impl(); ++ ++ paperPluginManager.registerEvents(impl, paperTestPlugin); ++ paperPluginManager.callEvent(event); ++ ++ Assertions.assertEquals(1, impl.callCount); ++ } ++ ++ public abstract static class Base<E extends Event> implements Listener { ++ int callCount = 0; ++ ++ public void accept(E evt) { ++ callCount++; ++ } ++ } ++ ++ public static class Impl extends Base<TestEvent> { ++ @Override ++ @EventHandler ++ public void accept(TestEvent evt) { ++ super.accept(evt); ++ } ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/TestEvent.java b/src/test/java/io/papermc/paper/plugin/TestEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..04903794a8ee4dd73162ae240862ff6dc4cb4e24 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/TestEvent.java +@@ -0,0 +1,22 @@ ++package io.papermc.paper.plugin; ++ ++ ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++ ++public class TestEvent extends Event { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ public TestEvent(boolean async) { ++ super(async); ++ } ++ ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..349932b582349c988f3244552b6e6da5dede3d1d +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java +@@ -0,0 +1,83 @@ ++package io.papermc.paper.plugin; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration; ++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; ++import net.kyori.adventure.text.logger.slf4j.ComponentLogger; ++import org.jetbrains.annotations.NotNull; ++ ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.jar.JarFile; ++ ++public class TestJavaPluginProvider implements PluginProvider<PaperTestPlugin> { ++ ++ private final TestPluginMeta testPluginConfiguration; ++ ++ public TestJavaPluginProvider(TestPluginMeta testPluginConfiguration) { ++ this.testPluginConfiguration = testPluginConfiguration; ++ } ++ ++ @Override ++ public @NotNull Path getSource() { ++ return Path.of("dummy"); ++ } ++ ++ @Override ++ public JarFile file() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public PaperTestPlugin createInstance() { ++ return new PaperTestPlugin(this.testPluginConfiguration); ++ } ++ ++ @Override ++ public TestPluginMeta getMeta() { ++ return this.testPluginConfiguration; ++ } ++ ++ @Override ++ public ComponentLogger getLogger() { ++ return ComponentLogger.logger("TestPlugin"); ++ } ++ ++ @Override ++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) { ++ return new LoadOrderConfiguration() { ++ @Override ++ public @NotNull List<String> getLoadBefore() { ++ return TestJavaPluginProvider.this.testPluginConfiguration.getLoadBeforePlugins(); ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadAfter() { ++ List<String> loadAfter = new ArrayList<>(); ++ loadAfter.addAll(TestJavaPluginProvider.this.testPluginConfiguration.getPluginDependencies()); ++ loadAfter.addAll(TestJavaPluginProvider.this.testPluginConfiguration.getPluginSoftDependencies()); ++ return loadAfter; ++ } ++ ++ @Override ++ public @NotNull PluginMeta getMeta() { ++ return TestJavaPluginProvider.this.testPluginConfiguration; ++ } ++ }; ++ } ++ ++ @Override ++ public List<String> validateDependencies(@NotNull DependencyContext context) { ++ List<String> missingDependencies = new ArrayList<>(); ++ for (String hardDependency : this.getMeta().getPluginDependencies()) { ++ if (!context.hasDependency(hardDependency)) { ++ missingDependencies.add(hardDependency); ++ } ++ } ++ ++ return missingDependencies; ++ } ++} +diff --git a/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ba271c35eb2804f94cfc893bf94affb9ae13d3ba +--- /dev/null ++++ b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java +@@ -0,0 +1,114 @@ ++package io.papermc.paper.plugin; ++ ++import io.papermc.paper.plugin.configuration.PluginMeta; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.bukkit.plugin.PluginLoadOrder; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.List; ++ ++public class TestPluginMeta implements PluginMeta { ++ ++ private final String identifier; ++ private List<String> hardDependencies = List.of(); ++ private List<String> softDependencies = List.of(); ++ private List<String> loadBefore = List.of(); ++ ++ public TestPluginMeta(String identifier) { ++ this.identifier = identifier; ++ } ++ ++ @Override ++ public @NotNull String getName() { ++ return this.identifier; ++ } ++ ++ @Override ++ public @NotNull String getMainClass() { ++ return "null"; ++ } ++ ++ @Override ++ public @NotNull PluginLoadOrder getLoadOrder() { ++ return PluginLoadOrder.POSTWORLD; ++ } ++ ++ @Override ++ public @NotNull String getVersion() { ++ return "1.0"; ++ } ++ ++ @Override ++ public @Nullable String getLoggerPrefix() { ++ return this.identifier; ++ } ++ ++ public void setHardDependencies(List<String> hardDependencies) { ++ this.hardDependencies = hardDependencies; ++ } ++ ++ @Override ++ public @NotNull List<String> getPluginDependencies() { ++ return this.hardDependencies; ++ } ++ ++ public void setSoftDependencies(List<String> softDependencies) { ++ this.softDependencies = softDependencies; ++ } ++ ++ @Override ++ public @NotNull List<String> getPluginSoftDependencies() { ++ return this.softDependencies; ++ } ++ ++ public void setLoadBefore(List<String> loadBefore) { ++ this.loadBefore = loadBefore; ++ } ++ ++ @Override ++ public @NotNull List<String> getLoadBeforePlugins() { ++ return this.loadBefore; ++ } ++ ++ @Override ++ public @NotNull List<String> getProvidedPlugins() { ++ return List.of(); ++ } ++ ++ @Override ++ public @NotNull List<String> getAuthors() { ++ return List.of(); ++ } ++ ++ @Override ++ public @NotNull List<String> getContributors() { ++ return List.of(); ++ } ++ ++ @Override ++ public @Nullable String getDescription() { ++ return "null"; ++ } ++ ++ @Override ++ public @Nullable String getWebsite() { ++ return "null"; ++ } ++ ++ @Override ++ public @NotNull List<Permission> getPermissions() { ++ return List.of(); ++ } ++ ++ @Override ++ public @NotNull PermissionDefault getPermissionDefault() { ++ return PermissionDefault.TRUE; ++ } ++ ++ @Override ++ public @NotNull String getAPIVersion() { ++ return "null"; ++ } ++} +diff --git a/src/test/java/org/bukkit/support/DummyServer.java b/src/test/java/org/bukkit/support/DummyServer.java +index b8fe92fc75c611ee1efb82a8ab7089f28bf338ea..c94dae13e8edfdb0adf73a5b3fda4eb1bc97f4bc 100644 +--- a/src/test/java/org/bukkit/support/DummyServer.java ++++ b/src/test/java/org/bukkit/support/DummyServer.java +@@ -54,7 +54,7 @@ public final class DummyServer { + final Thread currentThread = Thread.currentThread(); + when(instance.isPrimaryThread()).thenAnswer(ignored -> Thread.currentThread().equals(currentThread)); + +- final org.bukkit.plugin.PluginManager pluginManager = new org.bukkit.plugin.SimplePluginManager(instance, new org.bukkit.command.SimpleCommandMap(instance)); ++ final org.bukkit.plugin.PluginManager pluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(instance, new org.bukkit.command.SimpleCommandMap(instance), null); + when(instance.getPluginManager()).thenReturn(pluginManager); + when(instance.getTag(anyString(), any(org.bukkit.NamespacedKey.class), any())).thenAnswer(ignored -> new io.papermc.paper.util.EmptyTag()); + // paper end - testing additions diff --git a/patches/server/0020-Plugin-remapping.patch b/patches/server/0020-Plugin-remapping.patch new file mode 100644 index 0000000000..8089ed1b6d --- /dev/null +++ b/patches/server/0020-Plugin-remapping.patch @@ -0,0 +1,1917 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <[email protected]> +Date: Sat, 29 Oct 2022 15:22:32 -0700 +Subject: [PATCH] Plugin remapping + +Co-authored-by: Nassim Jahnke <[email protected]> + +diff --git a/build.gradle.kts b/build.gradle.kts +index a2bb659ae3a502d4c181d1ccbd15eefc38e4823b..8350fc099b68918fb03a21b6a5047ceee72dcbb4 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -46,6 +46,7 @@ dependencies { + testImplementation("org.ow2.asm:asm-tree:9.7") + testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest + implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling ++ implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins + } + + paperweight { +@@ -164,20 +165,41 @@ val runtimeClasspathWithoutVanillaServer = configurations.runtimeClasspath.flatM + runtime.filterNot { it.asFile.absolutePath == vanilla } + } + +-tasks.registerRunTask("runServerJar") { +- description = "Spin up a test server from the serverJar archiveFile" +- classpath(tasks.serverJar.flatMap { it.archiveFile }) ++tasks.registerRunTask("runServer") { ++ description = "Spin up a test server from the Mojang mapped server jar" ++ classpath(tasks.includeMappings.flatMap { it.outputJar }) + classpath(runtimeClasspathWithoutVanillaServer) + } + +-tasks.registerRunTask("runReobf") { ++tasks.registerRunTask("runReobfServer") { + description = "Spin up a test server from the reobfJar output jar" + classpath(tasks.reobfJar.flatMap { it.outputJar }) + classpath(runtimeClasspathWithoutVanillaServer) + } + +-tasks.registerRunTask("runDev") { +- description = "Spin up a non-relocated Mojang-mapped test server" ++tasks.registerRunTask("runDevServer") { ++ description = "Spin up a test server without assembling a jar" + classpath(sourceSets.main.map { it.runtimeClasspath }) + jvmArgs("-DPaper.pushPaperAssetsRoot=true") + } ++ ++tasks.registerRunTask("runBundler") { ++ description = "Spin up a test server from the Mojang mapped bundler jar" ++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreateBundlerJar>("createMojmapBundlerJar").flatMap { it.outputZip }) ++ mainClass.set(null as String?) ++} ++tasks.registerRunTask("runReobfBundler") { ++ description = "Spin up a test server from the reobf bundler jar" ++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreateBundlerJar>("createReobfBundlerJar").flatMap { it.outputZip }) ++ mainClass.set(null as String?) ++} ++tasks.registerRunTask("runPaperclip") { ++ description = "Spin up a test server from the Mojang mapped Paperclip jar" ++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreatePaperclipJar>("createMojmapPaperclipJar").flatMap { it.outputZip }) ++ mainClass.set(null as String?) ++} ++tasks.registerRunTask("runReobfPaperclip") { ++ description = "Spin up a test server from the reobf Paperclip jar" ++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreatePaperclipJar>("createReobfPaperclipJar").flatMap { it.outputZip }) ++ mainClass.set(null as String?) ++} +diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +index 708e5bb9bbf0476fcc2c4b92c6830b094703b43e..6f14cb9a73faa1d0ae2939d08809d9f6c2a99e1d 100644 +--- a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java ++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +@@ -6,10 +6,12 @@ import io.papermc.paper.plugin.entrypoint.Entrypoint; + import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; + import io.papermc.paper.plugin.provider.PluginProvider; + import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; ++import io.papermc.paper.pluginremap.PluginRemapper; ++import java.util.function.Function; + import joptsimple.OptionSet; + import net.minecraft.server.dedicated.DedicatedServer; + import org.bukkit.configuration.file.YamlConfiguration; +-import org.bukkit.craftbukkit.CraftServer; ++import org.bukkit.plugin.java.LibraryLoader; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; + import org.slf4j.Logger; +@@ -25,10 +27,15 @@ public class PluginInitializerManager { + private static PluginInitializerManager impl; + private final Path pluginDirectory; + private final Path updateDirectory; ++ public final io.papermc.paper.pluginremap.@org.checkerframework.checker.nullness.qual.MonotonicNonNull PluginRemapper pluginRemapper; // Paper + + PluginInitializerManager(final Path pluginDirectory, final Path updateDirectory) { + this.pluginDirectory = pluginDirectory; + this.updateDirectory = updateDirectory; ++ this.pluginRemapper = Boolean.getBoolean("paper.disable-plugin-rewriting") ++ ? null ++ : PluginRemapper.create(pluginDirectory); ++ LibraryLoader.REMAPPER = this.pluginRemapper == null ? Function.identity() : this.pluginRemapper::remapLibraries; + } + + private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception { +@@ -96,6 +103,7 @@ public class PluginInitializerManager { + public static void load(OptionSet optionSet) throws Exception { + // We have to load the bukkit configuration inorder to get the update folder location. + io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionSet); ++ if (pluginSystem.pluginRemapper != null) pluginSystem.pluginRemapper.loadingPlugins(); + + // Register the default plugin directory + io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath()); +diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +index f38ecd7f65dc24e4a3f0bc675e3730287ac353f1..f576060c8fe872772bbafe2016fc9b83a3c095f1 100644 +--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java ++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +@@ -1,5 +1,6 @@ + package io.papermc.paper.plugin.loader; + ++import io.papermc.paper.plugin.PluginInitializerManager; + import io.papermc.paper.plugin.bootstrap.PluginProviderContext; + import io.papermc.paper.plugin.loader.library.ClassPathLibrary; + import io.papermc.paper.plugin.loader.library.PaperLibraryStore; +@@ -45,9 +46,12 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder { + } + + List<Path> paths = paperLibraryStore.getPaths(); ++ if (PluginInitializerManager.instance().pluginRemapper != null) { ++ paths = PluginInitializerManager.instance().pluginRemapper.remapLibraries(paths); ++ } + URL[] urls = new URL[paths.size()]; + for (int i = 0; i < paths.size(); i++) { +- Path path = paperLibraryStore.getPaths().get(i); ++ Path path = paths.get(i); + try { + urls[i] = path.toUri().toURL(); + } catch (MalformedURLException e) { +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java +index 226f457db6c1461c943c157b2b91e7450abc9dc6..0846d3a904e470ae1920c5c8be3df9c5dfc3de27 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java +@@ -17,7 +17,7 @@ import org.slf4j.Logger; + public class DirectoryProviderSource implements ProviderSource<Path, List<Path>> { + + public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource(); +- private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("Directory '%s'"::formatted); ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("Directory '%s'"::formatted, false); // Paper - Remap plugins + private static final Logger LOGGER = LogUtils.getClassLogger(); + + @Override +@@ -37,6 +37,11 @@ public class DirectoryProviderSource implements ProviderSource<Path, List<Path>> + LOGGER.error("Error preparing plugin context: " + e.getMessage(), e); + } + }); ++ // Paper start - Remap plugins ++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) { ++ return io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.rewritePluginDirectory(files); ++ } ++ // Paper end - Remap plugins + return files; + } + +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +index 5b58df8df7efca0f67e3a14dd71051dfd7a26079..6b8ed8a0baaf4a57d20e57cec3400af5561ddd79 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +@@ -24,9 +24,15 @@ import java.util.jar.JarFile; + public class FileProviderSource implements ProviderSource<Path, Path> { + + private final Function<Path, String> contextChecker; ++ private final boolean applyRemap; + +- public FileProviderSource(Function<Path, String> contextChecker) { ++ public FileProviderSource(Function<Path, String> contextChecker, boolean applyRemap) { + this.contextChecker = contextChecker; ++ this.applyRemap = applyRemap; ++ } ++ ++ public FileProviderSource(Function<Path, String> contextChecker) { ++ this(contextChecker, true); + } + + @Override +@@ -50,6 +56,11 @@ public class FileProviderSource implements ProviderSource<Path, Path> { + } catch (Exception exception) { + throw new RuntimeException(source + " failed to update!", exception); + } ++ // Paper start - Remap plugins ++ if (this.applyRemap && io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) { ++ context = io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.rewritePlugin(context); ++ } ++ // Paper end - Remap plugins + return context; + } + +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java +index ac55ae0e30119556f01e2e36c20fc63a111fae5f..c2b60c74513544e5d96110c7c3ff80e8f1b686d1 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java +@@ -14,7 +14,7 @@ import java.util.List; + public class PluginFlagProviderSource implements ProviderSource<List<Path>, List<Path>> { + + public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource(); +- private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted); ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted, false); + private static final Logger LOGGER = LogUtils.getClassLogger(); + + @Override +@@ -27,6 +27,11 @@ public class PluginFlagProviderSource implements ProviderSource<List<Path>, List + LOGGER.error("Error preparing plugin context: " + e.getMessage(), e); + } + } ++ // Paper start - Remap plugins ++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null && !files.isEmpty()) { ++ return io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.rewriteExtraPlugins(files); ++ } ++ // Paper end - Remap plugins + return files; + } + +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java +index 87128685015d550440a798028f50be24bc755f6c..8d0da6e46d4eb5eb05c3144510c4ef083559d0ec 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java +@@ -22,9 +22,10 @@ import java.util.jar.JarFile; + */ + public abstract class PluginFileType<T, C extends PluginMeta> { + ++ public static final String PAPER_PLUGIN_YML = "paper-plugin.yml"; + private static final List<String> CONFIG_TYPES = new ArrayList<>(); + +- public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) { ++ public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>(PAPER_PLUGIN_YML, PaperPluginParent.FACTORY) { + @Override + protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) { + PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null; +diff --git a/src/main/java/io/papermc/paper/pluginremap/DebugLogger.java b/src/main/java/io/papermc/paper/pluginremap/DebugLogger.java +new file mode 100644 +index 0000000000000000000000000000000000000000..99e658e3a0f08dbd90b3cf48609613e8a085fd64 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/DebugLogger.java +@@ -0,0 +1,63 @@ ++package io.papermc.paper.pluginremap; ++ ++import java.io.IOException; ++import java.io.PrintWriter; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.function.Consumer; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++/** ++ * {@link PrintWriter}-backed logger implementation for use with {@link net.neoforged.art.api.Renamer} which ++ * only opens the backing writer and logs messages when the {@link PluginRemapper#DEBUG_LOGGING} system property ++ * is set to true. ++ */ ++@DefaultQualifier(NonNull.class) ++final class DebugLogger implements Consumer<String>, AutoCloseable { ++ private final @Nullable PrintWriter writer; ++ ++ DebugLogger(final Path logFile) { ++ try { ++ this.writer = createWriter(logFile); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Failed to initialize DebugLogger for file '" + logFile + "'", ex); ++ } ++ } ++ ++ @Override ++ public void accept(final String line) { ++ this.useWriter(writer -> writer.println(line)); ++ } ++ ++ @Override ++ public void close() { ++ this.useWriter(PrintWriter::close); ++ } ++ ++ private void useWriter(final Consumer<PrintWriter> op) { ++ final @Nullable PrintWriter writer = this.writer; ++ if (writer != null) { ++ op.accept(writer); ++ } ++ } ++ ++ Consumer<String> debug() { ++ return line -> this.accept("[debug]: " + line); ++ } ++ ++ static DebugLogger forOutputFile(final Path outputFile) { ++ return new DebugLogger(outputFile.resolveSibling(outputFile.getFileName() + ".log")); ++ } ++ ++ private static @Nullable PrintWriter createWriter(final Path logFile) throws IOException { ++ if (!PluginRemapper.DEBUG_LOGGING) { ++ return null; ++ } ++ if (!Files.exists(logFile.getParent())) { ++ Files.createDirectories(logFile.getParent()); ++ } ++ return new PrintWriter(logFile.toFile()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/pluginremap/InsertManifestAttribute.java b/src/main/java/io/papermc/paper/pluginremap/InsertManifestAttribute.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d738b31f0005aca352a511c1a57e76b627fca2dd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/InsertManifestAttribute.java +@@ -0,0 +1,69 @@ ++package io.papermc.paper.pluginremap; ++ ++import java.io.ByteArrayInputStream; ++import java.io.ByteArrayOutputStream; ++import java.io.IOException; ++import java.util.Collection; ++import java.util.List; ++import java.util.Set; ++import java.util.jar.Attributes; ++import java.util.jar.Manifest; ++import net.neoforged.art.api.Transformer; ++ ++final class InsertManifestAttribute implements Transformer { ++ static final String PAPERWEIGHT_NAMESPACE_MANIFEST_KEY = "paperweight-mappings-namespace"; ++ static final String MOJANG_NAMESPACE = "mojang"; ++ static final String MOJANG_PLUS_YARN_NAMESPACE = "mojang+yarn"; ++ static final String SPIGOT_NAMESPACE = "spigot"; ++ static final Set<String> KNOWN_NAMESPACES = Set.of(MOJANG_NAMESPACE, MOJANG_PLUS_YARN_NAMESPACE, SPIGOT_NAMESPACE); ++ ++ private final String mainAttributesKey; ++ private final String namespace; ++ private final boolean createIfMissing; ++ private volatile boolean visitedManifest = false; ++ ++ static Transformer addNamespaceManifestAttribute(final String namespace) { ++ return new InsertManifestAttribute(PAPERWEIGHT_NAMESPACE_MANIFEST_KEY, namespace, true); ++ } ++ ++ InsertManifestAttribute( ++ final String mainAttributesKey, ++ final String namespace, ++ final boolean createIfMissing ++ ) { ++ this.mainAttributesKey = mainAttributesKey; ++ this.namespace = namespace; ++ this.createIfMissing = createIfMissing; ++ } ++ ++ @Override ++ public ManifestEntry process(final ManifestEntry entry) { ++ this.visitedManifest = true; ++ try { ++ final Manifest manifest = new Manifest(new ByteArrayInputStream(entry.getData())); ++ manifest.getMainAttributes().putValue(this.mainAttributesKey, this.namespace); ++ final ByteArrayOutputStream out = new ByteArrayOutputStream(); ++ manifest.write(out); ++ return ManifestEntry.create(Entry.STABLE_TIMESTAMP, out.toByteArray()); ++ } catch (final IOException e) { ++ throw new RuntimeException("Failed to modify manifest", e); ++ } ++ } ++ ++ @Override ++ public Collection<? extends Entry> getExtras() { ++ if (!this.visitedManifest && this.createIfMissing) { ++ final Manifest manifest = new Manifest(); ++ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); ++ manifest.getMainAttributes().putValue(this.mainAttributesKey, this.namespace); ++ final ByteArrayOutputStream out = new ByteArrayOutputStream(); ++ try { ++ manifest.write(out); ++ } catch (final IOException e) { ++ throw new RuntimeException("Failed to write manifest", e); ++ } ++ return List.of(ManifestEntry.create(Entry.STABLE_TIMESTAMP, out.toByteArray())); ++ } ++ return Transformer.super.getExtras(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..265c636abfd63f9ba0b0f0198867a10401c14da1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java +@@ -0,0 +1,438 @@ ++package io.papermc.paper.pluginremap; ++ ++import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.provider.type.PluginFileType; ++import io.papermc.paper.util.AtomicFiles; ++import io.papermc.paper.util.MappingEnvironment; ++import io.papermc.paper.util.concurrent.ScalingThreadPool; ++import java.io.BufferedInputStream; ++import java.io.IOException; ++import java.io.InputStream; ++import java.nio.file.FileSystem; ++import java.nio.file.FileSystems; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.Executor; ++import java.util.concurrent.ExecutorService; ++import java.util.concurrent.ThreadPoolExecutor; ++import java.util.concurrent.TimeUnit; ++import java.util.function.Predicate; ++import java.util.function.Supplier; ++import java.util.jar.Manifest; ++import java.util.stream.Stream; ++import net.minecraft.DefaultUncaughtExceptionHandlerWithName; ++import net.minecraft.util.ExceptionCollector; ++import net.neoforged.art.api.Renamer; ++import net.neoforged.art.api.SignatureStripperConfig; ++import net.neoforged.art.api.Transformer; ++import net.neoforged.srgutils.IMappingFile; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.slf4j.Logger; ++ ++import static io.papermc.paper.pluginremap.InsertManifestAttribute.addNamespaceManifestAttribute; ++ ++@DefaultQualifier(NonNull.class) ++public final class PluginRemapper { ++ public static final boolean DEBUG_LOGGING = Boolean.getBoolean("Paper.PluginRemapperDebug"); ++ private static final String PAPER_REMAPPED = ".paper-remapped"; ++ private static final String UNKNOWN_ORIGIN = "unknown-origin"; ++ private static final String LIBRARIES = "libraries"; ++ private static final String EXTRA_PLUGINS = "extra-plugins"; ++ private static final String REMAP_CLASSPATH = "remap-classpath"; ++ private static final String REVERSED_MAPPINGS = "mappings/reversed"; ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private final ExecutorService threadPool; ++ private final ReobfServer reobf; ++ private final RemappedPluginIndex remappedPlugins; ++ private final RemappedPluginIndex extraPlugins; ++ private final UnknownOriginRemappedPluginIndex unknownOrigin; ++ private final UnknownOriginRemappedPluginIndex libraries; ++ private @Nullable CompletableFuture<IMappingFile> reversedMappings; ++ ++ public PluginRemapper(final Path pluginsDir) { ++ this.threadPool = createThreadPool(); ++ final CompletableFuture<IMappingFile> mappings = CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, this.threadPool); ++ final Path remappedPlugins = pluginsDir.resolve(PAPER_REMAPPED); ++ this.reversedMappings = this.reversedMappingsFuture(() -> mappings, remappedPlugins, this.threadPool); ++ this.reobf = new ReobfServer(remappedPlugins.resolve(REMAP_CLASSPATH), mappings, this.threadPool); ++ this.remappedPlugins = new RemappedPluginIndex(remappedPlugins, false); ++ this.extraPlugins = new RemappedPluginIndex(this.remappedPlugins.dir().resolve(EXTRA_PLUGINS), true); ++ this.unknownOrigin = new UnknownOriginRemappedPluginIndex(this.remappedPlugins.dir().resolve(UNKNOWN_ORIGIN)); ++ this.libraries = new UnknownOriginRemappedPluginIndex(this.remappedPlugins.dir().resolve(LIBRARIES)); ++ } ++ ++ public static @Nullable PluginRemapper create(final Path pluginsDir) { ++ if (MappingEnvironment.reobf() || !MappingEnvironment.hasMappings()) { ++ return null; ++ } ++ ++ return new PluginRemapper(pluginsDir); ++ } ++ ++ public void shutdown() { ++ this.threadPool.shutdown(); ++ this.save(true); ++ boolean didShutdown; ++ try { ++ didShutdown = this.threadPool.awaitTermination(3L, TimeUnit.SECONDS); ++ } catch (final InterruptedException ex) { ++ didShutdown = false; ++ } ++ if (!didShutdown) { ++ this.threadPool.shutdownNow(); ++ } ++ } ++ ++ public void save(final boolean clean) { ++ this.remappedPlugins.write(); ++ this.extraPlugins.write(); ++ this.unknownOrigin.write(clean); ++ this.libraries.write(clean); ++ } ++ ++ // Called on startup and reload ++ public void loadingPlugins() { ++ if (this.reversedMappings == null) { ++ this.reversedMappings = this.reversedMappingsFuture( ++ () -> CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, this.threadPool), ++ this.remappedPlugins.dir(), ++ this.threadPool ++ ); ++ } ++ } ++ ++ // Called after all plugins enabled during startup/reload ++ public void pluginsEnabled() { ++ this.reversedMappings = null; ++ this.save(false); ++ } ++ ++ public List<Path> remapLibraries(final List<Path> libraries) { ++ final List<CompletableFuture<Path>> tasks = new ArrayList<>(); ++ for (final Path lib : libraries) { ++ if (!lib.getFileName().toString().endsWith(".jar")) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Library '{}' is not a jar.", lib); ++ } ++ tasks.add(CompletableFuture.completedFuture(lib)); ++ continue; ++ } ++ final @Nullable Path cached = this.libraries.getIfPresent(lib); ++ if (cached != null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Library '{}' has not changed since last remap.", lib); ++ } ++ tasks.add(CompletableFuture.completedFuture(cached)); ++ continue; ++ } ++ tasks.add(this.remapLibrary(this.libraries, lib)); ++ } ++ return waitForAll(tasks); ++ } ++ ++ public Path rewritePlugin(final Path plugin) { ++ // Already remapped ++ if (plugin.getParent().equals(this.remappedPlugins.dir()) ++ || plugin.getParent().equals(this.extraPlugins.dir())) { ++ return plugin; ++ } ++ ++ final @Nullable Path cached = this.unknownOrigin.getIfPresent(plugin); ++ if (cached != null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Plugin '{}' has not changed since last remap.", plugin); ++ } ++ return cached; ++ } ++ ++ return this.remapPlugin(this.unknownOrigin, plugin).join(); ++ } ++ ++ public List<Path> rewriteExtraPlugins(final List<Path> plugins) { ++ final @Nullable List<Path> allCached = this.extraPlugins.getAllIfPresent(plugins); ++ if (allCached != null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("All extra plugins have a remapped variant cached."); ++ } ++ return allCached; ++ } ++ ++ final List<CompletableFuture<Path>> tasks = new ArrayList<>(); ++ for (final Path file : plugins) { ++ final @Nullable Path cached = this.extraPlugins.getIfPresent(file); ++ if (cached != null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Extra plugin '{}' has not changed since last remap.", file); ++ } ++ tasks.add(CompletableFuture.completedFuture(cached)); ++ continue; ++ } ++ tasks.add(this.remapPlugin(this.extraPlugins, file)); ++ } ++ return waitForAll(tasks); ++ } ++ ++ public List<Path> rewritePluginDirectory(final List<Path> jars) { ++ final @Nullable List<Path> remappedJars = this.remappedPlugins.getAllIfPresent(jars); ++ if (remappedJars != null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("All plugins have a remapped variant cached."); ++ } ++ return remappedJars; ++ } ++ ++ final List<CompletableFuture<Path>> tasks = new ArrayList<>(); ++ for (final Path file : jars) { ++ final @Nullable Path existingFile = this.remappedPlugins.getIfPresent(file); ++ if (existingFile != null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Plugin '{}' has not changed since last remap.", file); ++ } ++ tasks.add(CompletableFuture.completedFuture(existingFile)); ++ continue; ++ } ++ ++ tasks.add(this.remapPlugin(this.remappedPlugins, file)); ++ } ++ return waitForAll(tasks); ++ } ++ ++ private static IMappingFile reverse(final IMappingFile mappings) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Reversing mappings..."); ++ } ++ final long start = System.currentTimeMillis(); ++ final IMappingFile reversed = mappings.reverse(); ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Done reversing mappings in {}ms.", System.currentTimeMillis() - start); ++ } ++ return reversed; ++ } ++ ++ private CompletableFuture<IMappingFile> reversedMappingsFuture( ++ final Supplier<CompletableFuture<IMappingFile>> mappingsFuture, ++ final Path remappedPlugins, ++ final Executor executor ++ ) { ++ return CompletableFuture.supplyAsync(() -> { ++ try { ++ final String mappingsHash = MappingEnvironment.mappingsHash(); ++ final String fName = mappingsHash + ".tiny"; ++ final Path reversedMappings1 = remappedPlugins.resolve(REVERSED_MAPPINGS); ++ final Path file = reversedMappings1.resolve(fName); ++ if (Files.isDirectory(reversedMappings1)) { ++ if (Files.isRegularFile(file)) { ++ return CompletableFuture.completedFuture( ++ loadMappings("Reversed", Files.newInputStream(file)) ++ ); ++ } else { ++ for (final Path oldFile : list(reversedMappings1, Files::isRegularFile)) { ++ Files.delete(oldFile); ++ } ++ } ++ } else { ++ Files.createDirectories(reversedMappings1); ++ } ++ return mappingsFuture.get().thenApply(loadedMappings -> { ++ final IMappingFile reversed = reverse(loadedMappings); ++ try { ++ AtomicFiles.atomicWrite(file, writeTo -> { ++ reversed.write(writeTo, IMappingFile.Format.TINY, false); ++ }); ++ } catch (final IOException e) { ++ throw new RuntimeException("Failed to write reversed mappings", e); ++ } ++ return reversed; ++ }); ++ } catch (final IOException e) { ++ throw new RuntimeException("Failed to load reversed mappings", e); ++ } ++ }, executor).thenCompose(f -> f); ++ } ++ ++ private CompletableFuture<Path> remapPlugin( ++ final RemappedPluginIndex index, ++ final Path inputFile ++ ) { ++ return this.remap(index, inputFile, false); ++ } ++ ++ private CompletableFuture<Path> remapLibrary( ++ final RemappedPluginIndex index, ++ final Path inputFile ++ ) { ++ return this.remap(index, inputFile, true); ++ } ++ ++ /** ++ * Returns the remapped file if remapping was necessary, otherwise null. ++ * ++ * @param index remapped plugin index ++ * @param inputFile input file ++ * @return remapped file, or inputFile if no remapping was necessary ++ */ ++ private CompletableFuture<Path> remap( ++ final RemappedPluginIndex index, ++ final Path inputFile, ++ final boolean library ++ ) { ++ final Path destination = index.input(inputFile); ++ ++ try (final FileSystem fs = FileSystems.newFileSystem(inputFile, new HashMap<>())) { ++ // Leave dummy files if no remapping is required, so that we can check if they exist without copying the whole file ++ final Path manifestPath = fs.getPath("META-INF/MANIFEST.MF"); ++ final @Nullable String ns; ++ if (Files.exists(manifestPath)) { ++ final Manifest manifest; ++ try (final InputStream in = new BufferedInputStream(Files.newInputStream(manifestPath))) { ++ manifest = new Manifest(in); ++ } ++ ns = manifest.getMainAttributes().getValue(InsertManifestAttribute.PAPERWEIGHT_NAMESPACE_MANIFEST_KEY); ++ } else { ++ ns = null; ++ } ++ ++ if (ns != null && !InsertManifestAttribute.KNOWN_NAMESPACES.contains(ns)) { ++ throw new RuntimeException("Failed to remap plugin " + inputFile + " with unknown mapping namespace '" + ns + "'"); ++ } ++ ++ final boolean mojangMappedManifest = ns != null && (ns.equals(InsertManifestAttribute.MOJANG_NAMESPACE) || ns.equals(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE)); ++ if (library) { ++ if (mojangMappedManifest) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Library '{}' is already Mojang mapped.", inputFile); ++ } ++ index.skip(inputFile); ++ return CompletableFuture.completedFuture(inputFile); ++ } else if (ns == null) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Library '{}' does not specify a mappings namespace (not remapping).", inputFile); ++ } ++ index.skip(inputFile); ++ return CompletableFuture.completedFuture(inputFile); ++ } ++ } else { ++ if (mojangMappedManifest) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Plugin '{}' is already Mojang mapped.", inputFile); ++ } ++ index.skip(inputFile); ++ return CompletableFuture.completedFuture(inputFile); ++ } else if (ns == null && Files.exists(fs.getPath(PluginFileType.PAPER_PLUGIN_YML))) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Plugin '{}' is a Paper plugin with no namespace specified.", inputFile); ++ } ++ index.skip(inputFile); ++ return CompletableFuture.completedFuture(inputFile); ++ } ++ } ++ } catch (final IOException ex) { ++ throw new RuntimeException("Failed to open plugin jar " + inputFile, ex); ++ } ++ ++ return this.reobf.remapped().thenApplyAsync(reobfServer -> { ++ LOGGER.info("Remapping {} '{}'...", library ? "library" : "plugin", inputFile); ++ final long start = System.currentTimeMillis(); ++ try (final DebugLogger logger = DebugLogger.forOutputFile(destination)) { ++ try (final Renamer renamer = Renamer.builder() ++ .add(Transformer.renamerFactory(this.mappings(), false)) ++ .add(addNamespaceManifestAttribute(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE)) ++ .add(Transformer.signatureStripperFactory(SignatureStripperConfig.ALL)) ++ .lib(reobfServer.toFile()) ++ .threads(1) ++ .logger(logger) ++ .debug(logger.debug()) ++ .build()) { ++ renamer.run(inputFile.toFile(), destination.toFile()); ++ } ++ } catch (final Exception ex) { ++ throw new RuntimeException("Failed to remap plugin jar '" + inputFile + "'", ex); ++ } ++ LOGGER.info("Done remapping {} '{}' in {}ms.", library ? "library" : "plugin", inputFile, System.currentTimeMillis() - start); ++ return destination; ++ }, this.threadPool); ++ } ++ ++ private IMappingFile mappings() { ++ final @Nullable CompletableFuture<IMappingFile> mappings = this.reversedMappings; ++ if (mappings == null) { ++ return this.reversedMappingsFuture( ++ () -> CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, Runnable::run), ++ this.remappedPlugins.dir(), ++ Runnable::run ++ ).join(); ++ } ++ return mappings.join(); ++ } ++ ++ private static IMappingFile loadReobfMappings() { ++ return loadMappings("Reobf", MappingEnvironment.mappingsStream()); ++ } ++ ++ private static IMappingFile loadMappings(final String name, final InputStream stream) { ++ try (stream) { ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Loading {} mappings...", name); ++ } ++ final long start = System.currentTimeMillis(); ++ final IMappingFile load = IMappingFile.load(stream); ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Done loading {} mappings in {}ms.", name, System.currentTimeMillis() - start); ++ } ++ return load; ++ } catch (final IOException ex) { ++ throw new RuntimeException("Failed to load " + name + " mappings", ex); ++ } ++ } ++ ++ static List<Path> list(final Path dir, final Predicate<Path> filter) { ++ try (final Stream<Path> stream = Files.list(dir)) { ++ return stream.filter(filter).toList(); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Failed to list directory '" + dir + "'", ex); ++ } ++ } ++ ++ private static List<Path> waitForAll(final List<CompletableFuture<Path>> tasks) { ++ final ExceptionCollector<Exception> collector = new ExceptionCollector<>(); ++ final List<Path> ret = new ArrayList<>(); ++ for (final CompletableFuture<Path> task : tasks) { ++ try { ++ ret.add(task.join()); ++ } catch (final CompletionException ex) { ++ collector.add(ex); ++ } ++ } ++ try { ++ collector.throwIfPresent(); ++ } catch (final Exception ex) { ++ // Don't hard fail during bootstrap/plugin loading. The plugin(s) in question will be skipped ++ LOGGER.error("Encountered exception remapping plugins", ex); ++ } ++ return ret; ++ } ++ ++ private static ThreadPoolExecutor createThreadPool() { ++ return new ThreadPoolExecutor( ++ 0, ++ 4, ++ 5L, ++ TimeUnit.SECONDS, ++ ScalingThreadPool.createUnboundedQueue(), ++ new ThreadFactoryBuilder() ++ .setNameFormat("Paper Plugin Remapper Thread - %1$d") ++ .setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(LOGGER)) ++ .build(), ++ ScalingThreadPool.defaultReEnqueuePolicy() ++ ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/pluginremap/RemappedPluginIndex.java b/src/main/java/io/papermc/paper/pluginremap/RemappedPluginIndex.java +new file mode 100644 +index 0000000000000000000000000000000000000000..86fc60452404d1f4609c25a90c4803ffb80dc8ab +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/RemappedPluginIndex.java +@@ -0,0 +1,212 @@ ++package io.papermc.paper.pluginremap; ++ ++import com.google.gson.Gson; ++import com.google.gson.GsonBuilder; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.Hashing; ++import io.papermc.paper.util.MappingEnvironment; ++import java.io.BufferedReader; ++import java.io.BufferedWriter; ++import java.io.IOException; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.function.Function; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.loader.AtomicFiles; ++ ++@DefaultQualifier(NonNull.class) ++class RemappedPluginIndex { ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final Gson GSON = new GsonBuilder() ++ .setPrettyPrinting() ++ .create(); ++ private static final String INDEX_FILE_NAME = "index.json"; ++ ++ protected final State state; ++ private final Path dir; ++ private final Path indexFile; ++ private final boolean handleDuplicateFileNames; ++ ++ // todo maybe hash remapped variants to ensure they haven't changed? probably unneeded ++ static final class State { ++ final Map<String, String> hashes = new HashMap<>(); ++ final Set<String> skippedHashes = new HashSet<>(); ++ private final String mappingsHash = MappingEnvironment.mappingsHash(); ++ } ++ ++ RemappedPluginIndex(final Path dir, final boolean handleDuplicateFileNames) { ++ this.dir = dir; ++ this.handleDuplicateFileNames = handleDuplicateFileNames; ++ if (!Files.exists(this.dir)) { ++ try { ++ Files.createDirectories(this.dir); ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ } ++ ++ this.indexFile = dir.resolve(INDEX_FILE_NAME); ++ if (Files.isRegularFile(this.indexFile)) { ++ try { ++ this.state = this.readIndex(); ++ } catch (final IOException e) { ++ throw new RuntimeException(e); ++ } ++ } else { ++ this.state = new State(); ++ } ++ } ++ ++ private State readIndex() throws IOException { ++ final State state; ++ try (final BufferedReader reader = Files.newBufferedReader(this.indexFile)) { ++ state = GSON.fromJson(reader, State.class); ++ } ++ ++ // If mappings have changed, delete all cached files and create a new index ++ if (!state.mappingsHash.equals(MappingEnvironment.mappingsHash())) { ++ for (final String fileName : state.hashes.values()) { ++ Files.deleteIfExists(this.dir.resolve(fileName)); ++ } ++ return new State(); ++ } ++ return state; ++ } ++ ++ Path dir() { ++ return this.dir; ++ } ++ ++ /** ++ * Returns a list of cached paths if all of the input paths are present in the cache. ++ * The returned list may contain paths from different directories. ++ * ++ * @param paths plugin jar paths to check ++ * @return null if any of the paths are not present in the cache, otherwise a list of the cached paths ++ */ ++ @Nullable List<Path> getAllIfPresent(final List<Path> paths) { ++ final Map<Path, String> hashCache = new HashMap<>(); ++ final Function<Path, String> inputFileHash = path -> hashCache.computeIfAbsent(path, Hashing::sha256); ++ ++ // Delete cached entries we no longer need ++ final Iterator<Map.Entry<String, String>> iterator = this.state.hashes.entrySet().iterator(); ++ while (iterator.hasNext()) { ++ final Map.Entry<String, String> entry = iterator.next(); ++ final String inputHash = entry.getKey(); ++ final String fileName = entry.getValue(); ++ if (paths.stream().anyMatch(path -> inputFileHash.apply(path).equals(inputHash))) { ++ // Hash is used, keep it ++ continue; ++ } ++ ++ iterator.remove(); ++ try { ++ Files.deleteIfExists(this.dir.resolve(fileName)); ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ } ++ ++ // Also clear hashes of skipped files ++ this.state.skippedHashes.removeIf(hash -> paths.stream().noneMatch(path -> inputFileHash.apply(path).equals(hash))); ++ ++ final List<Path> ret = new ArrayList<>(); ++ for (final Path path : paths) { ++ final String inputHash = inputFileHash.apply(path); ++ if (this.state.skippedHashes.contains(inputHash)) { ++ // Add the original path ++ ret.add(path); ++ continue; ++ } ++ ++ final @Nullable Path cached = this.getIfPresent(inputHash); ++ if (cached == null) { ++ // Missing the remapped file ++ return null; ++ } ++ ret.add(cached); ++ } ++ return ret; ++ } ++ ++ private String createCachedFileName(final Path in) { ++ if (this.handleDuplicateFileNames) { ++ final String fileName = in.getFileName().toString(); ++ final int i = fileName.lastIndexOf(".jar"); ++ return fileName.substring(0, i) + "-" + System.currentTimeMillis() + ".jar"; ++ } ++ return in.getFileName().toString(); ++ } ++ ++ /** ++ * Returns the given path if the file was previously skipped for being remapped, otherwise the cached path or null. ++ * ++ * @param in input file ++ * @return {@code in} if already remapped, the cached path if present, otherwise null ++ */ ++ @Nullable Path getIfPresent(final Path in) { ++ final String inHash = Hashing.sha256(in); ++ if (this.state.skippedHashes.contains(inHash)) { ++ return in; ++ } ++ return this.getIfPresent(inHash); ++ } ++ ++ /** ++ * Returns the cached path if a remapped file is present for the given hash, otherwise null. ++ * ++ * @param inHash hash of the input file ++ * @return the cached path if present, otherwise null ++ * @see #getIfPresent(Path) ++ */ ++ protected @Nullable Path getIfPresent(final String inHash) { ++ final @Nullable String fileName = this.state.hashes.get(inHash); ++ if (fileName == null) { ++ return null; ++ } ++ ++ final Path path = this.dir.resolve(fileName); ++ if (Files.exists(path)) { ++ return path; ++ } ++ return null; ++ } ++ ++ Path input(final Path in) { ++ return this.input(in, Hashing.sha256(in)); ++ } ++ ++ /** ++ * Marks the given file as skipped for remapping. ++ * ++ * @param in input file ++ */ ++ void skip(final Path in) { ++ this.state.skippedHashes.add(Hashing.sha256(in)); ++ } ++ ++ protected Path input(final Path in, final String hashString) { ++ final String name = this.createCachedFileName(in); ++ this.state.hashes.put(hashString, name); ++ return this.dir.resolve(name); ++ } ++ ++ void write() { ++ try (final BufferedWriter writer = AtomicFiles.atomicBufferedWriter(this.indexFile, StandardCharsets.UTF_8)) { ++ GSON.toJson(this.state, writer); ++ } catch (final IOException ex) { ++ LOGGER.warn("Failed to write index file '{}'", this.indexFile, ex); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/pluginremap/ReobfServer.java b/src/main/java/io/papermc/paper/pluginremap/ReobfServer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aa5bf7ae042f3d43f7612d027ebef0e5c758ffc9 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/ReobfServer.java +@@ -0,0 +1,92 @@ ++package io.papermc.paper.pluginremap; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.AtomicFiles; ++import io.papermc.paper.util.MappingEnvironment; ++import java.io.IOException; ++import java.net.URISyntaxException; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.Executor; ++import net.neoforged.art.api.Renamer; ++import net.neoforged.art.api.Transformer; ++import net.neoforged.art.internal.RenamerImpl; ++import net.neoforged.srgutils.IMappingFile; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.slf4j.Logger; ++ ++import static io.papermc.paper.pluginremap.InsertManifestAttribute.addNamespaceManifestAttribute; ++ ++@DefaultQualifier(NonNull.class) ++final class ReobfServer { ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private final Path remapClasspathDir; ++ private final CompletableFuture<Void> load; ++ ++ ReobfServer(final Path remapClasspathDir, final CompletableFuture<IMappingFile> mappings, final Executor executor) { ++ this.remapClasspathDir = remapClasspathDir; ++ if (this.mappingsChanged()) { ++ this.load = mappings.thenAcceptAsync(this::remap, executor); ++ } else { ++ if (PluginRemapper.DEBUG_LOGGING) { ++ LOGGER.info("Have cached reobf server for current mappings."); ++ } ++ this.load = CompletableFuture.completedFuture(null); ++ } ++ } ++ ++ CompletableFuture<Path> remapped() { ++ return this.load.thenApply($ -> this.remappedPath()); ++ } ++ ++ private Path remappedPath() { ++ return this.remapClasspathDir.resolve(MappingEnvironment.mappingsHash() + ".jar"); ++ } ++ ++ private boolean mappingsChanged() { ++ return !Files.exists(this.remappedPath()); ++ } ++ ++ private void remap(final IMappingFile mappings) { ++ try { ++ if (!Files.exists(this.remapClasspathDir)) { ++ Files.createDirectories(this.remapClasspathDir); ++ } ++ for (final Path file : PluginRemapper.list(this.remapClasspathDir, Files::isRegularFile)) { ++ Files.delete(file); ++ } ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ ++ LOGGER.info("Remapping server..."); ++ final long startRemap = System.currentTimeMillis(); ++ try (final DebugLogger log = DebugLogger.forOutputFile(this.remappedPath())) { ++ AtomicFiles.atomicWrite(this.remappedPath(), writeTo -> { ++ try (final RenamerImpl renamer = (RenamerImpl) Renamer.builder() ++ .logger(log) ++ .debug(log.debug()) ++ .threads(1) ++ .add(Transformer.renamerFactory(mappings, false)) ++ .add(addNamespaceManifestAttribute(InsertManifestAttribute.SPIGOT_NAMESPACE)) ++ .build()) { ++ renamer.run(serverJar().toFile(), writeTo.toFile(), true); ++ } ++ }); ++ } catch (final Exception ex) { ++ throw new RuntimeException("Failed to remap server jar", ex); ++ } ++ LOGGER.info("Done remapping server in {}ms.", System.currentTimeMillis() - startRemap); ++ } ++ ++ private static Path serverJar() { ++ try { ++ return Path.of(ReobfServer.class.getProtectionDomain().getCodeSource().getLocation().toURI()); ++ } catch (final URISyntaxException ex) { ++ throw new RuntimeException(ex); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/pluginremap/UnknownOriginRemappedPluginIndex.java b/src/main/java/io/papermc/paper/pluginremap/UnknownOriginRemappedPluginIndex.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ad53aab4fee16b76f6e4fd44e4b28d06fef80de4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/UnknownOriginRemappedPluginIndex.java +@@ -0,0 +1,72 @@ ++package io.papermc.paper.pluginremap; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.Hashing; ++import java.io.IOException; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.Set; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.slf4j.Logger; ++ ++@DefaultQualifier(NonNull.class) ++final class UnknownOriginRemappedPluginIndex extends RemappedPluginIndex { ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private final Set<String> used = new HashSet<>(); ++ ++ UnknownOriginRemappedPluginIndex(final Path dir) { ++ super(dir, true); ++ } ++ ++ @Override ++ @Nullable Path getIfPresent(final Path in) { ++ final String hash = Hashing.sha256(in); ++ if (this.state.skippedHashes.contains(hash)) { ++ return in; ++ } ++ ++ final @Nullable Path path = super.getIfPresent(hash); ++ if (path != null) { ++ this.used.add(hash); ++ } ++ return path; ++ } ++ ++ @Override ++ Path input(final Path in) { ++ final String hash = Hashing.sha256(in); ++ this.used.add(hash); ++ return super.input(in, hash); ++ } ++ ++ void write(final boolean clean) { ++ if (!clean) { ++ super.write(); ++ return; ++ } ++ ++ final Iterator<Map.Entry<String, String>> it = this.state.hashes.entrySet().iterator(); ++ while (it.hasNext()) { ++ final Map.Entry<String, String> next = it.next(); ++ if (this.used.contains(next.getKey())) { ++ continue; ++ } ++ ++ // Remove unused mapped file ++ it.remove(); ++ final Path file = this.dir().resolve(next.getValue()); ++ try { ++ Files.deleteIfExists(file); ++ } catch (final IOException ex) { ++ LOGGER.warn("Failed to delete no longer needed cached jar '{}'", file, ex); ++ } ++ } ++ super.write(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/AtomicFiles.java b/src/main/java/io/papermc/paper/util/AtomicFiles.java +new file mode 100644 +index 0000000000000000000000000000000000000000..944250d2b8e1969f221b2f24cce7b1019c55fe01 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/AtomicFiles.java +@@ -0,0 +1,96 @@ ++package io.papermc.paper.util; ++ ++import java.io.IOException; ++import java.nio.file.AccessDeniedException; ++import java.nio.file.AtomicMoveNotSupportedException; ++import java.nio.file.CopyOption; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.util.concurrent.ThreadLocalRandom; ++import java.util.function.Consumer; ++import org.spongepowered.configurate.util.CheckedConsumer; ++ ++// Stripped down version of https://github.com/jpenilla/squaremap/blob/7d7994b4096e5fc61364ea2d87e9aa4e14edf5c6/common/src/main/java/xyz/jpenilla/squaremap/common/util/FileUtil.java ++public final class AtomicFiles { ++ ++ private AtomicFiles() { ++ } ++ ++ public static void atomicWrite(final Path path, final CheckedConsumer<Path, IOException> op) throws IOException { ++ final Path tmp = tempFile(path); ++ ++ try { ++ op.accept(tmp); ++ atomicMove(tmp, path, true); ++ } catch (final IOException ex) { ++ try { ++ Files.deleteIfExists(tmp); ++ } catch (final IOException ex1) { ++ ex.addSuppressed(ex1); ++ } ++ throw ex; ++ } ++ } ++ ++ private static Path tempFile(final Path path) { ++ return path.resolveSibling("." + System.nanoTime() + "-" + ThreadLocalRandom.current().nextInt() + "-" + path.getFileName().toString() + ".tmp"); } ++ ++ @SuppressWarnings("BusyWait") // not busy waiting ++ public static void atomicMove(final Path from, final Path to, final boolean replaceExisting) throws IOException { ++ final int maxRetries = 2; ++ ++ try { ++ atomicMoveIfPossible(from, to, replaceExisting); ++ } catch (final AccessDeniedException ex) { ++ // Sometimes because of file locking this will fail... Let's just try again and hope for the best ++ // Thanks Windows! ++ int retries = 1; ++ while (true) { ++ try { ++ // Pause for a bit ++ Thread.sleep(10L * retries); ++ atomicMoveIfPossible(from, to, replaceExisting); ++ break; // success ++ } catch (final AccessDeniedException ex1) { ++ ex.addSuppressed(ex1); ++ if (retries == maxRetries) { ++ throw ex; ++ } ++ } catch (final InterruptedException interruptedException) { ++ ex.addSuppressed(interruptedException); ++ Thread.currentThread().interrupt(); ++ throw ex; ++ } ++ ++retries; ++ } ++ } ++ } ++ ++ private static void atomicMoveIfPossible(final Path from, final Path to, final boolean replaceExisting) throws IOException { ++ final CopyOption[] options = replaceExisting ++ ? new CopyOption[]{StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING} ++ : new CopyOption[]{StandardCopyOption.ATOMIC_MOVE}; ++ ++ try { ++ Files.move(from, to, options); ++ } catch (final AtomicMoveNotSupportedException ex) { ++ Files.move(from, to, replaceExisting ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{}); ++ } ++ } ++ ++ private static <T, X extends Throwable> Consumer<T> sneaky(final CheckedConsumer<T, X> consumer) { ++ return t -> { ++ try { ++ consumer.accept(t); ++ } catch (final Throwable thr) { ++ rethrow(thr); ++ } ++ }; ++ } ++ ++ @SuppressWarnings("unchecked") ++ private static <X extends Throwable> RuntimeException rethrow(final Throwable t) throws X { ++ throw (X) t; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/Hashing.java b/src/main/java/io/papermc/paper/util/Hashing.java +new file mode 100644 +index 0000000000000000000000000000000000000000..54c1324faa190a06bfa2b3b8f86928b4c51a57f8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/Hashing.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.util; ++ ++import com.google.common.hash.HashCode; ++import java.io.IOException; ++import java.io.InputStream; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.Locale; ++import org.apache.commons.io.IOUtils; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class Hashing { ++ private Hashing() { ++ } ++ ++ /** ++ * Hash the provided {@link InputStream} using SHA-256. Stream will be closed. ++ * ++ * @param stream input stream ++ * @return SHA-256 hash string ++ */ ++ public static String sha256(final InputStream stream) { ++ try (stream) { ++ return com.google.common.hash.Hashing.sha256().hashBytes(IOUtils.toByteArray(stream)).toString().toUpperCase(Locale.ROOT); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Failed to take hash of InputStream", ex); ++ } ++ } ++ ++ /** ++ * Hash the provided file using SHA-256. ++ * ++ * @param file file ++ * @return SHA-256 hash string ++ */ ++ public static String sha256(final Path file) { ++ if (!Files.isRegularFile(file)) { ++ throw new IllegalArgumentException("'" + file + "' is not a regular file!"); ++ } ++ final HashCode hash; ++ try { ++ hash = com.google.common.io.Files.asByteSource(file.toFile()).hash(com.google.common.hash.Hashing.sha256()); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Failed to take hash of file '" + file + "'", ex); ++ } ++ return hash.toString().toUpperCase(Locale.ROOT); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/MappingEnvironment.java b/src/main/java/io/papermc/paper/util/MappingEnvironment.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e4229634d41a42b3d93948eebb77def7c0c72b1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/MappingEnvironment.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.util; ++ ++import java.io.InputStream; ++import java.util.Objects; ++import java.util.jar.Manifest; ++import net.minecraft.world.entity.MobCategory; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class MappingEnvironment { ++ private static final @Nullable String MAPPINGS_HASH = readMappingsHash(); ++ private static final boolean REOBF = checkReobf(); ++ ++ private MappingEnvironment() { ++ } ++ ++ public static boolean reobf() { ++ return REOBF; ++ } ++ ++ public static boolean hasMappings() { ++ return MAPPINGS_HASH != null; ++ } ++ ++ public static InputStream mappingsStream() { ++ return Objects.requireNonNull(mappingsStreamIfPresent(), "Missing mappings!"); ++ } ++ ++ public static @Nullable InputStream mappingsStreamIfPresent() { ++ return MappingEnvironment.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny"); ++ } ++ ++ public static String mappingsHash() { ++ return Objects.requireNonNull(MAPPINGS_HASH, "MAPPINGS_HASH"); ++ } ++ ++ private static @Nullable String readMappingsHash() { ++ final @Nullable Manifest manifest = JarManifests.manifest(MappingEnvironment.class); ++ if (manifest != null) { ++ final Object hash = manifest.getMainAttributes().getValue("Included-Mappings-Hash"); ++ if (hash != null) { ++ return hash.toString(); ++ } ++ } ++ ++ final @Nullable InputStream stream = mappingsStreamIfPresent(); ++ if (stream == null) { ++ return null; ++ } ++ return Hashing.sha256(stream); ++ } ++ ++ @SuppressWarnings("ConstantConditions") ++ private static boolean checkReobf() { ++ final Class<?> clazz = MobCategory.class; ++ if (clazz.getSimpleName().equals("MobCategory")) { ++ return false; ++ } else if (clazz.getSimpleName().equals("EnumCreatureType")) { ++ return true; ++ } ++ throw new IllegalStateException(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/ObfHelper.java b/src/main/java/io/papermc/paper/util/ObfHelper.java +index 9e6d48335b37fa5204bfebf396d748089884555b..6067be951c4c52c4b1da51efc01436b2c90ea3bf 100644 +--- a/src/main/java/io/papermc/paper/util/ObfHelper.java ++++ b/src/main/java/io/papermc/paper/util/ObfHelper.java +@@ -80,10 +80,10 @@ public enum ObfHelper { + } + + private static @Nullable Set<ClassMapping> loadMappingsIfPresent() { +- try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) { +- if (mappingsInputStream == null) { +- return null; +- } ++ if (!MappingEnvironment.hasMappings()) { ++ return null; ++ } ++ try (final InputStream mappingsInputStream = MappingEnvironment.mappingsStream()) { + final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot + final Set<ClassMapping> classes = new HashSet<>(); + +diff --git a/src/main/java/io/papermc/paper/util/concurrent/ScalingThreadPool.java b/src/main/java/io/papermc/paper/util/concurrent/ScalingThreadPool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..badff5d6ae6dd8d209c82bc7e8afe370db6148f2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/concurrent/ScalingThreadPool.java +@@ -0,0 +1,85 @@ ++package io.papermc.paper.util.concurrent; ++ ++import java.util.concurrent.BlockingQueue; ++import java.util.concurrent.LinkedBlockingQueue; ++import java.util.concurrent.RejectedExecutionHandler; ++import java.util.concurrent.ThreadPoolExecutor; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++/** ++ * Utilities for scaling thread pools. ++ * ++ * @see <a href="https://medium.com/@uditharosha/java-scale-first-executorservice-4245a63222df">Java Scale First ExecutorService — A myth or a reality</a> ++ */ ++public final class ScalingThreadPool { ++ private ScalingThreadPool() { ++ } ++ ++ public static RejectedExecutionHandler defaultReEnqueuePolicy() { ++ return reEnqueuePolicy(new ThreadPoolExecutor.AbortPolicy()); ++ } ++ ++ public static RejectedExecutionHandler reEnqueuePolicy(final RejectedExecutionHandler original) { ++ return new ReEnqueuePolicy(original); ++ } ++ ++ public static <E> BlockingQueue<E> createUnboundedQueue() { ++ return new Queue<>(); ++ } ++ ++ public static <E> BlockingQueue<E> createQueue(final int capacity) { ++ return new Queue<>(capacity); ++ } ++ ++ private static final class Queue<E> extends LinkedBlockingQueue<E> { ++ private final AtomicInteger idleThreads = new AtomicInteger(0); ++ ++ private Queue() { ++ super(); ++ } ++ ++ private Queue(final int capacity) { ++ super(capacity); ++ } ++ ++ @Override ++ public boolean offer(final E e) { ++ return this.idleThreads.get() > 0 && super.offer(e); ++ } ++ ++ @Override ++ public E take() throws InterruptedException { ++ this.idleThreads.incrementAndGet(); ++ try { ++ return super.take(); ++ } finally { ++ this.idleThreads.decrementAndGet(); ++ } ++ } ++ ++ @Override ++ public E poll(final long timeout, final TimeUnit unit) throws InterruptedException { ++ this.idleThreads.incrementAndGet(); ++ try { ++ return super.poll(timeout, unit); ++ } finally { ++ this.idleThreads.decrementAndGet(); ++ } ++ } ++ ++ @Override ++ public boolean add(final E e) { ++ return super.offer(e); ++ } ++ } ++ ++ private record ReEnqueuePolicy(RejectedExecutionHandler originalHandler) implements RejectedExecutionHandler { ++ @Override ++ public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { ++ if (!executor.getQueue().add(r)) { ++ this.originalHandler.rejectedExecution(r, executor); ++ } ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index e82218de79759dfaa7fe2a5a78e150f135bf01a3..202a6510d9d093119ff88b910cef6e47fce2e6b8 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -643,6 +643,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + + this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD); ++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins + this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP)); + this.connection.acceptConnections(); + } +@@ -916,6 +917,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.server.disablePlugins(); + } + // CraftBukkit end ++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.shutdown(); // Paper - Plugin remapping + this.getConnection().stop(); + this.isSaving = true; + if (this.playerList != null) { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 6ba90739c20995362a5275e2259cac9e17fbcf59..d38ecbc208c34509eaf77751ac45d9ef51a5dce8 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -319,6 +319,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + } + ++ // Paper start ++ public java.io.File getPluginsFolder() { ++ return (java.io.File) this.options.valueOf("plugins"); ++ } ++ // Paper end ++ + @Override + public boolean isSpawningAnimals() { + return this.getProperties().spawnAnimals && super.isSpawningAnimals(); +diff --git a/src/main/java/net/neoforged/art/internal/RenamerImpl.java b/src/main/java/net/neoforged/art/internal/RenamerImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..73b20a92f330311e3fef8f03b51a098513afafc1 +--- /dev/null ++++ b/src/main/java/net/neoforged/art/internal/RenamerImpl.java +@@ -0,0 +1,308 @@ ++/* ++ * Forge Auto Renaming Tool ++ * Copyright (c) 2021 ++ * ++ * This library is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation version 2.1 ++ * of the License. ++ * ++ * This library 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 ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library; if not, write to the Free Software ++ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++ */ ++ ++package net.neoforged.art.internal; ++ ++import java.io.BufferedOutputStream; ++import java.io.ByteArrayOutputStream; ++import java.io.File; ++import java.io.IOException; ++import java.io.InputStream; ++import java.io.OutputStream; ++import java.nio.file.Files; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.Enumeration; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Objects; ++import java.util.Set; ++import java.util.function.Consumer; ++import java.util.stream.Collectors; ++import java.util.zip.ZipEntry; ++import java.util.zip.ZipFile; ++import java.util.zip.ZipOutputStream; ++ ++import net.neoforged.cliutils.JarUtils; ++import net.neoforged.cliutils.progress.ProgressReporter; ++import org.objectweb.asm.Opcodes; ++ ++import net.neoforged.art.api.ClassProvider; ++import net.neoforged.art.api.Renamer; ++import net.neoforged.art.api.Transformer; ++import net.neoforged.art.api.Transformer.ClassEntry; ++import net.neoforged.art.api.Transformer.Entry; ++import net.neoforged.art.api.Transformer.ManifestEntry; ++import net.neoforged.art.api.Transformer.ResourceEntry; ++ ++public class RenamerImpl implements Renamer { // Paper - public ++ private static final ProgressReporter PROGRESS = ProgressReporter.getDefault(); ++ static final int MAX_ASM_VERSION = Opcodes.ASM9; ++ private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; ++ private final List<File> libraries; ++ private final List<Transformer> transformers; ++ private final SortedClassProvider sortedClassProvider; ++ private final List<ClassProvider> classProviders; ++ private final int threads; ++ private final Consumer<String> logger; ++ private final Consumer<String> debug; ++ private boolean setup = false; ++ private ClassProvider libraryClasses; ++ ++ RenamerImpl(List<File> libraries, List<Transformer> transformers, SortedClassProvider sortedClassProvider, List<ClassProvider> classProviders, ++ int threads, Consumer<String> logger, Consumer<String> debug) { ++ this.libraries = libraries; ++ this.transformers = transformers; ++ this.sortedClassProvider = sortedClassProvider; ++ this.classProviders = Collections.unmodifiableList(classProviders); ++ this.threads = threads; ++ this.logger = logger; ++ this.debug = debug; ++ } ++ ++ private void setup() { ++ if (this.setup) ++ return; ++ ++ this.setup = true; ++ ++ ClassProvider.Builder libraryClassesBuilder = ClassProvider.builder().shouldCacheAll(true); ++ this.logger.accept("Adding Libraries to Inheritance"); ++ this.libraries.forEach(f -> libraryClassesBuilder.addLibrary(f.toPath())); ++ ++ this.libraryClasses = libraryClassesBuilder.build(); ++ } ++ ++ @Override ++ public void run(File input, File output) { ++ // Paper start - Add remappingSelf ++ this.run(input, output, false); ++ } ++ public void run(File input, File output, boolean remappingSelf) { ++ // Paper end ++ if (!this.setup) ++ this.setup(); ++ ++ if (Boolean.getBoolean(ProgressReporter.ENABLED_PROPERTY)) { ++ try { ++ PROGRESS.setMaxProgress(JarUtils.getFileCountInZip(input)); ++ } catch (IOException e) { ++ logger.accept("Failed to read zip file count: " + e); ++ } ++ } ++ ++ input = Objects.requireNonNull(input).getAbsoluteFile(); ++ output = Objects.requireNonNull(output).getAbsoluteFile(); ++ ++ if (!input.exists()) ++ throw new IllegalArgumentException("Input file not found: " + input.getAbsolutePath()); ++ ++ logger.accept("Reading Input: " + input.getAbsolutePath()); ++ PROGRESS.setStep("Reading input jar"); ++ // Read everything from the input jar! ++ List<Entry> oldEntries = new ArrayList<>(); ++ try (ZipFile in = new ZipFile(input)) { ++ int amount = 0; ++ for (Enumeration<? extends ZipEntry> entries = in.entries(); entries.hasMoreElements();) { ++ final ZipEntry e = entries.nextElement(); ++ if (e.isDirectory()) ++ continue; ++ String name = e.getName(); ++ byte[] data; ++ try (InputStream entryInput = in.getInputStream(e)) { ++ data = entryInput.readAllBytes(); // Paper - Use readAllBytes ++ } ++ ++ if (name.endsWith(".class") && !name.contains("META-INF/")) // Paper - Skip META-INF entries ++ oldEntries.add(ClassEntry.create(name, e.getTime(), data)); ++ else if (name.equals(MANIFEST_NAME)) ++ oldEntries.add(ManifestEntry.create(e.getTime(), data)); ++ else if (name.equals("javadoctor.json")) ++ oldEntries.add(Transformer.JavadoctorEntry.create(e.getTime(), data)); ++ else ++ oldEntries.add(ResourceEntry.create(name, e.getTime(), data)); ++ ++ if ((++amount) % 10 == 0) { ++ PROGRESS.setProgress(amount); ++ } ++ } ++ } catch (IOException e) { ++ throw new RuntimeException("Could not parse input: " + input.getAbsolutePath(), e); ++ } ++ ++ this.sortedClassProvider.clearCache(); ++ ArrayList<ClassProvider> classProviders = new ArrayList<>(this.classProviders); ++ classProviders.add(0, this.libraryClasses); ++ this.sortedClassProvider.classProviders = classProviders; ++ ++ AsyncHelper async = new AsyncHelper(threads); ++ try { ++ ++ /* Disabled until we do something with it ++ // Gather original file Hashes, so that we can detect changes and update the manifest if necessary ++ log("Gathering original hashes"); ++ Map<String, String> oldHashes = async.invokeAll(oldEntries, ++ e -> new Pair<>(e.getName(), HashFunction.SHA256.hash(e.getData())) ++ ).stream().collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); ++ */ ++ ++ PROGRESS.setProgress(0); ++ PROGRESS.setIndeterminate(true); ++ PROGRESS.setStep("Processing entries"); ++ ++ List<ClassEntry> ourClasses = oldEntries.stream() ++ .filter(e -> e instanceof ClassEntry && !e.getName().startsWith("META-INF/")) ++ .map(ClassEntry.class::cast) ++ .collect(Collectors.toList()); ++ ++ // Add the original classes to the inheritance map, TODO: Multi-Release somehow? ++ logger.accept("Adding input to inheritance map"); ++ ClassProvider.Builder inputClassesBuilder = ClassProvider.builder(); ++ async.consumeAll(ourClasses, ClassEntry::getClassName, c -> ++ inputClassesBuilder.addClass(c.getName().substring(0, c.getName().length() - 6), c.getData()) ++ ); ++ classProviders.add(0, inputClassesBuilder.build()); ++ ++ // Process everything ++ logger.accept("Processing entries"); ++ List<Entry> newEntries = async.invokeAll(oldEntries, Entry::getName, this::processEntry); ++ ++ logger.accept("Adding extras"); ++ // Paper start - I'm pretty sure the duplicates are because the input is already on the classpath ++ List<Entry> finalNewEntries = newEntries; ++ transformers.forEach(t -> finalNewEntries.addAll(t.getExtras())); ++ ++ Set<String> seen = new HashSet<>(); ++ if (remappingSelf) { ++ // deduplicate ++ List<Entry> n = new ArrayList<>(); ++ for (final Entry e : newEntries) { ++ if (seen.add(e.getName())) { ++ n.add(e); ++ } ++ } ++ newEntries = n; ++ } else { ++ String dupes = newEntries.stream().map(Entry::getName) ++ .filter(n -> !seen.add(n)) ++ .sorted() ++ .collect(Collectors.joining(", ")); ++ if (!dupes.isEmpty()) ++ throw new IllegalStateException("Duplicate entries detected: " + dupes); ++ } ++ // Paper end ++ ++ // We care about stable output, so sort, and single thread write. ++ logger.accept("Sorting"); ++ Collections.sort(newEntries, this::compare); ++ ++ if (!output.getParentFile().exists()) ++ output.getParentFile().mkdirs(); ++ ++ seen.clear(); ++ ++ PROGRESS.setMaxProgress(newEntries.size()); ++ PROGRESS.setStep("Writing output"); ++ ++ logger.accept("Writing Output: " + output.getAbsolutePath()); ++ try (OutputStream fos = new BufferedOutputStream(Files.newOutputStream(output.toPath())); ++ ZipOutputStream zos = new ZipOutputStream(fos)) { ++ ++ int amount = 0; ++ for (Entry e : newEntries) { ++ String name = e.getName(); ++ int idx = name.lastIndexOf('/'); ++ if (idx != -1) ++ addDirectory(zos, seen, name.substring(0, idx)); ++ ++ logger.accept(" " + name); ++ ZipEntry entry = new ZipEntry(name); ++ entry.setTime(e.getTime()); ++ zos.putNextEntry(entry); ++ zos.write(e.getData()); ++ zos.closeEntry(); ++ ++ if ((++amount) % 10 == 0) { ++ PROGRESS.setProgress(amount); ++ } ++ } ++ ++ PROGRESS.setProgress(amount); ++ } ++ } catch (final IOException e) { ++ throw new RuntimeException("Could not write to file " + output.getAbsolutePath(), e); ++ } finally { ++ async.shutdown(); ++ } ++ } ++ ++ private byte[] readAllBytes(InputStream in, long size) throws IOException { ++ // This program will crash if size exceeds MAX_INT anyway since arrays are limited to 32-bit indices ++ ByteArrayOutputStream tmp = new ByteArrayOutputStream(size >= 0 ? (int) size : 0); ++ ++ byte[] buffer = new byte[8192]; ++ int read; ++ while ((read = in.read(buffer)) != -1) { ++ tmp.write(buffer, 0, read); ++ } ++ ++ return tmp.toByteArray(); ++ } ++ ++ // Tho Directory entries are not strictly necessary, we add them because some bad implementations of Zip extractors ++ // attempt to extract files without making sure the parents exist. ++ private void addDirectory(ZipOutputStream zos, Set<String> seen, String path) throws IOException { ++ if (!seen.add(path)) ++ return; ++ ++ int idx = path.lastIndexOf('/'); ++ if (idx != -1) ++ addDirectory(zos, seen, path.substring(0, idx)); ++ ++ logger.accept(" " + path + '/'); ++ ZipEntry dir = new ZipEntry(path + '/'); ++ dir.setTime(Entry.STABLE_TIMESTAMP); ++ zos.putNextEntry(dir); ++ zos.closeEntry(); ++ } ++ ++ private Entry processEntry(final Entry start) { ++ Entry entry = start; ++ for (Transformer transformer : RenamerImpl.this.transformers) { ++ entry = entry.process(transformer); ++ if (entry == null) ++ return null; ++ } ++ return entry; ++ } ++ ++ private int compare(Entry o1, Entry o2) { ++ // In order for JarInputStream to work, MANIFEST has to be the first entry, so make it first! ++ if (MANIFEST_NAME.equals(o1.getName())) ++ return MANIFEST_NAME.equals(o2.getName()) ? 0 : -1; ++ if (MANIFEST_NAME.equals(o2.getName())) ++ return MANIFEST_NAME.equals(o1.getName()) ? 0 : 1; ++ return o1.getName().compareTo(o2.getName()); ++ } ++ ++ @Override ++ public void close() throws IOException { ++ this.sortedClassProvider.close(); ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 3f748a8f067d8ce8c39272cb3decaf4ec4879da5..d1699fcca66bcfbbe8fcc426802cb766cf1e580b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -992,6 +992,7 @@ public final class CraftServer implements Server { + this.loadPlugins(); + this.enablePlugins(PluginLoadOrder.STARTUP); + this.enablePlugins(PluginLoadOrder.POSTWORLD); ++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins + this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD)); + } + diff --git a/patches/server/0021-Hook-into-CB-plugin-rewrites.patch b/patches/server/0021-Hook-into-CB-plugin-rewrites.patch new file mode 100644 index 0000000000..2652791358 --- /dev/null +++ b/patches/server/0021-Hook-into-CB-plugin-rewrites.patch @@ -0,0 +1,185 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Wed, 3 Oct 2018 20:09:18 -0400 +Subject: [PATCH] Hook into CB plugin rewrites + +Allows us to do fun stuff like rewrite the OBC util fastutil location to +our own relocation. Also lets us rewrite NMS calls for when we're +debugging in an IDE pre-relocate. + +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java +index 6500c1dad7a48256b8a58b0655f4972c0e7a5dd0..c8c40052d0bb7791297a215ba18fb093fbfda7b6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java +@@ -8,6 +8,7 @@ import java.util.ArrayList; + import java.util.Arrays; + import java.util.Collections; + import java.util.Enumeration; ++import java.util.HashMap; + import java.util.HashSet; + import java.util.List; + import java.util.Map; +@@ -17,6 +18,7 @@ import java.util.jar.JarEntry; + import java.util.jar.JarFile; + import java.util.jar.JarOutputStream; + import java.util.zip.ZipEntry; ++import javax.annotation.Nonnull; + import joptsimple.OptionParser; + import joptsimple.OptionSet; + import joptsimple.OptionSpec; +@@ -79,6 +81,40 @@ public class Commodore { + private static final Map<String, RerouteMethodData> MATERIAL_METHOD_REROUTE = Commodore.createReroutes(MaterialRerouting.class); + private static final Map<String, RerouteMethodData> METHOD_REROUTE = Commodore.createReroutes(MethodRerouting.class); + ++ // Paper start - Plugin rewrites ++ private static final Map<String, String> SEARCH_AND_REMOVE = initReplacementsMap(); ++ private static Map<String, String> initReplacementsMap() { ++ Map<String, String> getAndRemove = new HashMap<>(); ++ // Be wary of maven shade's relocations ++ ++ final java.util.jar.Manifest manifest = io.papermc.paper.util.JarManifests.manifest(Commodore.class); ++ if (Boolean.getBoolean( "debug.rewriteForIde") && manifest != null) ++ { ++ // unversion incoming calls for pre-relocate debug work ++ final String NMS_REVISION_PACKAGE = "v" + manifest.getMainAttributes().getValue("CraftBukkit-Package-Version") + "/"; ++ ++ getAndRemove.put("org/bukkit/".concat("craftbukkit/" + NMS_REVISION_PACKAGE), NMS_REVISION_PACKAGE); ++ } ++ ++ return getAndRemove; ++ } ++ ++ @Nonnull ++ private static String getOriginalOrRewrite(@Nonnull String original) ++ { ++ String rewrite = null; ++ for ( Map.Entry<String, String> entry : SEARCH_AND_REMOVE.entrySet() ) ++ { ++ if ( original.contains( entry.getKey() ) ) ++ { ++ rewrite = original.replace( entry.getValue(), "" ); ++ } ++ } ++ ++ return rewrite != null ? rewrite : original; ++ } ++ // Paper end - Plugin rewrites ++ + public static void main(String[] args) { + OptionParser parser = new OptionParser(); + OptionSpec<File> inputFlag = parser.acceptsAll(Arrays.asList("i", "input")).withRequiredArg().ofType(File.class).required(); +@@ -205,9 +241,49 @@ public class Commodore { + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + return new MethodVisitor(this.api, super.visitMethod(access, name, desc, signature, exceptions)) { ++ // Paper start - Plugin rewrites ++ @Override ++ public void visitTypeInsn(int opcode, String type) { ++ type = getOriginalOrRewrite(type); ++ ++ super.visitTypeInsn(opcode, type); ++ } ++ ++ @Override ++ public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { ++ for (int i = 0; i < local.length; i++) ++ { ++ if (!(local[i] instanceof String)) { continue; } ++ ++ local[i] = getOriginalOrRewrite((String) local[i]); ++ } ++ ++ for (int i = 0; i < stack.length; i++) ++ { ++ if (!(stack[i] instanceof String)) { continue; } ++ ++ stack[i] = getOriginalOrRewrite((String) stack[i]); ++ } ++ ++ super.visitFrame(type, nLocal, local, nStack, stack); ++ } ++ ++ @Override ++ public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { ++ descriptor = getOriginalOrRewrite(descriptor); ++ ++ super.visitLocalVariable(name, descriptor, signature, start, end, index); ++ } ++ // Paper end - Plugin rewrites + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { ++ // Paper start - Rewrite plugins ++ owner = getOriginalOrRewrite(owner); ++ if (desc != null) { ++ desc = getOriginalOrRewrite(desc); ++ } ++ // Paper end + name = FieldRename.rename(pluginVersion, owner, name); + + if (modern) { +@@ -306,6 +382,13 @@ public class Commodore { + return; + } + ++ // Paper start - Rewrite plugins ++ owner = getOriginalOrRewrite(owner) ; ++ if (desc != null) { ++ desc = getOriginalOrRewrite(desc); ++ } ++ // Paper end - Rewrite plugins ++ + if (modern) { + if (owner.equals("org/bukkit/Material") || (instantiatedMethodType != null && instantiatedMethodType.getDescriptor().startsWith("(Lorg/bukkit/Material;)"))) { + switch (name) { +@@ -402,6 +485,13 @@ public class Commodore { + + @Override + public void visitLdcInsn(Object value) { ++ // Paper start ++ if (value instanceof Type type) { ++ if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) { ++ value = Type.getType(getOriginalOrRewrite(type.getDescriptor())); ++ } ++ } ++ // Paper end + if (value instanceof String && ((String) value).equals("com.mysql.jdbc.Driver")) { + super.visitLdcInsn("com.mysql.cj.jdbc.Driver"); + return; +@@ -412,6 +502,14 @@ public class Commodore { + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { ++ // Paper start - Rewrite plugins ++ name = getOriginalOrRewrite(name); ++ if (descriptor != null) { ++ descriptor = getOriginalOrRewrite(descriptor); ++ } ++ final String fName = name; ++ final String fDescriptor = descriptor; ++ // Paper end - Rewrite plugins + if (bootstrapMethodHandle.getOwner().equals("java/lang/invoke/LambdaMetafactory") + && bootstrapMethodHandle.getName().equals("metafactory") && bootstrapMethodArguments.length == 3) { + Type samMethodType = (Type) bootstrapMethodArguments[0]; +@@ -428,7 +526,7 @@ public class Commodore { + methodArgs.add(new Handle(newOpcode, newOwner, newName, newDescription, newItf)); + methodArgs.add(newInstantiated); + +- super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, methodArgs.toArray(Object[]::new)); ++ super.visitInvokeDynamicInsn(fName, fDescriptor, bootstrapMethodHandle, methodArgs.toArray(Object[]::new)); // Paper - use final local vars + }, implMethod.getTag(), implMethod.getOwner(), implMethod.getName(), implMethod.getDesc(), implMethod.isInterface(), samMethodType, instantiatedMethodType); + return; + } +@@ -479,6 +577,12 @@ public class Commodore { + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { ++ // Paper start - Rewrite plugins ++ descriptor = getOriginalOrRewrite(descriptor); ++ if ( signature != null ) { ++ signature = getOriginalOrRewrite(signature); ++ } ++ // Paper end + return new FieldVisitor(this.api, super.visitField(access, name, descriptor, signature, value)) { + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { diff --git a/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch b/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch new file mode 100644 index 0000000000..8211d56891 --- /dev/null +++ b/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch @@ -0,0 +1,728 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke <[email protected]> +Date: Sun, 30 Oct 2022 23:47:26 +0100 +Subject: [PATCH] Remap reflection calls in plugins using internals + +Co-authored-by: Jason Penilla <[email protected]> + +diff --git a/build.gradle.kts b/build.gradle.kts +index 8350fc099b68918fb03a21b6a5047ceee72dcbb4..708448e071ddb5558658d3096b1d7ee2593ec8c5 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -47,6 +47,12 @@ dependencies { + testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest + implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling + implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins ++ // Paper start - Remap reflection ++ val reflectionRewriterVersion = "0.0.1" ++ implementation("io.papermc:reflection-rewriter:$reflectionRewriterVersion") ++ implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion") ++ implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion") ++ // Paper end - Remap reflection + } + + paperweight { +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java +index 893ad5e7c2d32ccd64962d95d146bbd317c28ab8..3d73ea0e63c97b2b08e719b7be7af3894fb2d4e8 100644 +--- a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java ++++ b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java +@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableBiMap; + import com.mojang.logging.LogUtils; + import io.leangen.geantyref.TypeToken; + import io.papermc.paper.configuration.serializer.collections.MapSerializer; ++import io.papermc.paper.util.MappingEnvironment; + import io.papermc.paper.util.ObfHelper; + import net.minecraft.network.protocol.Packet; + import org.checkerframework.checker.nullness.qual.Nullable; +@@ -69,7 +70,7 @@ public final class PacketClassSerializer extends ScalarSerializer<Class<? extend + @Override + protected @Nullable Object serialize(final Class<? extends Packet<?>> packetClass, final Predicate<Class<?>> typeSupported) { + final String name = packetClass.getName(); +- @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server ++ @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null || !MappingEnvironment.reobf() ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server + if (mojName == null && MOJANG_TO_OBF.containsKey(name)) { + mojName = name; + } +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1240c061c121e8d5eb9add4e5e21955ee6df9368 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java +@@ -0,0 +1,187 @@ ++package io.papermc.paper.plugin.entrypoint.classloader; ++ ++import io.papermc.paper.pluginremap.reflect.ReflectionRemapper; ++import java.io.IOException; ++import java.io.InputStream; ++import java.io.UncheckedIOException; ++import java.net.JarURLConnection; ++import java.net.URI; ++import java.net.URL; ++import java.net.URLClassLoader; ++import java.security.CodeSigner; ++import java.security.CodeSource; ++import java.util.Map; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.function.Function; ++import java.util.jar.Attributes; ++import java.util.jar.Manifest; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.objectweb.asm.ClassReader; ++import org.objectweb.asm.ClassVisitor; ++import org.objectweb.asm.ClassWriter; ++ ++import static java.util.Objects.requireNonNullElse; ++ ++public final class BytecodeModifyingURLClassLoader extends URLClassLoader { ++ static { ++ ClassLoader.registerAsParallelCapable(); ++ } ++ ++ private static final Object MISSING_MANIFEST = new Object(); ++ ++ private final Function<byte[], byte[]> modifier; ++ private final Map<String, Object> manifests = new ConcurrentHashMap<>(); ++ ++ public BytecodeModifyingURLClassLoader( ++ final URL[] urls, ++ final ClassLoader parent, ++ final Function<byte[], byte[]> modifier ++ ) { ++ super(urls, parent); ++ this.modifier = modifier; ++ } ++ ++ public BytecodeModifyingURLClassLoader( ++ final URL[] urls, ++ final ClassLoader parent ++ ) { ++ this(urls, parent, bytes -> { ++ final ClassReader classReader = new ClassReader(bytes); ++ final ClassWriter classWriter = new ClassWriter(classReader, 0); ++ final ClassVisitor visitor = ReflectionRemapper.visitor(classWriter); ++ if (visitor == classWriter) { ++ return bytes; ++ } ++ classReader.accept(visitor, 0); ++ return classWriter.toByteArray(); ++ }); ++ } ++ ++ @Override ++ protected Class<?> findClass(final String name) throws ClassNotFoundException { ++ final Class<?> result; ++ final String path = name.replace('.', '/').concat(".class"); ++ final URL url = this.findResource(path); ++ if (url != null) { ++ try { ++ result = this.defineClass(name, url); ++ } catch (final IOException e) { ++ throw new ClassNotFoundException(name, e); ++ } ++ } else { ++ result = null; ++ } ++ if (result == null) { ++ throw new ClassNotFoundException(name); ++ } ++ return result; ++ } ++ ++ private Class<?> defineClass(String name, URL url) throws IOException { ++ int i = name.lastIndexOf('.'); ++ if (i != -1) { ++ String pkgname = name.substring(0, i); ++ // Check if package already loaded. ++ final @Nullable Manifest man = this.manifestFor(url); ++ final URL jarUrl = URI.create(jarName(url)).toURL(); ++ if (this.getAndVerifyPackage(pkgname, man, jarUrl) == null) { ++ try { ++ if (man != null) { ++ this.definePackage(pkgname, man, jarUrl); ++ } else { ++ this.definePackage(pkgname, null, null, null, null, null, null, null); ++ } ++ } catch (IllegalArgumentException iae) { ++ // parallel-capable class loaders: re-verify in case of a ++ // race condition ++ if (this.getAndVerifyPackage(pkgname, man, jarUrl) == null) { ++ // Should never happen ++ throw new AssertionError("Cannot find package " + ++ pkgname); ++ } ++ } ++ } ++ } ++ final byte[] bytes; ++ try (final InputStream is = url.openStream()) { ++ bytes = is.readAllBytes(); ++ } ++ ++ final byte[] modified = this.modifier.apply(bytes); ++ ++ final CodeSource cs = new CodeSource(url, (CodeSigner[]) null); ++ return this.defineClass(name, modified, 0, modified.length, cs); ++ } ++ ++ private Package getAndVerifyPackage( ++ String pkgname, ++ Manifest man, URL url ++ ) { ++ Package pkg = getDefinedPackage(pkgname); ++ if (pkg != null) { ++ // Package found, so check package sealing. ++ if (pkg.isSealed()) { ++ // Verify that code source URL is the same. ++ if (!pkg.isSealed(url)) { ++ throw new SecurityException( ++ "sealing violation: package " + pkgname + " is sealed"); ++ } ++ } else { ++ // Make sure we are not attempting to seal the package ++ // at this code source URL. ++ if ((man != null) && this.isSealed(pkgname, man)) { ++ throw new SecurityException( ++ "sealing violation: can't seal package " + pkgname + ++ ": already loaded"); ++ } ++ } ++ } ++ return pkg; ++ } ++ ++ private boolean isSealed(String name, Manifest man) { ++ Attributes attr = man.getAttributes(name.replace('.', '/').concat("/")); ++ String sealed = null; ++ if (attr != null) { ++ sealed = attr.getValue(Attributes.Name.SEALED); ++ } ++ if (sealed == null) { ++ if ((attr = man.getMainAttributes()) != null) { ++ sealed = attr.getValue(Attributes.Name.SEALED); ++ } ++ } ++ return "true".equalsIgnoreCase(sealed); ++ } ++ ++ private @Nullable Manifest manifestFor(final URL url) throws IOException { ++ Manifest man = null; ++ if (url.getProtocol().equals("jar")) { ++ try { ++ final Object computedManifest = this.manifests.computeIfAbsent(jarName(url), $ -> { ++ try { ++ final Manifest m = ((JarURLConnection) url.openConnection()).getManifest(); ++ return requireNonNullElse(m, MISSING_MANIFEST); ++ } catch (final IOException e) { ++ throw new UncheckedIOException(e); ++ } ++ }); ++ if (computedManifest instanceof Manifest found) { ++ man = found; ++ } ++ } catch (final UncheckedIOException e) { ++ throw e.getCause(); ++ } catch (final IllegalArgumentException e) { ++ throw new IOException(e); ++ } ++ } ++ return man; ++ } ++ ++ private static String jarName(final URL sourceUrl) { ++ final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!'); ++ if (exclamationIdx != -1) { ++ return sourceUrl.getPath().substring(0, exclamationIdx); ++ } ++ throw new IllegalArgumentException("Could not find jar for URL " + sourceUrl); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java +index f9a2c55a354c877749db3f92956de802ae575788..39182cdd17473da0123dc7172dce507eab29fedc 100644 +--- a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java ++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java +@@ -1,12 +1,17 @@ + package io.papermc.paper.plugin.entrypoint.classloader; + + import io.papermc.paper.plugin.configuration.PluginMeta; ++import org.objectweb.asm.ClassReader; ++import org.objectweb.asm.ClassWriter; + + // Stub, implement in future. + public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier { + + @Override + public byte[] modify(PluginMeta configuration, byte[] bytecode) { +- return bytecode; ++ ClassReader classReader = new ClassReader(bytecode); ++ ClassWriter classWriter = new ClassWriter(classReader, 0); ++ classReader.accept(io.papermc.paper.pluginremap.reflect.ReflectionRemapper.visitor(classWriter), 0); ++ return classWriter.toByteArray(); + } + } +diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +index f576060c8fe872772bbafe2016fc9b83a3c095f1..f9d4b33050a6fe8c2dabe8e5eec075d95dc513e0 100644 +--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java ++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +@@ -2,12 +2,12 @@ package io.papermc.paper.plugin.loader; + + import io.papermc.paper.plugin.PluginInitializerManager; + import io.papermc.paper.plugin.bootstrap.PluginProviderContext; ++import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader; ++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; + import io.papermc.paper.plugin.loader.library.ClassPathLibrary; + import io.papermc.paper.plugin.loader.library.PaperLibraryStore; +-import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; + import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +-import org.jetbrains.annotations.NotNull; +- ++import io.papermc.paper.util.MappingEnvironment; + import java.io.IOException; + import java.net.MalformedURLException; + import java.net.URL; +@@ -17,6 +17,7 @@ import java.util.ArrayList; + import java.util.List; + import java.util.jar.JarFile; + import java.util.logging.Logger; ++import org.jetbrains.annotations.NotNull; + + public class PaperClasspathBuilder implements PluginClasspathBuilder { + +@@ -60,7 +61,10 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder { + } + + try { +- return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader())); ++ final URLClassLoader libraryLoader = MappingEnvironment.DISABLE_PLUGIN_REWRITING ++ ? new URLClassLoader(urls, this.getClass().getClassLoader()) ++ : new BytecodeModifyingURLClassLoader(urls, this.getClass().getClassLoader()); ++ return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), libraryLoader); + } catch (IOException exception) { + throw new RuntimeException(exception); + } +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java +index bdd9bc8a414719b9f1d6f01f90539ddb8603a878..fdb52ad85cfaa1d53aadcad72cec3d3c8c12c058 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java +@@ -1,9 +1,12 @@ + package io.papermc.paper.plugin.provider.type.spigot; + ++import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader; + import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; + import io.papermc.paper.plugin.provider.type.PluginTypeFactory; ++import io.papermc.paper.util.MappingEnvironment; + import org.bukkit.plugin.InvalidDescriptionException; + import org.bukkit.plugin.PluginDescriptionFile; ++import org.bukkit.plugin.java.LibraryLoader; + import org.yaml.snakeyaml.error.YAMLException; + + import java.io.IOException; +@@ -15,6 +18,12 @@ import java.util.jar.JarFile; + + class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> { + ++ static { ++ if (!MappingEnvironment.DISABLE_PLUGIN_REWRITING) { ++ LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new; ++ } ++ } ++ + @Override + public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException { + // Copied from SimplePluginManager#loadPlugins +diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c653d1f5af8b407cfba715e6027dbb695046892a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java +@@ -0,0 +1,212 @@ ++package io.papermc.paper.pluginremap.reflect; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.MappingEnvironment; ++import io.papermc.paper.util.ObfHelper; ++import io.papermc.reflectionrewriter.runtime.AbstractDefaultRulesReflectionProxy; ++import io.papermc.reflectionrewriter.runtime.DefineClassReflectionProxy; ++import java.lang.invoke.MethodHandles; ++import java.nio.ByteBuffer; ++import java.security.CodeSource; ++import java.security.ProtectionDomain; ++import java.util.Map; ++import java.util.Objects; ++import java.util.stream.Collectors; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.objectweb.asm.ClassReader; ++import org.objectweb.asm.ClassWriter; ++import org.slf4j.Logger; ++ ++// todo proper inheritance handling ++@SuppressWarnings("unused") ++@DefaultQualifier(NonNull.class) ++public final class PaperReflection extends AbstractDefaultRulesReflectionProxy implements DefineClassReflectionProxy { ++ // concat to avoid being rewritten by shadow ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final String CB_PACKAGE_PREFIX = "org.bukkit.".concat("craftbukkit."); ++ private static final String LEGACY_CB_PACKAGE_PREFIX = "org.bukkit.".concat("craftbukkit.") + MappingEnvironment.LEGACY_CB_VERSION + "."; ++ ++ private final DefineClassReflectionProxy defineClassProxy; ++ private final Map<String, ObfHelper.ClassMapping> mappingsByMojangName; ++ private final Map<String, ObfHelper.ClassMapping> mappingsByObfName; ++ // Reflection does not care about method return values, so this map removes the return value descriptor from the key ++ private final Map<String, Map<String, String>> strippedMethodMappings; ++ ++ PaperReflection() { ++ this.defineClassProxy = DefineClassReflectionProxy.create(PaperReflection::processClass); ++ if (!MappingEnvironment.hasMappings()) { ++ this.mappingsByMojangName = Map.of(); ++ this.mappingsByObfName = Map.of(); ++ this.strippedMethodMappings = Map.of(); ++ return; ++ } ++ final ObfHelper obfHelper = ObfHelper.INSTANCE; ++ this.mappingsByMojangName = Objects.requireNonNull(obfHelper.mappingsByMojangName(), "mappingsByMojangName"); ++ this.mappingsByObfName = Objects.requireNonNull(obfHelper.mappingsByObfName(), "mappingsByObfName"); ++ this.strippedMethodMappings = this.mappingsByMojangName.entrySet().stream().collect(Collectors.toUnmodifiableMap( ++ Map.Entry::getKey, ++ entry -> entry.getValue().strippedMethods() ++ )); ++ } ++ ++ @Override ++ protected String mapClassName(final String name) { ++ final ObfHelper.@Nullable ClassMapping mapping = this.mappingsByObfName.get(name); ++ return mapping != null ? mapping.mojangName() : removeCraftBukkitRelocation(name); ++ } ++ ++ @Override ++ protected String mapDeclaredMethodName(final Class<?> clazz, final String name, final Class<?>... parameterTypes) { ++ final @Nullable Map<String, String> mapping = this.strippedMethodMappings.get(clazz.getName()); ++ if (mapping == null) { ++ return name; ++ } ++ return mapping.getOrDefault(strippedMethodKey(name, parameterTypes), name); ++ } ++ ++ @Override ++ protected String mapMethodName(final Class<?> clazz, final String name, final Class<?>... parameterTypes) { ++ final @Nullable String mapped = this.findMappedMethodName(clazz, name, parameterTypes); ++ return mapped != null ? mapped : name; ++ } ++ ++ @Override ++ protected String mapDeclaredFieldName(final Class<?> clazz, final String name) { ++ final ObfHelper.@Nullable ClassMapping mapping = this.mappingsByMojangName.get(clazz.getName()); ++ if (mapping == null) { ++ return name; ++ } ++ return mapping.fieldsByObf().getOrDefault(name, name); ++ } ++ ++ @Override ++ protected String mapFieldName(final Class<?> clazz, final String name) { ++ final @Nullable String mapped = this.findMappedFieldName(clazz, name); ++ return mapped != null ? mapped : name; ++ } ++ ++ private @Nullable String findMappedMethodName(final Class<?> clazz, final String name, final Class<?>... parameterTypes) { ++ final Map<String, String> map = this.strippedMethodMappings.get(clazz.getName()); ++ @Nullable String mapped = null; ++ if (map != null) { ++ mapped = map.get(strippedMethodKey(name, parameterTypes)); ++ if (mapped != null) { ++ return mapped; ++ } ++ } ++ // JVM checks super before interfaces ++ final Class<?> superClass = clazz.getSuperclass(); ++ if (superClass != null) { ++ mapped = this.findMappedMethodName(superClass, name, parameterTypes); ++ } ++ if (mapped == null) { ++ for (final Class<?> i : clazz.getInterfaces()) { ++ mapped = this.findMappedMethodName(i, name, parameterTypes); ++ if (mapped != null) { ++ break; ++ } ++ } ++ } ++ return mapped; ++ } ++ ++ private @Nullable String findMappedFieldName(final Class<?> clazz, final String name) { ++ final ObfHelper.ClassMapping mapping = this.mappingsByMojangName.get(clazz.getName()); ++ @Nullable String mapped = null; ++ if (mapping != null) { ++ mapped = mapping.fieldsByObf().get(name); ++ if (mapped != null) { ++ return mapped; ++ } ++ } ++ // The JVM checks super before interfaces ++ final Class<?> superClass = clazz.getSuperclass(); ++ if (superClass != null) { ++ mapped = this.findMappedFieldName(superClass, name); ++ } ++ if (mapped == null) { ++ for (final Class<?> i : clazz.getInterfaces()) { ++ mapped = this.findMappedFieldName(i, name); ++ if (mapped != null) { ++ break; ++ } ++ } ++ } ++ return mapped; ++ } ++ ++ private static String strippedMethodKey(final String methodName, final Class<?>... parameterTypes) { ++ return methodName + parameterDescriptor(parameterTypes); ++ } ++ ++ private static String parameterDescriptor(final Class<?>... parameterTypes) { ++ final StringBuilder builder = new StringBuilder(); ++ builder.append('('); ++ for (final Class<?> parameterType : parameterTypes) { ++ builder.append(parameterType.descriptorString()); ++ } ++ builder.append(')'); ++ return builder.toString(); ++ } ++ ++ private static String removeCraftBukkitRelocation(final String name) { ++ if (MappingEnvironment.hasMappings()) { ++ // Relocation is applied in reobf, and when mappings are present they handle the relocation ++ return name; ++ } ++ if (name.startsWith(LEGACY_CB_PACKAGE_PREFIX)) { ++ return CB_PACKAGE_PREFIX + name.substring(LEGACY_CB_PACKAGE_PREFIX.length()); ++ } ++ return name; ++ } ++ ++ @Override ++ public Class<?> defineClass(final Object loader, final byte[] b, final int off, final int len) throws ClassFormatError { ++ return this.defineClassProxy.defineClass(loader, b, off, len); ++ } ++ ++ @Override ++ public Class<?> defineClass(final Object loader, final String name, final byte[] b, final int off, final int len) throws ClassFormatError { ++ return this.defineClassProxy.defineClass(loader, name, b, off, len); ++ } ++ ++ @Override ++ public Class<?> defineClass(final Object loader, final @Nullable String name, final byte[] b, final int off, final int len, final @Nullable ProtectionDomain protectionDomain) throws ClassFormatError { ++ return this.defineClassProxy.defineClass(loader, name, b, off, len, protectionDomain); ++ } ++ ++ @Override ++ public Class<?> defineClass(final Object loader, final String name, final ByteBuffer b, final ProtectionDomain protectionDomain) throws ClassFormatError { ++ return this.defineClassProxy.defineClass(loader, name, b, protectionDomain); ++ } ++ ++ @Override ++ public Class<?> defineClass(final Object secureLoader, final String name, final byte[] b, final int off, final int len, final CodeSource cs) { ++ return this.defineClassProxy.defineClass(secureLoader, name, b, off, len, cs); ++ } ++ ++ @Override ++ public Class<?> defineClass(final Object secureLoader, final String name, final ByteBuffer b, final CodeSource cs) { ++ return this.defineClassProxy.defineClass(secureLoader, name, b, cs); ++ } ++ ++ @Override ++ public Class<?> defineClass(final MethodHandles.Lookup lookup, final byte[] bytes) throws IllegalAccessException { ++ return this.defineClassProxy.defineClass(lookup, bytes); ++ } ++ ++ // todo apply bytecode remap here as well ++ private static byte[] processClass(final byte[] bytes) { ++ try { ++ final ClassReader reader = new ClassReader(bytes); ++ final ClassWriter writer = new ClassWriter(reader, 0); ++ reader.accept(ReflectionRemapper.visitor(writer), 0); ++ return writer.toByteArray(); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to process class bytes", ex); ++ return bytes; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5fa5868e82d1f00498d0c5771369e1718b2df4ee +--- /dev/null ++++ b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java +@@ -0,0 +1,54 @@ ++package io.papermc.paper.pluginremap.reflect; ++ ++import io.papermc.asm.ClassInfoProvider; ++import io.papermc.asm.RewriteRuleVisitorFactory; ++import io.papermc.paper.util.MappingEnvironment; ++import io.papermc.reflectionrewriter.BaseReflectionRules; ++import io.papermc.reflectionrewriter.DefineClassRule; ++import io.papermc.reflectionrewriter.proxygenerator.ProxyGenerator; ++import java.lang.invoke.MethodHandles; ++import java.lang.reflect.Method; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.objectweb.asm.ClassVisitor; ++import org.objectweb.asm.Opcodes; ++ ++@DefaultQualifier(NonNull.class) ++public final class ReflectionRemapper { ++ private static final String PAPER_REFLECTION_HOLDER = "io.papermc.paper.pluginremap.reflect.PaperReflectionHolder"; ++ private static final String PAPER_REFLECTION_HOLDER_DESC = PAPER_REFLECTION_HOLDER.replace('.', '/'); ++ private static final RewriteRuleVisitorFactory VISITOR_FACTORY = RewriteRuleVisitorFactory.create( ++ Opcodes.ASM9, ++ chain -> chain.then(new BaseReflectionRules(PAPER_REFLECTION_HOLDER).rules()) ++ .then(DefineClassRule.create(PAPER_REFLECTION_HOLDER_DESC, true)), ++ ClassInfoProvider.basic() ++ ); ++ ++ static { ++ if (!MappingEnvironment.reobf()) { ++ setupProxy(); ++ } ++ } ++ ++ private ReflectionRemapper() { ++ } ++ ++ public static ClassVisitor visitor(final ClassVisitor parent) { ++ if (MappingEnvironment.reobf()) { ++ return parent; ++ } ++ return VISITOR_FACTORY.createVisitor(parent); ++ } ++ ++ private static void setupProxy() { ++ try { ++ final byte[] bytes = ProxyGenerator.generateProxy(PaperReflection.class, PAPER_REFLECTION_HOLDER_DESC); ++ final MethodHandles.Lookup lookup = MethodHandles.lookup(); ++ final Class<?> generated = lookup.defineClass(bytes); ++ final Method init = generated.getDeclaredMethod("init", PaperReflection.class); ++ init.invoke(null, new PaperReflection()); ++ } catch (final ReflectiveOperationException ex) { ++ throw new RuntimeException(ex); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/MappingEnvironment.java b/src/main/java/io/papermc/paper/util/MappingEnvironment.java +index 8e4229634d41a42b3d93948eebb77def7c0c72b1..4d57213c9917ea965bfe06a5d242e61422457ee2 100644 +--- a/src/main/java/io/papermc/paper/util/MappingEnvironment.java ++++ b/src/main/java/io/papermc/paper/util/MappingEnvironment.java +@@ -10,6 +10,8 @@ import org.checkerframework.framework.qual.DefaultQualifier; + + @DefaultQualifier(NonNull.class) + public final class MappingEnvironment { ++ public static final boolean DISABLE_PLUGIN_REWRITING = Boolean.getBoolean("paper.disable-plugin-rewriting"); ++ public static final String LEGACY_CB_VERSION = "v1_20_R4"; + private static final @Nullable String MAPPINGS_HASH = readMappingsHash(); + private static final boolean REOBF = checkReobf(); + +diff --git a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java +index 242811578a786e3807a1a7019d472d5a68f87116..0b65fdf53124f3dd042b2363b1b8df8e1ca7de00 100644 +--- a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java ++++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java +@@ -29,6 +29,9 @@ public enum StacktraceDeobfuscator { + }); + + public void deobfuscateThrowable(final Throwable throwable) { ++ if (!MappingEnvironment.reobf()) { ++ return; ++ } + if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true + return; + } +@@ -44,6 +47,9 @@ public enum StacktraceDeobfuscator { + } + + public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) { ++ if (!MappingEnvironment.reobf()) { ++ return traceElements; ++ } + if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true + return traceElements; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java +index c8c40052d0bb7791297a215ba18fb093fbfda7b6..e8bdc9f97cc506ca8509480ea0c07facc778df8d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java +@@ -82,36 +82,26 @@ public class Commodore { + private static final Map<String, RerouteMethodData> METHOD_REROUTE = Commodore.createReroutes(MethodRerouting.class); + + // Paper start - Plugin rewrites +- private static final Map<String, String> SEARCH_AND_REMOVE = initReplacementsMap(); +- private static Map<String, String> initReplacementsMap() { +- Map<String, String> getAndRemove = new HashMap<>(); +- // Be wary of maven shade's relocations +- +- final java.util.jar.Manifest manifest = io.papermc.paper.util.JarManifests.manifest(Commodore.class); +- if (Boolean.getBoolean( "debug.rewriteForIde") && manifest != null) +- { +- // unversion incoming calls for pre-relocate debug work +- final String NMS_REVISION_PACKAGE = "v" + manifest.getMainAttributes().getValue("CraftBukkit-Package-Version") + "/"; +- +- getAndRemove.put("org/bukkit/".concat("craftbukkit/" + NMS_REVISION_PACKAGE), NMS_REVISION_PACKAGE); ++ private static final String CB_PACKAGE_PREFIX = "org/bukkit/".concat("craftbukkit/"); ++ private static final String LEGACY_CB_PACKAGE_PREFIX = CB_PACKAGE_PREFIX + io.papermc.paper.util.MappingEnvironment.LEGACY_CB_VERSION + "/"; ++ private static String runtimeCbPkgPrefix() { ++ if (io.papermc.paper.util.MappingEnvironment.reobf()) { ++ return LEGACY_CB_PACKAGE_PREFIX; + } +- +- return getAndRemove; ++ return CB_PACKAGE_PREFIX; + } + + @Nonnull + private static String getOriginalOrRewrite(@Nonnull String original) + { +- String rewrite = null; +- for ( Map.Entry<String, String> entry : SEARCH_AND_REMOVE.entrySet() ) +- { +- if ( original.contains( entry.getKey() ) ) +- { +- rewrite = original.replace( entry.getValue(), "" ); ++ // Relocation is applied in reobf, and when mappings are present they handle the relocation ++ if (!io.papermc.paper.util.MappingEnvironment.reobf() && !io.papermc.paper.util.MappingEnvironment.hasMappings()) { ++ if (original.contains(LEGACY_CB_PACKAGE_PREFIX)) { ++ original = original.replace(LEGACY_CB_PACKAGE_PREFIX, CB_PACKAGE_PREFIX); + } + } + +- return rewrite != null ? rewrite : original; ++ return original; + } + // Paper end - Plugin rewrites + +@@ -182,7 +172,7 @@ public class Commodore { + ClassReader cr = new ClassReader(b); + ClassWriter cw = new ClassWriter(cr, 0); + +- cr.accept(new ClassRemapper(new ClassVisitor(Opcodes.ASM9, cw) { ++ cr.accept(new ClassRemapper(new ClassVisitor(Opcodes.ASM9, io.papermc.paper.pluginremap.reflect.ReflectionRemapper.visitor(cw)) { // Paper + final Set<RerouteMethodData> rerouteMethodData = new HashSet<>(); + String className; + boolean isInterface; +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 297767f64c1a7c01cffd6d78e71b60746622f22e..d551c9d898d8b5128dfef84d206396c84072abe4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -361,7 +361,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + throw new InvalidPluginException("Plugin API version " + pdf.getAPIVersion() + " is lower than the minimum allowed version. Please update or replace it."); + } + +- if (toCheck.isOlderThan(ApiVersion.FLATTENING)) { ++ if (!io.papermc.paper.util.MappingEnvironment.DISABLE_PLUGIN_REWRITING && toCheck.isOlderThan(ApiVersion.FLATTENING)) { // Paper + CraftLegacy.init(); + } + +@@ -376,6 +376,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + + @Override + public byte[] processClass(PluginDescriptionFile pdf, String path, byte[] clazz) { ++ if (io.papermc.paper.util.MappingEnvironment.DISABLE_PLUGIN_REWRITING) return clazz; // Paper + try { + clazz = Commodore.convert(clazz, pdf.getName(), ApiVersion.getOrCreateVersion(pdf.getAPIVersion()), ((CraftServer) Bukkit.getServer()).activeCompatibilities); + } catch (Exception ex) { diff --git a/patches/server/0023-Timings-v2.patch b/patches/server/0023-Timings-v2.patch new file mode 100644 index 0000000000..74a61a6b4a --- /dev/null +++ b/patches/server/0023-Timings-v2.patch @@ -0,0 +1,2097 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Thu, 3 Mar 2016 04:00:11 -0600 +Subject: [PATCH] Timings v2 + + +diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4bd813161a5d76a83cdbd0a9209b9ea9e60ffe1b +--- /dev/null ++++ b/src/main/java/co/aikar/timings/MinecraftTimings.java +@@ -0,0 +1,169 @@ ++package co.aikar.timings; ++ ++import com.google.common.collect.MapMaker; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import net.minecraft.commands.functions.CommandFunction; ++import net.minecraft.network.protocol.Packet; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.scheduler.BukkitTask; ++ ++import org.bukkit.craftbukkit.scheduler.CraftTask; ++ ++import java.util.Map; ++ ++// TODO: Re-implement missing timers ++@Deprecated(forRemoval = true) ++public final class MinecraftTimings { ++ ++ public static final Timing serverOversleep = Timings.ofSafe("Server Oversleep"); ++ public static final Timing playerListTimer = Timings.ofSafe("Player List"); ++ public static final Timing commandFunctionsTimer = Timings.ofSafe("Command Functions"); ++ public static final Timing connectionTimer = Timings.ofSafe("Connection Handler"); ++ public static final Timing tickablesTimer = Timings.ofSafe("Tickables"); ++ public static final Timing minecraftSchedulerTimer = Timings.ofSafe("Minecraft Scheduler"); ++ public static final Timing bukkitSchedulerTimer = Timings.ofSafe("Bukkit Scheduler"); ++ public static final Timing bukkitSchedulerPendingTimer = Timings.ofSafe("Bukkit Scheduler - Pending"); ++ public static final Timing bukkitSchedulerFinishTimer = Timings.ofSafe("Bukkit Scheduler - Finishing"); ++ public static final Timing chunkIOTickTimer = Timings.ofSafe("ChunkIOTick"); ++ public static final Timing timeUpdateTimer = Timings.ofSafe("Time Update"); ++ public static final Timing serverCommandTimer = Timings.ofSafe("Server Command"); ++ public static final Timing savePlayers = Timings.ofSafe("Save Players"); ++ ++ public static final Timing tickEntityTimer = Timings.ofSafe("## tickEntity"); ++ public static final Timing tickTileEntityTimer = Timings.ofSafe("## tickTileEntity"); ++ public static final Timing packetProcessTimer = Timings.ofSafe("## Packet Processing"); ++ public static final Timing scheduledBlocksTimer = Timings.ofSafe("## Scheduled Blocks"); ++ public static final Timing structureGenerationTimer = Timings.ofSafe("Structure Generation"); ++ ++ public static final Timing processQueueTimer = Timings.ofSafe("processQueue"); ++ public static final Timing processTasksTimer = Timings.ofSafe("processTasks"); ++ ++ public static final Timing playerCommandTimer = Timings.ofSafe("playerCommand"); ++ ++ public static final Timing entityActivationCheckTimer = Timings.ofSafe("entityActivationCheck"); ++ ++ public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update"); ++ public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate"); ++ ++ private static final Map<Class<?>, String> taskNameCache = new MapMaker().weakKeys().makeMap(); ++ ++ private MinecraftTimings() {} ++ ++ public static Timing getInternalTaskName(String taskName) { ++ return Timings.ofSafe(taskName); ++ } ++ ++ /** ++ * Gets a timer associated with a plugins tasks. ++ * @param bukkitTask ++ * @param period ++ * @return ++ */ ++ public static Timing getPluginTaskTimings(BukkitTask bukkitTask, long period) { ++ if (!bukkitTask.isSync()) { ++ return NullTimingHandler.NULL; ++ } ++ Plugin plugin; ++ ++ CraftTask craftTask = (CraftTask) bukkitTask; ++ ++ final Class<?> taskClass = craftTask.getTaskClass(); ++ if (bukkitTask.getOwner() != null) { ++ plugin = bukkitTask.getOwner(); ++ } else { ++ plugin = TimingsManager.getPluginByClassloader(taskClass); ++ } ++ ++ final String taskname = taskNameCache.computeIfAbsent(taskClass, clazz -> { ++ try { ++ String clsName = !clazz.isMemberClass() ++ ? clazz.getName() ++ : clazz.getCanonicalName(); ++ if (clsName != null && clsName.contains("$Lambda$")) { ++ clsName = clsName.replaceAll("(Lambda\\$.*?)/.*", "$1"); ++ } ++ return clsName != null ? clsName : "UnknownTask"; ++ } catch (Throwable ex) { ++ new Exception("Error occurred detecting class name", ex).printStackTrace(); ++ return "MangledClassFile"; ++ } ++ }); ++ ++ StringBuilder name = new StringBuilder(64); ++ name.append("Task: ").append(taskname); ++ if (period > 0) { ++ name.append(" (interval:").append(period).append(")"); ++ } else { ++ name.append(" (Single)"); ++ } ++ ++ if (plugin == null) { ++ return Timings.ofSafe(null, name.toString()); ++ } ++ ++ return Timings.ofSafe(plugin, name.toString()); ++ } ++ ++ /** ++ * Get a named timer for the specified entity type to track type specific timings. ++ * @param entityType ++ * @return ++ */ ++ public static Timing getEntityTimings(String entityType, String type) { ++ return Timings.ofSafe("Minecraft", "## tickEntity - " + entityType + " - " + type, tickEntityTimer); ++ } ++ ++ /** ++ * Get a named timer for the specified tile entity type to track type specific timings. ++ * @param entity ++ * @return ++ */ ++ public static Timing getTileEntityTimings(BlockEntity entity) { ++ String entityType = entity.getClass().getName(); ++ return Timings.ofSafe("Minecraft", "## tickTileEntity - " + entityType, tickTileEntityTimer); ++ } ++ public static Timing getCancelTasksTimer() { ++ return Timings.ofSafe("Cancel Tasks"); ++ } ++ public static Timing getCancelTasksTimer(Plugin plugin) { ++ return Timings.ofSafe(plugin, "Cancel Tasks"); ++ } ++ ++ public static void stopServer() { ++ TimingsManager.stopServer(); ++ } ++ ++ public static Timing getBlockTiming(Block block) { ++ return Timings.ofSafe("## Scheduled Block: " + block.toString(), scheduledBlocksTimer); ++ } ++/* ++ public static Timing getStructureTiming(StructureGenerator structureGenerator) { ++ return Timings.ofSafe("Structure Generator - " + structureGenerator.getName(), structureGenerationTimer); ++ }*/ ++ ++ public static Timing getPacketTiming(Packet packet) { ++ return Timings.ofSafe("## Packet - " + packet.getClass().getName(), packetProcessTimer); ++ } ++ ++ public static Timing getCommandFunctionTiming(CommandFunction<?> function) { ++ return Timings.ofSafe("Command Function - " + function.id()); ++ } ++ ++ public static void processConfig(GlobalConfiguration.Timings config) { ++ TimingsManager.url = config.url; ++ if (!TimingsManager.url.endsWith("/")) { ++ TimingsManager.url += "/"; ++ } ++ TimingsManager.privacy = config.serverNamePrivacy; ++ if (!config.hiddenConfigEntries.contains("proxies.velocity.secret")) { ++ config.hiddenConfigEntries.add("proxies.velocity.secret"); ++ } ++ TimingsManager.hiddenConfigs.addAll(config.hiddenConfigEntries); ++ co.aikar.timings.Timings.setVerboseTimingsEnabled(config.verbose); ++ co.aikar.timings.Timings.setTimingsEnabled(config.enabled); ++ co.aikar.timings.Timings.setHistoryInterval(config.historyInterval * 20); ++ co.aikar.timings.Timings.setHistoryLength(config.historyLength * 20); ++ } ++} +diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java +new file mode 100644 +index 0000000000000000000000000000000000000000..49028463ba47e760281545c2f7597e3db8d6c453 +--- /dev/null ++++ b/src/main/java/co/aikar/timings/TimingsExport.java +@@ -0,0 +1,388 @@ ++/* ++ * This file is licensed under the MIT License (MIT). ++ * ++ * Copyright (c) 2014 Daniel Ennis <http://aikar.co> ++ * ++ * 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. ++ */ ++package co.aikar.timings; ++ ++import com.google.common.collect.Sets; ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import net.minecraft.server.MinecraftServer; ++import org.apache.commons.lang.StringUtils; ++import org.bukkit.Bukkit; ++import org.bukkit.Material; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.MemorySection; ++import org.bukkit.entity.EntityType; ++import org.json.simple.JSONObject; ++import org.json.simple.JSONValue; ++import oshi.SystemInfo; ++import oshi.hardware.HardwareAbstractionLayer; ++ ++import java.io.ByteArrayOutputStream; ++import java.io.IOException; ++import java.io.InputStream; ++import java.io.OutputStream; ++import java.lang.management.ManagementFactory; ++import java.lang.management.OperatingSystemMXBean; ++import java.lang.management.RuntimeMXBean; ++import java.net.HttpURLConnection; ++import java.net.InetAddress; ++import java.net.URL; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.logging.Level; ++import java.util.zip.GZIPOutputStream; ++ ++import static co.aikar.timings.TimingsManager.HISTORY; ++import static co.aikar.util.JSONUtil.appendObjectData; ++import static co.aikar.util.JSONUtil.createObject; ++import static co.aikar.util.JSONUtil.pair; ++import static co.aikar.util.JSONUtil.toArray; ++import static co.aikar.util.JSONUtil.toArrayMapper; ++import static co.aikar.util.JSONUtil.toObjectMapper; ++import static net.kyori.adventure.text.Component.text; ++ ++@SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) ++@Deprecated(forRemoval = true) ++public class TimingsExport extends Thread { ++ ++ private final TimingsReportListener listeners; ++ private final Map out; ++ private final TimingHistory[] history; ++ private static long lastReport = 0; ++ ++ private TimingsExport(TimingsReportListener listeners, Map out, TimingHistory[] history) { ++ super("Timings paste thread"); ++ this.listeners = listeners; ++ this.out = out; ++ this.history = history; ++ } ++ ++ /** ++ * Checks if any pending reports are being requested, and builds one if needed. ++ */ ++ public static void reportTimings() { ++ if (Timings.requestingReport.isEmpty()) { ++ return; ++ } ++ TimingsReportListener listeners = new TimingsReportListener(Timings.requestingReport); ++ listeners.addConsoleIfNeeded(); ++ ++ Timings.requestingReport.clear(); ++ long now = System.currentTimeMillis(); ++ final long lastReportDiff = now - lastReport; ++ if (lastReportDiff < 60000) { ++ listeners.sendMessage(text("Please wait at least 1 minute in between Timings reports. (" + (int)((60000 - lastReportDiff) / 1000) + " seconds)", NamedTextColor.RED)); ++ listeners.done(); ++ return; ++ } ++ final long lastStartDiff = now - TimingsManager.timingStart; ++ if (lastStartDiff < 180000) { ++ listeners.sendMessage(text("Please wait at least 3 minutes before generating a Timings report. Unlike Timings v1, v2 benefits from longer timings and is not as useful with short timings. (" + (int)((180000 - lastStartDiff) / 1000) + " seconds)", NamedTextColor.RED)); ++ listeners.done(); ++ return; ++ } ++ listeners.sendMessage(text("Preparing Timings Report...", NamedTextColor.GREEN)); ++ lastReport = now; ++ Map parent = createObject( ++ // Get some basic system details about the server ++ pair("version", Bukkit.getVersion()), ++ pair("maxplayers", Bukkit.getMaxPlayers()), ++ pair("start", TimingsManager.timingStart / 1000), ++ pair("end", System.currentTimeMillis() / 1000), ++ pair("online-mode", Bukkit.getServer().getOnlineMode()), ++ pair("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000), ++ pair("datapacks", toArrayMapper(MinecraftServer.getServer().getPackRepository().getSelectedPacks(), pack -> { ++ return PlainTextComponentSerializer.plainText().serialize(PaperAdventure.asAdventure(pack.getChatLink(true))); ++ })) ++ ); ++ if (!TimingsManager.privacy) { ++ appendObjectData(parent, ++ pair("server", Bukkit.getUnsafe().getTimingsServerName()), ++ pair("motd", Bukkit.getServer().getMotd()), ++ pair("icon", Bukkit.getServer().getServerIcon().getData()) ++ ); ++ } ++ ++ final Runtime runtime = Runtime.getRuntime(); ++ RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); ++ ++ OperatingSystemMXBean osInfo = ManagementFactory.getOperatingSystemMXBean(); ++ ++ HardwareAbstractionLayer hardwareInfo = new SystemInfo().getHardware(); ++ ++ parent.put("system", createObject( ++ pair("timingcost", getCost()), ++ pair("loadavg", osInfo.getSystemLoadAverage()), ++ pair("name", System.getProperty("os.name")), ++ pair("version", System.getProperty("os.version")), ++ pair("jvmversion", System.getProperty("java.version")), ++ pair("jvmvendor", System.getProperty("java.vendor")), ++ pair("jvmvendorversion", System.getProperty("java.vendor.version")), ++ pair("arch", System.getProperty("os.arch")), ++ pair("maxmem", runtime.maxMemory()), ++ pair("memory", createObject( ++ pair("heap", ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().toString()), ++ pair("nonheap", ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().toString()), ++ pair("finalizing", ManagementFactory.getMemoryMXBean().getObjectPendingFinalizationCount()) ++ )), ++ pair("cpu", runtime.availableProcessors()), ++ pair("cpuname", hardwareInfo.getProcessor().getProcessorIdentifier().getName().trim()), ++ pair("runtime", runtimeBean.getUptime()), ++ pair("flags", StringUtils.join(runtimeBean.getInputArguments(), " ")), ++ pair("gc", toObjectMapper(ManagementFactory.getGarbageCollectorMXBeans(), input -> pair(input.getName(), toArray(input.getCollectionCount(), input.getCollectionTime())))) ++ ) ++ ); ++ ++ parent.put("worlds", toObjectMapper(MinecraftServer.getServer().getAllLevels(), world -> { ++ if (world.getWorld().getName().equals("worldeditregentempworld")) return null; ++ return pair(world.getWorld().getName(), createObject( ++ pair("gamerules", toObjectMapper(world.getWorld().getGameRules(), rule -> { ++ return pair(rule, world.getWorld().getGameRuleValue(rule)); ++ })), ++ pair("ticking-distance", world.getWorld().getSimulationDistance()), ++ pair("no-ticking-distance", world.getWorld().getViewDistance()), ++ pair("sending-distance", world.getWorld().getSendViewDistance()) ++ )); ++ })); ++ ++ Set<Material> tileEntityTypeSet = Sets.newHashSet(); ++ Set<EntityType> entityTypeSet = Sets.newHashSet(); ++ ++ int size = HISTORY.size(); ++ TimingHistory[] history = new TimingHistory[size + 1]; ++ int i = 0; ++ for (TimingHistory timingHistory : HISTORY) { ++ tileEntityTypeSet.addAll(timingHistory.tileEntityTypeSet); ++ entityTypeSet.addAll(timingHistory.entityTypeSet); ++ history[i++] = timingHistory; ++ } ++ ++ history[i] = new TimingHistory(); // Current snapshot ++ tileEntityTypeSet.addAll(history[i].tileEntityTypeSet); ++ entityTypeSet.addAll(history[i].entityTypeSet); ++ ++ ++ Map handlers = createObject(); ++ Map groupData; ++ synchronized (TimingIdentifier.GROUP_MAP) { ++ for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) { ++ synchronized (group.handlers) { ++ for (TimingHandler id : group.handlers) { ++ ++ if (!id.isTimed() && !id.isSpecial()) { ++ continue; ++ } ++ ++ String name = id.identifier.name; ++ if (name.startsWith("##")) { ++ name = name.substring(3); ++ } ++ handlers.put(id.id, toArray( ++ group.id, ++ name ++ )); ++ } ++ } ++ } ++ ++ groupData = toObjectMapper( ++ TimingIdentifier.GROUP_MAP.values(), group -> pair(group.id, group.name)); ++ } ++ ++ parent.put("idmap", createObject( ++ pair("groups", groupData), ++ pair("handlers", handlers), ++ pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), input -> pair(input.getValue(), input.getKey()))), ++ pair("tileentity", ++ toObjectMapper(tileEntityTypeSet, input -> pair(input.ordinal(), input.name()))), ++ pair("entity", ++ toObjectMapper(entityTypeSet, input -> pair(input.ordinal(), input.name()))) ++ )); ++ ++ // Information about loaded plugins ++ ++ parent.put("plugins", toObjectMapper(Bukkit.getPluginManager().getPlugins(), ++ plugin -> pair(plugin.getName(), createObject( ++ pair("version", plugin.getDescription().getVersion()), ++ pair("description", String.valueOf(plugin.getDescription().getDescription()).trim()), ++ pair("website", plugin.getDescription().getWebsite()), ++ pair("authors", StringUtils.join(plugin.getDescription().getAuthors(), ", ")) ++ )))); ++ ++ ++ ++ // Information on the users Config ++ ++ 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)) ++ )); ++ ++ new TimingsExport(listeners, parent, history).start(); ++ } ++ ++ static long getCost() { ++ // Benchmark the users System.nanotime() for cost basis ++ int passes = 100; ++ TimingHandler SAMPLER1 = Timings.ofSafe("Timings Sampler 1"); ++ TimingHandler SAMPLER2 = Timings.ofSafe("Timings Sampler 2"); ++ TimingHandler SAMPLER3 = Timings.ofSafe("Timings Sampler 3"); ++ TimingHandler SAMPLER4 = Timings.ofSafe("Timings Sampler 4"); ++ TimingHandler SAMPLER5 = Timings.ofSafe("Timings Sampler 5"); ++ TimingHandler SAMPLER6 = Timings.ofSafe("Timings Sampler 6"); ++ ++ long start = System.nanoTime(); ++ for (int i = 0; i < passes; i++) { ++ SAMPLER1.startTiming(); ++ SAMPLER2.startTiming(); ++ SAMPLER3.startTiming(); ++ SAMPLER3.stopTiming(); ++ SAMPLER4.startTiming(); ++ SAMPLER5.startTiming(); ++ SAMPLER6.startTiming(); ++ SAMPLER6.stopTiming(); ++ SAMPLER5.stopTiming(); ++ SAMPLER4.stopTiming(); ++ SAMPLER2.stopTiming(); ++ SAMPLER1.stopTiming(); ++ } ++ long timingsCost = (System.nanoTime() - start) / passes / 6; ++ SAMPLER1.reset(true); ++ SAMPLER2.reset(true); ++ SAMPLER3.reset(true); ++ SAMPLER4.reset(true); ++ SAMPLER5.reset(true); ++ SAMPLER6.reset(true); ++ return timingsCost; ++ } ++ ++ private static JSONObject mapAsJSON(ConfigurationSection config, String parentKey) { ++ ++ JSONObject object = new JSONObject(); ++ for (String key : config.getKeys(false)) { ++ String fullKey = (parentKey != null ? parentKey + "." + key : key); ++ if (fullKey.equals("database") || fullKey.equals("settings.bungeecord-addresses") || TimingsManager.hiddenConfigs.contains(fullKey) || key.startsWith("seed-") || key.equals("worldeditregentempworld")) { ++ continue; ++ } ++ final Object val = config.get(key); ++ ++ object.put(key, valAsJSON(val, fullKey)); ++ } ++ return object; ++ } ++ ++ private static Object valAsJSON(Object val, final String parentKey) { ++ if (!(val instanceof MemorySection)) { ++ if (val instanceof List) { ++ Iterable<Object> v = (Iterable<Object>) val; ++ return toArrayMapper(v, input -> valAsJSON(input, parentKey)); ++ } else { ++ return String.valueOf(val); ++ } ++ } else { ++ return mapAsJSON((ConfigurationSection) val, parentKey); ++ } ++ } ++ ++ @Override ++ public void run() { ++ out.put("data", toArrayMapper(history, TimingHistory::export)); ++ ++ ++ String response = null; ++ String timingsURL = null; ++ try { ++ HttpURLConnection con = (HttpURLConnection) new URL(TimingsManager.url + "post").openConnection(); ++ con.setDoOutput(true); ++ String hostName = "BrokenHost"; ++ try { ++ hostName = InetAddress.getLocalHost().getHostName(); ++ } catch (Exception ignored) {} ++ con.setRequestProperty("User-Agent", "Paper/" + Bukkit.getUnsafe().getTimingsServerName() + "/" + hostName); ++ con.setRequestMethod("POST"); ++ con.setInstanceFollowRedirects(false); ++ ++ OutputStream request = new GZIPOutputStream(con.getOutputStream()) {{ ++ this.def.setLevel(7); ++ }}; ++ ++ request.write(JSONValue.toJSONString(out).getBytes("UTF-8")); ++ request.close(); ++ ++ response = getResponse(con); ++ ++ if (con.getResponseCode() != 302) { ++ listeners.sendMessage(text( "Upload Error: " + con.getResponseCode() + ": " + con.getResponseMessage(), NamedTextColor.RED)); ++ listeners.sendMessage(text("Check your logs for more information", NamedTextColor.RED)); ++ if (response != null) { ++ Bukkit.getLogger().log(Level.SEVERE, response); ++ } ++ return; ++ } ++ ++ timingsURL = con.getHeaderField("Location"); ++ listeners.sendMessage(text("View Timings Report: ", NamedTextColor.GREEN).append(text(timingsURL).clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, timingsURL)))); ++ ++ if (response != null && !response.isEmpty()) { ++ Bukkit.getLogger().log(Level.INFO, "Timing Response: " + response); ++ } ++ } catch (IOException ex) { ++ listeners.sendMessage(text("Error uploading timings, check your logs for more information", NamedTextColor.RED)); ++ if (response != null) { ++ Bukkit.getLogger().log(Level.SEVERE, response); ++ } ++ Bukkit.getLogger().log(Level.SEVERE, "Could not paste timings", ex); ++ } finally { ++ this.listeners.done(timingsURL); ++ } ++ } ++ ++ private String getResponse(HttpURLConnection con) throws IOException { ++ InputStream is = null; ++ try { ++ is = con.getInputStream(); ++ ByteArrayOutputStream bos = new ByteArrayOutputStream(); ++ ++ byte[] b = new byte[1024]; ++ int bytesRead; ++ while ((bytesRead = is.read(b)) != -1) { ++ bos.write(b, 0, bytesRead); ++ } ++ return bos.toString(); ++ ++ } catch (IOException ex) { ++ listeners.sendMessage(text("Error uploading timings, check your logs for more information", NamedTextColor.RED)); ++ Bukkit.getLogger().log(Level.WARNING, con.getResponseMessage(), ex); ++ return null; ++ } finally { ++ if (is != null) { ++ is.close(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2f0d9b953802dee821cfde82d22b0567cce8ee91 +--- /dev/null ++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +@@ -0,0 +1,120 @@ ++package co.aikar.timings; ++ ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.storage.PrimaryLevelData; ++ ++/** ++ * Set of timers per world, to track world specific timings. ++ */ ++// TODO: Re-implement missing timers ++@Deprecated(forRemoval = true) ++public class WorldTimingsHandler { ++ public final Timing mobSpawn; ++ public final Timing doChunkUnload; ++ public final Timing doPortalForcer; ++ public final Timing scheduledBlocks; ++ public final Timing scheduledBlocksCleanup; ++ public final Timing scheduledBlocksTicking; ++ public final Timing chunkTicks; ++ public final Timing lightChunk; ++ public final Timing chunkTicksBlocks; ++ public final Timing doVillages; ++ public final Timing doChunkMap; ++ public final Timing doChunkMapUpdate; ++ public final Timing doChunkMapToUpdate; ++ public final Timing doChunkMapSortMissing; ++ public final Timing doChunkMapSortSendToPlayers; ++ public final Timing doChunkMapPlayersNeedingChunks; ++ public final Timing doChunkMapPendingSendToPlayers; ++ public final Timing doChunkMapUnloadChunks; ++ public final Timing doChunkGC; ++ public final Timing doSounds; ++ public final Timing entityRemoval; ++ public final Timing entityTick; ++ public final Timing tileEntityTick; ++ public final Timing tileEntityPending; ++ public final Timing tracker1; ++ public final Timing tracker2; ++ public final Timing doTick; ++ public final Timing tickEntities; ++ public final Timing chunks; ++ public final Timing newEntities; ++ public final Timing raids; ++ public final Timing chunkProviderTick; ++ public final Timing broadcastChunkUpdates; ++ public final Timing countNaturalMobs; ++ ++ public final Timing chunkLoad; ++ public final Timing chunkLoadPopulate; ++ public final Timing syncChunkLoad; ++ public final Timing chunkLoadLevelTimer; ++ public final Timing chunkIO; ++ public final Timing chunkPostLoad; ++ public final Timing worldSave; ++ public final Timing worldSaveChunks; ++ public final Timing worldSaveLevel; ++ public final Timing chunkSaveData; ++ ++ ++ public final Timing miscMobSpawning; ++ ++ public WorldTimingsHandler(Level server) { ++ String name = ((PrimaryLevelData) server.getLevelData()).getLevelName() + " - "; ++ ++ mobSpawn = Timings.ofSafe(name + "mobSpawn"); ++ doChunkUnload = Timings.ofSafe(name + "doChunkUnload"); ++ scheduledBlocks = Timings.ofSafe(name + "Scheduled Blocks"); ++ scheduledBlocksCleanup = Timings.ofSafe(name + "Scheduled Blocks - Cleanup"); ++ scheduledBlocksTicking = Timings.ofSafe(name + "Scheduled Blocks - Ticking"); ++ chunkTicks = Timings.ofSafe(name + "Chunk Ticks"); ++ lightChunk = Timings.ofSafe(name + "Light Chunk"); ++ chunkTicksBlocks = Timings.ofSafe(name + "Chunk Ticks - Blocks"); ++ doVillages = Timings.ofSafe(name + "doVillages"); ++ doChunkMap = Timings.ofSafe(name + "doChunkMap"); ++ doChunkMapUpdate = Timings.ofSafe(name + "doChunkMap - Update"); ++ doChunkMapToUpdate = Timings.ofSafe(name + "doChunkMap - To Update"); ++ doChunkMapSortMissing = Timings.ofSafe(name + "doChunkMap - Sort Missing"); ++ doChunkMapSortSendToPlayers = Timings.ofSafe(name + "doChunkMap - Sort Send To Players"); ++ doChunkMapPlayersNeedingChunks = Timings.ofSafe(name + "doChunkMap - Players Needing Chunks"); ++ doChunkMapPendingSendToPlayers = Timings.ofSafe(name + "doChunkMap - Pending Send To Players"); ++ doChunkMapUnloadChunks = Timings.ofSafe(name + "doChunkMap - Unload Chunks"); ++ doSounds = Timings.ofSafe(name + "doSounds"); ++ doChunkGC = Timings.ofSafe(name + "doChunkGC"); ++ doPortalForcer = Timings.ofSafe(name + "doPortalForcer"); ++ entityTick = Timings.ofSafe(name + "entityTick"); ++ entityRemoval = Timings.ofSafe(name + "entityRemoval"); ++ tileEntityTick = Timings.ofSafe(name + "tileEntityTick"); ++ tileEntityPending = Timings.ofSafe(name + "tileEntityPending"); ++ ++ chunkLoad = Timings.ofSafe(name + "Chunk Load"); ++ chunkLoadPopulate = Timings.ofSafe(name + "Chunk Load - Populate"); ++ syncChunkLoad = Timings.ofSafe(name + "Sync Chunk Load"); ++ chunkLoadLevelTimer = Timings.ofSafe(name + "Chunk Load - Load Level"); ++ chunkIO = Timings.ofSafe(name + "Chunk Load - DiskIO"); ++ chunkPostLoad = Timings.ofSafe(name + "Chunk Load - Post Load"); ++ worldSave = Timings.ofSafe(name + "World Save"); ++ worldSaveLevel = Timings.ofSafe(name + "World Save - Level"); ++ worldSaveChunks = Timings.ofSafe(name + "World Save - Chunks"); ++ chunkSaveData = Timings.ofSafe(name + "Chunk Save - Data"); ++ ++ tracker1 = Timings.ofSafe(name + "tracker stage 1"); ++ tracker2 = Timings.ofSafe(name + "tracker stage 2"); ++ doTick = Timings.ofSafe(name + "doTick"); ++ tickEntities = Timings.ofSafe(name + "tickEntities"); ++ ++ chunks = Timings.ofSafe(name + "Chunks"); ++ newEntities = Timings.ofSafe(name + "New entity registration"); ++ raids = Timings.ofSafe(name + "Raids"); ++ chunkProviderTick = Timings.ofSafe(name + "Chunk provider tick"); ++ broadcastChunkUpdates = Timings.ofSafe(name + "Broadcast chunk updates"); ++ countNaturalMobs = Timings.ofSafe(name + "Count natural mobs"); ++ ++ ++ miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc"); ++ } ++ ++ public static Timing getTickList(ServerLevel worldserver, String timingsType) { ++ return Timings.ofSafe(((PrimaryLevelData) worldserver.getLevelData()).getLevelName() + " - Scheduled " + timingsType); ++ } ++} +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index f7197f1347251a37dd0f6d9ffa2f09bc3a4e1233..d0d36a57ec4896bcb74970f8fb24d8f3e17db133 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -31,7 +31,8 @@ public class PacketUtils { + engine.executeIfPossible(() -> { + if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players + if (listener.shouldHandleMessage(packet)) { +- try { ++ co.aikar.timings.Timing timing = co.aikar.timings.MinecraftTimings.getPacketTiming(packet); // Paper - timings ++ try (co.aikar.timings.Timing ignored = timing.startTiming()) { // Paper - timings + packet.handle(listener); + } catch (Exception exception) { + if (exception instanceof ReportedException) { +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 202a6510d9d093119ff88b910cef6e47fce2e6b8..4137cf4d716680ff1b1ab0b8a3e8f7cb4bae7dbe 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -196,7 +196,7 @@ import org.bukkit.craftbukkit.Main; + import org.bukkit.event.server.ServerLoadEvent; + // CraftBukkit end + +-import org.bukkit.craftbukkit.SpigotTimings; // Spigot ++import co.aikar.timings.MinecraftTimings; // Paper + + public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable { + +@@ -912,6 +912,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + + MinecraftServer.LOGGER.info("Stopping server"); ++ MinecraftTimings.stopServer(); // Paper + // CraftBukkit start + if (this.server != null) { + this.server.disablePlugins(); +@@ -1189,9 +1190,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + private boolean haveTime() { + // CraftBukkit start ++ if (isOversleep) return canOversleep(); // Paper - because of our changes, this logic is broken + return this.forceTicks || this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos); + } + ++ // Paper start ++ boolean isOversleep = false; ++ private boolean canOversleep() { ++ return this.mayHaveDelayedTasks && Util.getNanos() < this.delayedTasksMaxNextTickTimeNanos; ++ } ++ ++ private boolean canSleepForTickNoOversleep() { ++ return this.forceTicks || this.runningTask() || Util.getNanos() < this.nextTickTimeNanos; ++ } ++ // Paper end ++ + private void executeModerately() { + this.runAllTasks(); + java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L); +@@ -1220,9 +1233,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + + protected void waitUntilNextTick() { +- this.runAllTasks(); ++ //this.executeAll(); // Paper - move this into the tick method for timings + this.managedBlock(() -> { +- return !this.haveTime(); ++ return !this.canSleepForTickNoOversleep(); // Paper - move oversleep into full server tick + }); + } + +@@ -1323,9 +1336,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + + public void tickServer(BooleanSupplier shouldKeepTicking) { +- SpigotTimings.serverTickTimer.startTiming(); // Spigot ++ co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper + long i = Util.getNanos(); + ++ // Paper start - move oversleep into full server tick ++ isOversleep = true;MinecraftTimings.serverOversleep.startTiming(); ++ this.managedBlock(() -> { ++ return !this.canOversleep(); ++ }); ++ isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); ++ // Paper end ++ + ++this.tickCount; + this.tickRateManager.tick(); + this.tickChildren(shouldKeepTicking); +@@ -1339,15 +1360,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + if (this.autosavePeriod > 0 && this.ticksUntilAutosave <= 0) { + this.ticksUntilAutosave = this.autosavePeriod; + // CraftBukkit end +- SpigotTimings.worldSaveTimer.startTiming(); // Spigot + MinecraftServer.LOGGER.debug("Autosave started"); + this.profiler.push("save"); + this.saveEverything(true, false, false); + this.profiler.pop(); + MinecraftServer.LOGGER.debug("Autosave finished"); +- SpigotTimings.worldSaveTimer.stopTiming(); // Spigot + } + io.papermc.paper.util.CachedLists.reset(); // Paper ++ // Paper start - move executeAll() into full server tick timing ++ try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { ++ this.runAllTasks(); ++ } ++ // Paper end + this.profiler.push("tallying"); + long j = Util.getNanos() - i; + int k = this.tickCount % 100; +@@ -1359,8 +1383,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.logTickMethodTime(i); + this.profiler.pop(); + org.spigotmc.WatchdogThread.tick(); // Spigot +- SpigotTimings.serverTickTimer.stopTiming(); // Spigot +- org.spigotmc.CustomTimingsHandler.tick(); // Spigot ++ co.aikar.timings.TimingsManager.FULL_SERVER_TICK.stopTiming(); // Paper + } + + private void logTickMethodTime(long tickStartTime) { +@@ -1431,26 +1454,26 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.getPlayerList().getPlayers().forEach((entityplayer) -> { + entityplayer.connection.suspendFlushing(); + }); +- SpigotTimings.schedulerTimer.startTiming(); // Spigot ++ MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper + this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit +- SpigotTimings.schedulerTimer.stopTiming(); // Spigot ++ MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper + this.profiler.push("commandFunctions"); +- SpigotTimings.commandFunctionsTimer.startTiming(); // Spigot ++ MinecraftTimings.commandFunctionsTimer.startTiming(); // Spigot // Paper + this.getFunctions().tick(); +- SpigotTimings.commandFunctionsTimer.stopTiming(); // Spigot ++ MinecraftTimings.commandFunctionsTimer.stopTiming(); // Spigot // Paper + this.profiler.popPush("levels"); + Iterator iterator = this.getAllLevels().iterator(); + + // CraftBukkit start + // Run tasks that are waiting on processing +- SpigotTimings.processQueueTimer.startTiming(); // Spigot ++ MinecraftTimings.processQueueTimer.startTiming(); // Spigot + while (!this.processQueue.isEmpty()) { + this.processQueue.remove().run(); + } +- SpigotTimings.processQueueTimer.stopTiming(); // Spigot ++ MinecraftTimings.processQueueTimer.stopTiming(); // Spigot + +- SpigotTimings.timeUpdateTimer.startTiming(); // Spigot ++ MinecraftTimings.timeUpdateTimer.startTiming(); // Spigot // Paper + // Send time updates to everyone, it will get the right time from the world the player is in. + if (this.tickCount % 20 == 0) { + for (int i = 0; i < this.getPlayerList().players.size(); ++i) { +@@ -1458,7 +1481,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + entityplayer.connection.send(new ClientboundSetTimePacket(entityplayer.level().getGameTime(), entityplayer.getPlayerTime(), entityplayer.level().getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); // Add support for per player time + } + } +- SpigotTimings.timeUpdateTimer.stopTiming(); // Spigot ++ MinecraftTimings.timeUpdateTimer.stopTiming(); // Spigot // Paper + + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); +@@ -1499,24 +1522,24 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + + this.profiler.popPush("connection"); +- SpigotTimings.connectionTimer.startTiming(); // Spigot ++ MinecraftTimings.connectionTimer.startTiming(); // Spigot // Paper + this.getConnection().tick(); +- SpigotTimings.connectionTimer.stopTiming(); // Spigot ++ MinecraftTimings.connectionTimer.stopTiming(); // Spigot // Paper + this.profiler.popPush("players"); +- SpigotTimings.playerListTimer.startTiming(); // Spigot ++ MinecraftTimings.playerListTimer.startTiming(); // Spigot // Paper + this.playerList.tick(); +- SpigotTimings.playerListTimer.stopTiming(); // Spigot ++ MinecraftTimings.playerListTimer.stopTiming(); // Spigot // Paper + if (SharedConstants.IS_RUNNING_IN_IDE && this.tickRateManager.runsNormally()) { + GameTestTicker.SINGLETON.tick(); + } + + this.profiler.popPush("server gui refresh"); + +- SpigotTimings.tickablesTimer.startTiming(); // Spigot ++ MinecraftTimings.tickablesTimer.startTiming(); // Spigot // Paper + for (int i = 0; i < this.tickables.size(); ++i) { + ((Runnable) this.tickables.get(i)).run(); + } +- SpigotTimings.tickablesTimer.stopTiming(); // Spigot ++ MinecraftTimings.tickablesTimer.stopTiming(); // Spigot // Paper + + this.profiler.popPush("send chunks"); + iterator = this.playerList.getPlayers().iterator(); +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index d38ecbc208c34509eaf77751ac45d9ef51a5dce8..b51c3f8c485496734ea58c15377a1215a334c765 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -65,10 +65,11 @@ import org.apache.logging.log4j.Level; + import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.io.IoBuilder; + import org.bukkit.command.CommandSender; +-import org.bukkit.craftbukkit.SpigotTimings; // Spigot ++import co.aikar.timings.MinecraftTimings; // Paper + import org.bukkit.craftbukkit.util.TerminalCompletionHandler; + import org.bukkit.craftbukkit.util.TerminalConsoleWriterThread; + import org.bukkit.event.server.ServerCommandEvent; ++import org.bukkit.craftbukkit.util.Waitable; // Paper + import org.bukkit.event.server.RemoteServerCommandEvent; + // CraftBukkit end + +@@ -439,7 +440,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + public void handleConsoleInputs() { +- SpigotTimings.serverCommandTimer.startTiming(); // Spigot ++ MinecraftTimings.serverCommandTimer.startTiming(); // Spigot + while (!this.consoleInput.isEmpty()) { + ConsoleInput servercommand = (ConsoleInput) this.consoleInput.remove(0); + +@@ -454,7 +455,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + // CraftBukkit end + } + +- SpigotTimings.serverCommandTimer.stopTiming(); // Spigot ++ MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot + } + + @Override +@@ -712,7 +713,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + public String runCommand(RconConsoleSource rconConsoleSource, String s) { ++ Waitable[] waitableArray = new Waitable[1]; // Paper + rconConsoleSource.prepareForCommand(); ++ final java.util.concurrent.atomic.AtomicReference<String> command = new java.util.concurrent.atomic.AtomicReference<>(s); // Paper + this.executeBlocking(() -> { + CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack(); + RemoteServerCommandEvent event = new RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s); +@@ -720,9 +723,39 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + if (event.isCancelled()) { + return; + } ++ // Paper start ++ command.set(event.getCommand()); ++ if (event.getCommand().toLowerCase(java.util.Locale.ROOT).startsWith("timings") && event.getCommand().toLowerCase(java.util.Locale.ROOT).matches("timings (report|paste|get|merged|seperate)")) { ++ org.bukkit.command.BufferedCommandSender sender = new org.bukkit.command.BufferedCommandSender(); ++ Waitable<String> waitable = new Waitable<>() { ++ @Override ++ protected String evaluate() { ++ return sender.getBuffer(); ++ } ++ }; ++ waitableArray[0] = waitable; ++ co.aikar.timings.Timings.generateReport(new co.aikar.timings.TimingsReportListener(sender, waitable)); ++ } else { ++ // Paper end + ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper); + this.server.dispatchServerCommand(event.getSender(), serverCommand); ++ } // Paper + }); ++ // Paper start ++ if (waitableArray[0] != null) { ++ //noinspection unchecked ++ Waitable<String> waitable = waitableArray[0]; ++ try { ++ return waitable.get(); ++ } catch (java.util.concurrent.ExecutionException e) { ++ throw new RuntimeException("Exception processing rcon command " + command.get(), e.getCause()); ++ } catch (InterruptedException e) { ++ Thread.currentThread().interrupt(); // Maintain interrupted state ++ throw new RuntimeException("Interrupted processing rcon command " + command.get(), e); ++ } ++ ++ } ++ // Paper end + return rconConsoleSource.getCommandResponse(); + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index ce0d22452171857e3cf070bf01450a7653ec7142..6581566ca4e4fac0691e4f5851f8895d9ac7a38f 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -1,8 +1,10 @@ + package net.minecraft.server.level; + ++import co.aikar.timings.Timing; // Paper + import com.google.common.collect.ImmutableList; + import com.google.common.collect.ImmutableList.Builder; + import com.google.common.collect.Iterables; ++import com.google.common.collect.ComparisonChain; // Paper + import com.google.common.collect.Lists; + import com.google.common.collect.Queues; + import com.google.common.collect.Sets; +@@ -1363,6 +1365,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + List<ServerPlayer> list = Lists.newArrayList(); + List<ServerPlayer> list1 = this.level.players(); + ObjectIterator objectiterator = this.entityMap.values().iterator(); ++ level.timings.tracker1.startTiming(); // Paper + + ChunkMap.TrackedEntity playerchunkmap_entitytracker; + +@@ -1387,14 +1390,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + playerchunkmap_entitytracker.serverEntity.sendChanges(); + } + } ++ level.timings.tracker1.stopTiming(); // Paper + + if (!list.isEmpty()) { + objectiterator = this.entityMap.values().iterator(); + ++ level.timings.tracker2.startTiming(); // Paper + while (objectiterator.hasNext()) { + playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); + playerchunkmap_entitytracker.updatePlayers(list); + } ++ level.timings.tracker2.stopTiming(); // Paper + } + + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index ff68eff051c5f50b20b26d33001e12741dd3000e..946cf6e186c6b283e705aa9a8cc9726889ebb954 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -269,13 +269,15 @@ public class ServerChunkCache extends ChunkSource { + } + + gameprofilerfiller.incrementCounter("getChunkCacheMiss"); +- this.level.timings.syncChunkLoadTimer.startTiming(); // Spigot + CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create); + ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor; + + Objects.requireNonNull(completablefuture); ++ if (!completablefuture.isDone()) { // Paper ++ this.level.timings.syncChunkLoad.startTiming(); // Paper + chunkproviderserver_b.managedBlock(completablefuture::isDone); +- this.level.timings.syncChunkLoadTimer.stopTiming(); // Spigot ++ this.level.timings.syncChunkLoad.stopTiming(); // Paper ++ } // Paper + ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join(); + ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error + +@@ -440,7 +442,9 @@ public class ServerChunkCache extends ChunkSource { + + public void save(boolean flush) { + this.runDistanceManagerUpdates(); ++ try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings + this.chunkMap.saveAllChunks(flush); ++ } // Paper - Timings + } + + @Override +@@ -482,10 +486,10 @@ public class ServerChunkCache extends ChunkSource { + this.level.timings.doChunkMap.stopTiming(); // Spigot + this.level.getProfiler().popPush("chunks"); + if (tickChunks) { ++ this.level.timings.chunks.startTiming(); // Paper - timings + this.tickChunks(); +- this.level.timings.tracker.startTiming(); // Spigot ++ this.level.timings.chunks.stopTiming(); // Paper - timings + this.chunkMap.tick(); +- this.level.timings.tracker.stopTiming(); // Spigot + } + + this.level.timings.doChunkUnload.startTiming(); // Spigot +@@ -508,6 +512,7 @@ public class ServerChunkCache extends ChunkSource { + gameprofilerfiller.push("filteringLoadedChunks"); + List<ServerChunkCache.ChunkAndHolder> list = Lists.newArrayListWithCapacity(this.chunkMap.size()); + Iterator iterator = this.chunkMap.getChunks().iterator(); ++ if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper + + while (iterator.hasNext()) { + ChunkHolder playerchunk = (ChunkHolder) iterator.next(); +@@ -520,8 +525,10 @@ public class ServerChunkCache extends ChunkSource { + + if (this.level.tickRateManager().runsNormally()) { + gameprofilerfiller.popPush("naturalSpawnCount"); ++ this.level.timings.countNaturalMobs.startTiming(); // Paper - timings + int k = this.distanceManager.getNaturalSpawnChunkCount(); + NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(k, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); ++ this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings + + this.lastSpawnState = spawnercreature_d; + gameprofilerfiller.popPush("spawnAndTick"); +@@ -544,22 +551,25 @@ public class ServerChunkCache extends ChunkSource { + } + + if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { +- this.level.timings.doTickTiles.startTiming(); // Spigot + this.level.tickChunk(chunk1, l); +- this.level.timings.doTickTiles.stopTiming(); // Spigot + } + } + } ++ this.level.timings.chunkTicks.stopTiming(); // Paper + + gameprofilerfiller.popPush("customSpawners"); + if (flag) { ++ try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings + this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); ++ } // Paper - timings + } + } + + gameprofilerfiller.popPush("broadcast"); + list.forEach((chunkproviderserver_a1) -> { ++ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing + chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk); ++ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing + }); + gameprofilerfiller.pop(); + gameprofilerfiller.pop(); +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index eea8bafd98e3a8d82b3216488537ab898cc4ae7a..9675d91e4e7ed46147c3f7a11dd65122fe998dc2 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1,6 +1,8 @@ + package net.minecraft.server.level; + + import com.google.common.annotations.VisibleForTesting; ++import co.aikar.timings.TimingHistory; // Paper ++import co.aikar.timings.Timings; // Paper + import com.google.common.collect.Lists; + import com.mojang.datafixers.DataFixer; + import com.mojang.datafixers.util.Pair; +@@ -173,7 +175,6 @@ import net.minecraft.world.ticks.LevelTicks; + import org.slf4j.Logger; + import org.bukkit.Bukkit; + import org.bukkit.WeatherType; +-import org.bukkit.craftbukkit.SpigotTimings; // Spigot + import org.bukkit.craftbukkit.event.CraftEventFactory; + import org.bukkit.craftbukkit.generator.CustomWorldChunkManager; + import org.bukkit.craftbukkit.util.WorldUUID; +@@ -478,7 +479,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + gameprofilerfiller.popPush("tickPending"); +- this.timings.doTickPending.startTiming(); // Spigot ++ this.timings.scheduledBlocks.startTiming(); // Paper + if (!this.isDebug() && flag) { + j = this.getGameTime(); + gameprofilerfiller.push("blockTicks"); +@@ -487,15 +488,19 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.fluidTicks.tick(j, 65536, this::tickFluid); + gameprofilerfiller.pop(); + } +- this.timings.doTickPending.stopTiming(); // Spigot ++ this.timings.scheduledBlocks.stopTiming(); // Paper + + gameprofilerfiller.popPush("raid"); + if (flag) { ++ this.timings.raids.startTiming(); // Paper - timings + this.raids.tick(); ++ this.timings.raids.stopTiming(); // Paper - timings + } + + gameprofilerfiller.popPush("chunkSource"); ++ this.timings.chunkProviderTick.startTiming(); // Paper - timings + this.getChunkSource().tick(shouldKeepTicking, true); ++ this.timings.chunkProviderTick.stopTiming(); // Paper - timings + gameprofilerfiller.popPush("blockEvents"); + if (flag) { + this.timings.doSounds.startTiming(); // Spigot +@@ -648,6 +653,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + gameprofilerfiller.popPush("tickBlocks"); ++ timings.chunkTicksBlocks.startTiming(); // Paper + if (randomTickSpeed > 0) { + LevelChunkSection[] achunksection = chunk.getSections(); + +@@ -680,6 +686,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + ++ timings.chunkTicksBlocks.stopTiming(); // Paper + gameprofilerfiller.pop(); + } + +@@ -956,14 +963,22 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public void tickNonPassenger(Entity entity) { ++ ++TimingHistory.entityTicks; // Paper - timings + // Spigot start ++ co.aikar.timings.Timing timer; // Paper + if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { + entity.tickCount++; ++ timer = entity.getType().inactiveTickTimer.startTiming(); try { // Paper - timings + entity.inactiveTick(); ++ } finally { timer.stopTiming(); } // Paper + return; + } + // Spigot end +- entity.tickTimer.startTiming(); // Spigot ++ // Paper start- timings ++ TimingHistory.activatedEntityTicks++; ++ timer = entity.getVehicle() != null ? entity.getType().passengerTickTimer.startTiming() : entity.getType().tickTimer.startTiming(); ++ try { ++ // Paper end - timings + entity.setOldPosAndRot(); + ProfilerFiller gameprofilerfiller = this.getProfiler(); + +@@ -982,7 +997,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.tickPassenger(entity, entity1); + } +- entity.tickTimer.stopTiming(); // Spigot ++ } finally { timer.stopTiming(); } // Paper - timings + + } + +@@ -1024,6 +1039,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + if (!savingDisabled) { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit ++ try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper + if (progressListener != null) { + progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); + } +@@ -1033,7 +1049,10 @@ public class ServerLevel extends Level implements WorldGenLevel { + progressListener.progressStage(Component.translatable("menu.savingChunks")); + } + ++ timings.worldSaveChunks.startTiming(); // Paper + chunkproviderserver.save(flush); ++ timings.worldSaveChunks.stopTiming(); // Paper ++ }// Paper + if (flush) { + this.entityManager.saveAll(); + } else { +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 27cf5dceba5835f94f5397ec011f409e7b226ad5..4f50e2f5de529813c269b7670a47e06306575b60 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -330,7 +330,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + @Override + public void tick() { +- org.bukkit.craftbukkit.SpigotTimings.playerConnectionTimer.startTiming(); // Spigot + if (this.ackBlockChangesUpTo > -1) { + this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); + this.ackBlockChangesUpTo = -1; +@@ -397,7 +396,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 + this.disconnect((Component) Component.translatable("multiplayer.disconnect.idling")); + } +- org.bukkit.craftbukkit.SpigotTimings.playerConnectionTimer.stopTiming(); // Spigot + + } + +@@ -2103,7 +2101,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + private void handleCommand(String s) { +- org.bukkit.craftbukkit.SpigotTimings.playerCommandTimer.startTiming(); // Spigot ++ co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper + if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot + this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s); + +@@ -2113,7 +2111,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.cserver.getPluginManager().callEvent(event); + + if (event.isCancelled()) { +- org.bukkit.craftbukkit.SpigotTimings.playerCommandTimer.stopTiming(); // Spigot ++ co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper + return; + } + +@@ -2126,7 +2124,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + return; + } finally { +- org.bukkit.craftbukkit.SpigotTimings.playerCommandTimer.stopTiming(); // Spigot ++ co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper + } + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 9807c5b2b248a62a476bfe3ae023d57d35811049..62174dae20bd9ff092238f1437f7e2b0114ef83b 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -1,5 +1,6 @@ + package net.minecraft.server.players; + ++import co.aikar.timings.MinecraftTimings; + import com.google.common.collect.Lists; + import com.google.common.collect.Maps; + import com.google.common.collect.Sets; +@@ -1007,10 +1008,11 @@ public abstract class PlayerList { + } + + public void saveAll() { ++ MinecraftTimings.savePlayers.startTiming(); // Paper + for (int i = 0; i < this.players.size(); ++i) { + this.save((ServerPlayer) this.players.get(i)); + } +- ++ MinecraftTimings.savePlayers.stopTiming(); // Paper + } + + public UserWhiteList getWhiteList() { +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 6041033b3ea201bde1a73ce4e429e8b80e05e2eb..4f321f13352636999c3abc5332e50c747fb45cc9 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -140,7 +140,6 @@ import org.bukkit.command.CommandSender; + import org.bukkit.entity.Hanging; + import org.bukkit.entity.LivingEntity; + import org.bukkit.entity.Vehicle; +-import org.spigotmc.CustomTimingsHandler; // Spigot + import org.bukkit.event.entity.EntityCombustByEntityEvent; + import org.bukkit.event.hanging.HangingBreakByEntityEvent; + import org.bukkit.event.vehicle.VehicleBlockCollisionEvent; +@@ -323,7 +322,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // Marks an entity, that it was removed by a plugin via Entity#remove + // Main use case currently is for SPIGOT-7487, preventing dropping of leash when leash is removed + public boolean pluginRemoved = false; +- public CustomTimingsHandler tickTimer = org.bukkit.craftbukkit.SpigotTimings.getEntityTimings(this); // Spigot + // Spigot start + public final org.spigotmc.ActivationRange.ActivationType activationType = org.spigotmc.ActivationRange.initializeEntityActivationType(this); + public final boolean defaultActivationState; +@@ -840,7 +838,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public void move(MoverType movementType, Vec3 movement) { +- org.bukkit.craftbukkit.SpigotTimings.entityMoveTimer.startTiming(); // Spigot + if (this.noPhysics) { + this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); + } else { +@@ -1001,7 +998,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.level().getProfiler().pop(); + } + } +- org.bukkit.craftbukkit.SpigotTimings.entityMoveTimer.stopTiming(); // Spigot + } + + private boolean isStateClimbable(BlockState state) { +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index e465aaa4fd29b4966ea8d88316c6d8f217da2e73..474f020371bb9e5fd2c5b22e44d7902977c4fc18 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -339,6 +339,15 @@ public class EntityType<T extends Entity> implements FeatureElement, EntityTypeT + } + + public EntityType(EntityType.EntityFactory<T> factory, MobCategory spawnGroup, boolean saveable, boolean summonable, boolean fireImmune, boolean spawnableFarFromPlayer, ImmutableSet<Block> canSpawnInside, EntityDimensions dimensions, float spawnBoxScale, int maxTrackDistance, int trackTickInterval, FeatureFlagSet requiredFeatures) { ++ // Paper start ++ this(factory, spawnGroup, saveable, summonable, fireImmune, spawnableFarFromPlayer, canSpawnInside, dimensions, spawnBoxScale, maxTrackDistance, trackTickInterval, requiredFeatures, "custom"); ++ } ++ public EntityType(EntityType.EntityFactory<T> factory, MobCategory spawnGroup, boolean saveable, boolean summonable, boolean fireImmune, boolean spawnableFarFromPlayer, ImmutableSet<Block> canSpawnInside, EntityDimensions dimensions, float spawnBoxScale, int maxTrackDistance, int trackTickInterval, FeatureFlagSet requiredFeatures, String id) { ++ this.tickTimer = co.aikar.timings.MinecraftTimings.getEntityTimings(id, "tick"); ++ this.inactiveTickTimer = co.aikar.timings.MinecraftTimings.getEntityTimings(id, "inactiveTick"); ++ this.passengerTickTimer = co.aikar.timings.MinecraftTimings.getEntityTimings(id, "passengerTick"); ++ this.passengerInactiveTickTimer = co.aikar.timings.MinecraftTimings.getEntityTimings(id, "passengerInactiveTick"); ++ // Paper end + this.builtInRegistryHolder = BuiltInRegistries.ENTITY_TYPE.createIntrusiveHolder(this); + this.factory = factory; + this.category = spawnGroup; +@@ -654,6 +663,12 @@ public class EntityType<T extends Entity> implements FeatureElement, EntityTypeT + return this.updateInterval; + } + ++ // Paper start - timings ++ public final co.aikar.timings.Timing tickTimer; ++ public final co.aikar.timings.Timing inactiveTickTimer; ++ public final co.aikar.timings.Timing passengerTickTimer; ++ public final co.aikar.timings.Timing passengerInactiveTickTimer; ++ // Paper end + public boolean trackDeltas() { + return this != EntityType.PLAYER && this != EntityType.LLAMA_SPIT && this != EntityType.WITHER && this != EntityType.BAT && this != EntityType.ITEM_FRAME && this != EntityType.GLOW_ITEM_FRAME && this != EntityType.LEASH_KNOT && this != EntityType.PAINTING && this != EntityType.END_CRYSTAL && this != EntityType.EVOKER_FANGS; + } +@@ -823,7 +838,7 @@ public class EntityType<T extends Entity> implements FeatureElement, EntityTypeT + Util.fetchChoiceType(References.ENTITY_TREE, id); + } + +- return new EntityType<>(this.factory, this.category, this.serialize, this.summon, this.fireImmune, this.canSpawnFarFromPlayer, this.immuneTo, this.dimensions.withAttachments(this.attachments), this.spawnDimensionsScale, this.clientTrackingRange, this.updateInterval, this.requiredFeatures); ++ return new EntityType<>(this.factory, this.category, this.serialize, this.summon, this.fireImmune, this.canSpawnFarFromPlayer, this.immuneTo, this.dimensions.withAttachments(this.attachments), this.spawnDimensionsScale, this.clientTrackingRange, this.updateInterval, this.requiredFeatures, id); // Paper - add id + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 9deb6b90f3c4281280deb2f609a55923147a675a..fe52b8c57f652fcd49a2282f7a8f1041909b35cf 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -156,7 +156,7 @@ import org.bukkit.event.entity.EntityTeleportEvent; + import org.bukkit.event.player.PlayerItemConsumeEvent; + // CraftBukkit end + +-import org.bukkit.craftbukkit.SpigotTimings; // Spigot ++import co.aikar.timings.MinecraftTimings; // Paper + + public abstract class LivingEntity extends Entity implements Attackable { + +@@ -2949,7 +2949,6 @@ public abstract class LivingEntity extends Entity implements Attackable { + + @Override + public void tick() { +- SpigotTimings.timerEntityBaseTick.startTiming(); // Spigot + super.tick(); + this.updatingUsingItem(); + this.updateSwimAmount(); +@@ -2991,9 +2990,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + if (!this.isRemoved()) { +- SpigotTimings.timerEntityBaseTick.stopTiming(); // Spigot + this.aiStep(); +- SpigotTimings.timerEntityTickRest.startTiming(); // Spigot + } + + double d0 = this.getX() - this.xo; +@@ -3084,7 +3081,6 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.refreshDimensions(); + } + +- SpigotTimings.timerEntityTickRest.stopTiming(); // Spigot + } + + public void detectEquipmentUpdatesPublic() { // CraftBukkit +@@ -3300,7 +3296,6 @@ public abstract class LivingEntity extends Entity implements Attackable { + + this.setDeltaMovement(d0, d1, d2); + this.level().getProfiler().push("ai"); +- SpigotTimings.timerEntityAI.startTiming(); // Spigot + if (this.isImmobile()) { + this.jumping = false; + this.xxa = 0.0F; +@@ -3310,7 +3305,6 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.serverAiStep(); + this.level().getProfiler().pop(); + } +- SpigotTimings.timerEntityAI.stopTiming(); // Spigot + + this.level().getProfiler().pop(); + this.level().getProfiler().push("jump"); +@@ -3350,7 +3344,6 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.resetFallDistance(); + } + +- SpigotTimings.timerEntityAIMove.startTiming(); // Spigot + label104: + { + LivingEntity entityliving = this.getControllingPassenger(); +@@ -3364,7 +3357,6 @@ public abstract class LivingEntity extends Entity implements Attackable { + + this.travel(vec3d1); + } +- SpigotTimings.timerEntityAIMove.stopTiming(); // Spigot + + this.level().getProfiler().pop(); + this.level().getProfiler().push("freezing"); +@@ -3391,9 +3383,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.checkAutoSpinAttack(axisalignedbb, this.getBoundingBox()); + } + +- SpigotTimings.timerEntityAICollision.startTiming(); // Spigot + this.pushEntities(); +- SpigotTimings.timerEntityAICollision.stopTiming(); // Spigot + this.level().getProfiler().pop(); + if (!this.level().isClientSide && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) { + this.hurt(this.damageSources().drown(), 1.0F); +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 55b30e1df4a05802977b0c3f3b518ef0676eae2d..1702cd6aa8b4fc29b8f2539604f6e203bf95d020 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -93,7 +93,6 @@ import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePa + import org.bukkit.Bukkit; + import org.bukkit.craftbukkit.CraftServer; + import org.bukkit.craftbukkit.CraftWorld; +-import org.bukkit.craftbukkit.SpigotTimings; // Spigot + import org.bukkit.craftbukkit.block.CapturedBlockState; + import org.bukkit.craftbukkit.block.CraftBlockState; + import org.bukkit.craftbukkit.block.data.CraftBlockData; +@@ -164,7 +163,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - add paper world config + +- public final SpigotTimings.WorldTimingsHandler timings; // Spigot ++ public final co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; + private org.spigotmc.TickLimiter tileLimiter; +@@ -259,7 +258,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public void onBorderSetDamageSafeZOne(WorldBorder border, double safeZoneRadius) {} + }); + // CraftBukkit end +- this.timings = new SpigotTimings.WorldTimingsHandler(this); // Spigot - code below can generate new world and access timings ++ this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings + this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime); + this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime); + } +@@ -723,15 +722,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + this.timings.tileEntityTick.stopTiming(); // Spigot + this.tickingBlockEntities = false; ++ co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper + gameprofilerfiller.pop(); + this.spigotConfig.currentPrimedTnt = 0; // Spigot + } + + public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) { + try { +- SpigotTimings.tickEntityTimer.startTiming(); // Spigot + tickConsumer.accept(entity); +- SpigotTimings.tickEntityTimer.stopTiming(); // Spigot + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking entity"); + CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being ticked"); +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 d1cfcc8a36964f006f1af6764c52b5ca458b478d..def3e28edc206e0ba41111e26332db468223fb2e 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -88,6 +88,15 @@ public class Block extends BlockBehaviour implements ItemLike { + public static final int UPDATE_LIMIT = 512; + protected final StateDefinition<Block, BlockState> stateDefinition; + private BlockState defaultBlockState; ++ // Paper start ++ public co.aikar.timings.Timing timing; ++ public co.aikar.timings.Timing getTiming() { ++ if (timing == null) { ++ timing = co.aikar.timings.MinecraftTimings.getBlockTiming(this); ++ } ++ return timing; ++ } ++ // Paper end + @Nullable + private String descriptionId; + @Nullable +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 e6c586eb85c6c477a3c130e1e1a37b41f17c30c8..6e35709f2a7c32050908e7e5af5529c9f342b787 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 +@@ -34,10 +34,12 @@ import org.bukkit.inventory.InventoryHolder; + // CraftBukkit end + + import org.spigotmc.CustomTimingsHandler; // Spigot ++import co.aikar.timings.MinecraftTimings; // Paper ++import co.aikar.timings.Timing; // Paper + + public abstract class BlockEntity { + +- public CustomTimingsHandler tickTimer = org.bukkit.craftbukkit.SpigotTimings.getTileEntityTimings(this); // Spigot ++ public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper + // CraftBukkit start - data containers + private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry(); + public CraftPersistentDataContainer persistentDataContainer; +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 7898e1aaf82f096fa74bd3f5859f0f4303ea677f..05d959ccc424aaaa465ec256213f2ec4d44ef8b5 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -710,6 +710,7 @@ public class LevelChunk extends ChunkAccess { + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration)); + + if (this.needsDecoration) { ++ try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper + this.needsDecoration = false; + java.util.Random random = new java.util.Random(); + random.setSeed(this.level.getSeed()); +@@ -729,6 +730,7 @@ public class LevelChunk extends ChunkAccess { + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); ++ } // Paper + } + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index 46a090123e205394791cdbde2af84c58ce55f7e1..47f5f3d58bb3bf85cf35f9baae77df7fab5c844f 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -472,13 +472,10 @@ public class ChunkSerializer { + ListTag nbttaglist1 = ChunkSerializer.getListOfCompoundsOrNull(nbt, "block_entities"); + + return nbttaglist == null && nbttaglist1 == null ? null : (chunk) -> { +- world.timings.syncChunkLoadEntitiesTimer.startTiming(); // Spigot + if (nbttaglist != null) { + world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world)); + } +- world.timings.syncChunkLoadEntitiesTimer.stopTiming(); // Spigot + +- world.timings.syncChunkLoadTileEntitiesTimer.startTiming(); // Spigot + if (nbttaglist1 != null) { + for (int i = 0; i < nbttaglist1.size(); ++i) { + CompoundTag nbttagcompound1 = nbttaglist1.getCompound(i); +@@ -496,7 +493,6 @@ public class ChunkSerializer { + } + } + } +- world.timings.syncChunkLoadTileEntitiesTimer.stopTiming(); // Spigot + + }; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index d1699fcca66bcfbbe8fcc426802cb766cf1e580b..de55611daeb6d55f69c4cb72137eb7b050e727f7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -376,7 +376,7 @@ public final class CraftServer implements Server { + this.saveCommandsConfig(); + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); +- this.pluginManager.useTimings(this.configuration.getBoolean("settings.plugin-profiling")); ++ //this.pluginManager.useTimings(this.configuration.getBoolean("settings.plugin-profiling")); // Paper - we already moved this + this.overrideSpawnLimits(); + console.autosavePeriod = this.configuration.getInt("ticks-per.autosave"); + this.warningState = WarningState.value(this.configuration.getString("settings.deprecated-verbose")); +@@ -2612,12 +2612,31 @@ public final class CraftServer implements Server { + private final org.bukkit.Server.Spigot spigot = new org.bukkit.Server.Spigot() + { + ++ @Deprecated + @Override + public YamlConfiguration getConfig() + { + return org.spigotmc.SpigotConfig.config; + } + ++ @Override ++ public YamlConfiguration getBukkitConfig() ++ { ++ return configuration; ++ } ++ ++ @Override ++ public YamlConfiguration getSpigotConfig() ++ { ++ return org.spigotmc.SpigotConfig.config; ++ } ++ ++ @Override ++ public YamlConfiguration getPaperConfig() ++ { ++ return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console); ++ } ++ + @Override + public void restart() { + org.spigotmc.RestartCommand.restart(); +diff --git a/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java b/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java +deleted file mode 100644 +index b0ffa23faf62629043dfd613315eaf9c5fcc2cfe..0000000000000000000000000000000000000000 +--- a/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java ++++ /dev/null +@@ -1,163 +0,0 @@ +-package org.bukkit.craftbukkit; +- +-import java.util.HashMap; +-import net.minecraft.world.entity.Entity; +-import net.minecraft.world.level.Level; +-import net.minecraft.world.level.block.entity.BlockEntity; +-import net.minecraft.world.level.storage.PrimaryLevelData; +-import org.bukkit.craftbukkit.scheduler.CraftTask; +-import org.bukkit.plugin.java.JavaPluginLoader; +-import org.bukkit.scheduler.BukkitTask; +-import org.spigotmc.CustomTimingsHandler; +- +-public class SpigotTimings { +- +- public static final CustomTimingsHandler serverTickTimer = new CustomTimingsHandler("** Full Server Tick"); +- public static final CustomTimingsHandler playerListTimer = new CustomTimingsHandler("Player List"); +- public static final CustomTimingsHandler commandFunctionsTimer = new CustomTimingsHandler("Command Functions"); +- public static final CustomTimingsHandler connectionTimer = new CustomTimingsHandler("Connection Handler"); +- public static final CustomTimingsHandler playerConnectionTimer = new CustomTimingsHandler("** PlayerConnection"); +- public static final CustomTimingsHandler tickablesTimer = new CustomTimingsHandler("Tickables"); +- public static final CustomTimingsHandler schedulerTimer = new CustomTimingsHandler("Scheduler"); +- public static final CustomTimingsHandler timeUpdateTimer = new CustomTimingsHandler("Time Update"); +- public static final CustomTimingsHandler serverCommandTimer = new CustomTimingsHandler("Server Command"); +- public static final CustomTimingsHandler worldSaveTimer = new CustomTimingsHandler("World Save"); +- +- public static final CustomTimingsHandler entityMoveTimer = new CustomTimingsHandler("** entityMove"); +- public static final CustomTimingsHandler tickEntityTimer = new CustomTimingsHandler("** tickEntity"); +- public static final CustomTimingsHandler activatedEntityTimer = new CustomTimingsHandler("** activatedTickEntity"); +- public static final CustomTimingsHandler tickTileEntityTimer = new CustomTimingsHandler("** tickTileEntity"); +- +- public static final CustomTimingsHandler timerEntityBaseTick = new CustomTimingsHandler("** livingEntityBaseTick"); +- public static final CustomTimingsHandler timerEntityAI = new CustomTimingsHandler("** livingEntityAI"); +- public static final CustomTimingsHandler timerEntityAICollision = new CustomTimingsHandler("** livingEntityAICollision"); +- public static final CustomTimingsHandler timerEntityAIMove = new CustomTimingsHandler("** livingEntityAIMove"); +- public static final CustomTimingsHandler timerEntityTickRest = new CustomTimingsHandler("** livingEntityTickRest"); +- +- public static final CustomTimingsHandler processQueueTimer = new CustomTimingsHandler("processQueue"); +- public static final CustomTimingsHandler schedulerSyncTimer = new CustomTimingsHandler("** Scheduler - Sync Tasks", JavaPluginLoader.pluginParentTimer); +- +- public static final CustomTimingsHandler playerCommandTimer = new CustomTimingsHandler("** playerCommand"); +- +- public static final CustomTimingsHandler entityActivationCheckTimer = new CustomTimingsHandler("entityActivationCheck"); +- public static final CustomTimingsHandler checkIfActiveTimer = new CustomTimingsHandler("** checkIfActive"); +- +- public static final HashMap<String, CustomTimingsHandler> entityTypeTimingMap = new HashMap<String, CustomTimingsHandler>(); +- public static final HashMap<String, CustomTimingsHandler> tileEntityTypeTimingMap = new HashMap<String, CustomTimingsHandler>(); +- public static final HashMap<String, CustomTimingsHandler> pluginTaskTimingMap = new HashMap<String, CustomTimingsHandler>(); +- +- /** +- * Gets a timer associated with a plugins tasks. +- * @param task +- * @param period +- * @return +- */ +- public static CustomTimingsHandler getPluginTaskTimings(BukkitTask task, long period) { +- if (!task.isSync()) { +- return null; +- } +- String plugin; +- final CraftTask ctask = (CraftTask) task; +- +- if (task.getOwner() != null) { +- plugin = task.getOwner().getDescription().getFullName(); +- } else { +- plugin = "Unknown"; +- } +- String taskname = ctask.getTaskName(); +- +- String name = "Task: " + plugin + " Runnable: " + taskname; +- if (period > 0) { +- name += "(interval:" + period + ")"; +- } else { +- name += "(Single)"; +- } +- CustomTimingsHandler result = SpigotTimings.pluginTaskTimingMap.get(name); +- if (result == null) { +- result = new CustomTimingsHandler(name, SpigotTimings.schedulerSyncTimer); +- SpigotTimings.pluginTaskTimingMap.put(name, result); +- } +- return result; +- } +- +- /** +- * Get a named timer for the specified entity type to track type specific timings. +- * @param entity +- * @return +- */ +- public static CustomTimingsHandler getEntityTimings(Entity entity) { +- String entityType = entity.getClass().getName(); +- CustomTimingsHandler result = SpigotTimings.entityTypeTimingMap.get(entityType); +- if (result == null) { +- result = new CustomTimingsHandler("** tickEntity - " + entity.getClass().getSimpleName(), SpigotTimings.activatedEntityTimer); +- SpigotTimings.entityTypeTimingMap.put(entityType, result); +- } +- return result; +- } +- +- /** +- * Get a named timer for the specified tile entity type to track type specific timings. +- * @param entity +- * @return +- */ +- public static CustomTimingsHandler getTileEntityTimings(BlockEntity entity) { +- String entityType = entity.getClass().getName(); +- CustomTimingsHandler result = SpigotTimings.tileEntityTypeTimingMap.get(entityType); +- if (result == null) { +- result = new CustomTimingsHandler("** tickTileEntity - " + entity.getClass().getSimpleName(), SpigotTimings.tickTileEntityTimer); +- SpigotTimings.tileEntityTypeTimingMap.put(entityType, result); +- } +- return result; +- } +- +- /** +- * Set of timers per world, to track world specific timings. +- */ +- public static class WorldTimingsHandler { +- public final CustomTimingsHandler mobSpawn; +- public final CustomTimingsHandler doChunkUnload; +- public final CustomTimingsHandler doTickPending; +- public final CustomTimingsHandler doTickTiles; +- public final CustomTimingsHandler doChunkMap; +- public final CustomTimingsHandler doSounds; +- public final CustomTimingsHandler entityTick; +- public final CustomTimingsHandler tileEntityTick; +- public final CustomTimingsHandler tileEntityPending; +- public final CustomTimingsHandler tracker; +- public final CustomTimingsHandler doTick; +- public final CustomTimingsHandler tickEntities; +- +- public final CustomTimingsHandler syncChunkLoadTimer; +- public final CustomTimingsHandler syncChunkLoadStructuresTimer; +- public final CustomTimingsHandler syncChunkLoadEntitiesTimer; +- public final CustomTimingsHandler syncChunkLoadTileEntitiesTimer; +- public final CustomTimingsHandler syncChunkLoadTileTicksTimer; +- public final CustomTimingsHandler syncChunkLoadPostTimer; +- +- public WorldTimingsHandler(Level server) { +- String name = ((PrimaryLevelData) server.levelData).getLevelName() + " - "; +- +- this.mobSpawn = new CustomTimingsHandler("** " + name + "mobSpawn"); +- this.doChunkUnload = new CustomTimingsHandler("** " + name + "doChunkUnload"); +- this.doTickPending = new CustomTimingsHandler("** " + name + "doTickPending"); +- this.doTickTiles = new CustomTimingsHandler("** " + name + "doTickTiles"); +- this.doChunkMap = new CustomTimingsHandler("** " + name + "doChunkMap"); +- this.doSounds = new CustomTimingsHandler("** " + name + "doSounds"); +- this.entityTick = new CustomTimingsHandler("** " + name + "entityTick"); +- this.tileEntityTick = new CustomTimingsHandler("** " + name + "tileEntityTick"); +- this.tileEntityPending = new CustomTimingsHandler("** " + name + "tileEntityPending"); +- +- this.syncChunkLoadTimer = new CustomTimingsHandler("** " + name + "syncChunkLoad"); +- this.syncChunkLoadStructuresTimer = new CustomTimingsHandler("** " + name + "chunkLoad - Structures"); +- this.syncChunkLoadEntitiesTimer = new CustomTimingsHandler("** " + name + "chunkLoad - Entities"); +- this.syncChunkLoadTileEntitiesTimer = new CustomTimingsHandler("** " + name + "chunkLoad - TileEntities"); +- this.syncChunkLoadTileTicksTimer = new CustomTimingsHandler("** " + name + "chunkLoad - TileTicks"); +- this.syncChunkLoadPostTimer = new CustomTimingsHandler("** " + name + "chunkLoad - Post"); +- +- +- this.tracker = new CustomTimingsHandler(name + "tracker"); +- this.doTick = new CustomTimingsHandler(name + "doTick"); +- this.tickEntities = new CustomTimingsHandler(name + "tickEntities"); +- } +- } +-} +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 64aed98084aeb3f29db301adf3c8c49ee9236a0b..0bea424418984e17193ff107d2b4cf13278a3d3e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2781,6 +2781,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + CraftPlayer.this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundSystemChatPacket(components, position == net.md_5.bungee.api.ChatMessageType.ACTION_BAR)); + } ++ ++ // Paper start ++ @Override ++ public int getPing() ++ { ++ return CraftPlayer.this.getPing(); ++ } ++ // Paper end + }; + + public Player.Spigot spigot() +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index 0f7c3a44acf3c59ae43605e573f9da7f7c594647..a3ccc2da0927cc49e5fcfbd863e648ad0f25cc0d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -1,5 +1,6 @@ + package org.bukkit.craftbukkit.scheduler; + ++import co.aikar.timings.MinecraftTimings; // Paper + import com.google.common.base.Preconditions; + import com.google.common.util.concurrent.ThreadFactoryBuilder; + import java.util.ArrayList; +@@ -196,7 +197,8 @@ public class CraftScheduler implements BukkitScheduler { + } + + public BukkitTask scheduleInternalTask(Runnable run, int delay, String taskName) { +- final CraftTask task = new CraftTask(run, nextId(), taskName); ++ final CraftTask task = new CraftTask(run, nextId(), "Internal - " + (taskName != null ? taskName : "Unknown")); ++ task.internal = true; + return handle(task, delay); + } + +@@ -277,7 +279,7 @@ public class CraftScheduler implements BukkitScheduler { + } + return false; + } +- }); ++ }){{this.timings=co.aikar.timings.MinecraftTimings.getCancelTasksTimer();}}; // Paper + this.handle(task, 0L); + for (CraftTask taskPending = this.head.getNext(); taskPending != null; taskPending = taskPending.getNext()) { + if (taskPending == task) { +@@ -312,7 +314,7 @@ public class CraftScheduler implements BukkitScheduler { + } + } + } +- }); ++ }){{this.timings=co.aikar.timings.MinecraftTimings.getCancelTasksTimer(plugin);}}; // Paper + this.handle(task, 0L); + for (CraftTask taskPending = this.head.getNext(); taskPending != null; taskPending = taskPending.getNext()) { + if (taskPending == task) { +@@ -419,9 +421,7 @@ public class CraftScheduler implements BukkitScheduler { + if (task.isSync()) { + this.currentTask = task; + try { +- task.timings.startTiming(); // Spigot + task.run(); +- task.timings.stopTiming(); // Spigot + } catch (final Throwable throwable) { + // Paper start + String msg = String.format( +@@ -455,8 +455,10 @@ public class CraftScheduler implements BukkitScheduler { + this.runners.remove(task.getTaskId()); + } + } ++ MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper + this.pending.addAll(temp); + temp.clear(); ++ MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper + this.debugHead = this.debugHead.getNextHead(currentTick); + } + +@@ -493,6 +495,7 @@ public class CraftScheduler implements BukkitScheduler { + } + + private void parsePending() { ++ MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); + CraftTask head = this.head; + CraftTask task = head.getNext(); + CraftTask lastTask = head; +@@ -511,6 +514,7 @@ public class CraftScheduler implements BukkitScheduler { + task.setNext(null); + } + this.head = lastTask; ++ MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); + } + + 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 d56abf283f38548faa790c57045033f7ade6f958..ea26d9464644b5217879b8c21b4da28e57708dcb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +@@ -1,12 +1,15 @@ + package org.bukkit.craftbukkit.scheduler; + + import java.util.function.Consumer; ++ ++import co.aikar.timings.NullTimingHandler; + import org.bukkit.Bukkit; + import org.bukkit.plugin.Plugin; + import org.bukkit.scheduler.BukkitTask; + +-import org.bukkit.craftbukkit.SpigotTimings; // Spigot + import org.spigotmc.CustomTimingsHandler; // Spigot ++import co.aikar.timings.MinecraftTimings; // Paper ++import co.aikar.timings.Timing; // Paper + + public class CraftTask implements BukkitTask, Runnable { // Spigot + +@@ -26,13 +29,13 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + */ + private volatile long period; + private long nextRun; +- private final Runnable rTask; +- private final Consumer<BukkitTask> cTask; ++ public final Runnable rTask; // Paper ++ public final Consumer<BukkitTask> cTask; // Paper ++ public Timing timings; // Paper + private final Plugin plugin; + private final int id; + private final long createdAt = System.nanoTime(); + +- final CustomTimingsHandler timings; // Spigot + CraftTask() { + this(null, null, CraftTask.NO_REPEATING, CraftTask.NO_REPEATING); + } +@@ -52,7 +55,7 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + this.id = id; + this.period = CraftTask.NO_REPEATING; + this.taskName = taskName; +- this.timings = null; // Will be changed in later patch ++ this.timings = MinecraftTimings.getInternalTaskName(taskName); + } + // Paper end + +@@ -73,7 +76,7 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + } + this.id = id; + this.period = period; +- this.timings = this.isSync() ? SpigotTimings.getPluginTaskTimings(this, period) : null; // Spigot ++ timings = task != null ? MinecraftTimings.getPluginTaskTimings(this, period) : NullTimingHandler.NULL; // Paper + } + + @Override +@@ -93,11 +96,13 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + + @Override + public void run() { ++ try (Timing ignored = timings.startTiming()) { // Paper + if (this.rTask != null) { + this.rTask.run(); + } else { + this.cTask.accept(this); + } ++ } // Paper + } + + long getCreatedAt() { +@@ -128,7 +133,7 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + this.next = next; + } + +- Class<?> getTaskClass() { ++ public Class<?> getTaskClass() { // Paper + return (this.rTask != null) ? this.rTask.getClass() : ((this.cTask != null) ? this.cTask.getClass() : null); + } + +@@ -152,9 +157,4 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + return true; + } + +- // Spigot start +- public String getTaskName() { +- return (this.getTaskClass() == null) ? "Unknown" : this.getTaskClass().getName(); +- } +- // Spigot end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java b/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java +index f97eccb6a17c7876e1e002d798eb67bbe80571a0..76effc345d362047e64d064eb64a5222612aec14 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java +@@ -8,4 +8,11 @@ public class CraftIconCache implements CachedServerIcon { + public CraftIconCache(final byte[] value) { + this.value = value; + } ++ ++ public String getData() { ++ if (value == null) { ++ return null; ++ } ++ return "data:image/png;base64," + new String(java.util.Base64.getEncoder().encode(value), java.nio.charset.StandardCharsets.UTF_8); ++ } // Paper + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index d551c9d898d8b5128dfef84d206396c84072abe4..99f134d03b76e867a3e5084c5c89e53c74d8e7af 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -217,6 +217,12 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end + // ======================================================================== ++ // Paper start ++ @Override ++ public void reportTimings() { ++ co.aikar.timings.TimingsExport.reportTimings(); ++ } ++ // Paper end + + public static byte toLegacyData(BlockState data) { + return CraftLegacy.toLegacyData(data); +@@ -467,6 +473,12 @@ public final class CraftMagicNumbers implements UnsafeValues { + public DamageSource.Builder createDamageSourceBuilder(DamageType damageType) { + return new CraftDamageSourceBuilder(damageType); + } ++ // Paper start ++ @Override ++ public String getTimingsServerName() { ++ return io.papermc.paper.configuration.GlobalConfiguration.get().timings.serverName; ++ } ++ // Paper end + + @Override + public String get(Class<?> aClass, String s) { +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index ff422d4d4f2b764370f0ee2af13034853c1d3fe1..a5da6c1cae0afbde684be250e2fc3c0c32a1265b 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -27,7 +27,7 @@ import net.minecraft.world.entity.projectile.ThrownTrident; + import net.minecraft.world.entity.raid.Raider; + import net.minecraft.world.level.Level; + import net.minecraft.world.phys.AABB; +-import org.bukkit.craftbukkit.SpigotTimings; ++import co.aikar.timings.MinecraftTimings; + + public class ActivationRange + { +@@ -74,8 +74,8 @@ public class ActivationRange + /** + * These entities are excluded from Activation range checks. + * +- * @param entity +- * @param config ++ * @param entity Entity to initialize ++ * @param config Spigot config to determine ranges + * @return boolean If it should always tick. + */ + public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config) +@@ -110,7 +110,7 @@ public class ActivationRange + */ + public static void activateEntities(Level world) + { +- SpigotTimings.entityActivationCheckTimer.startTiming(); ++ MinecraftTimings.entityActivationCheckTimer.startTiming(); + final int miscActivationRange = world.spigotConfig.miscActivationRange; + final int raiderActivationRange = world.spigotConfig.raiderActivationRange; + final int animalActivationRange = world.spigotConfig.animalActivationRange; +@@ -137,7 +137,7 @@ public class ActivationRange + + world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity); + } +- SpigotTimings.entityActivationCheckTimer.stopTiming(); ++ MinecraftTimings.entityActivationCheckTimer.stopTiming(); + } + + /** +@@ -232,10 +232,8 @@ public class ActivationRange + */ + public static boolean checkIfActive(Entity entity) + { +- SpigotTimings.checkIfActiveTimer.startTiming(); + // Never safe to skip fireworks or entities not yet added to chunk + if ( entity instanceof FireworkRocketEntity ) { +- SpigotTimings.checkIfActiveTimer.stopTiming(); + return true; + } + +@@ -259,7 +257,6 @@ public class ActivationRange + { + isActive = false; + } +- SpigotTimings.checkIfActiveTimer.stopTiming(); + return isActive; + } + } diff --git a/patches/server/0024-Add-TickThread.patch b/patches/server/0024-Add-TickThread.patch new file mode 100644 index 0000000000..bf8c922243 --- /dev/null +++ b/patches/server/0024-Add-TickThread.patch @@ -0,0 +1,109 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Sun, 3 Mar 2019 20:53:18 -0800 +Subject: [PATCH] Add TickThread + +Placeholder patch, to be used by chunksystem rewrite + +diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..73e83d56a340f0c7dcb8ff737d621003e72c6de4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/TickThread.java +@@ -0,0 +1,83 @@ ++package io.papermc.paper.util; ++ ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.Entity; ++import org.bukkit.Bukkit; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++public final class TickThread extends Thread { ++ ++ public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); ++ ++ static { ++ if (STRICT_THREAD_CHECKS) { ++ MinecraftServer.LOGGER.warn("Strict thread checks enabled - performance may suffer"); ++ } ++ } ++ ++ public static void softEnsureTickThread(final String reason) { ++ if (!STRICT_THREAD_CHECKS) { ++ return; ++ } ++ ensureTickThread(reason); ++ } ++ ++ public static void ensureTickThread(final String reason) { ++ if (!isTickThread()) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { ++ if (!isTickThreadFor(world, chunkX, chunkZ)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final Entity entity, final String reason) { ++ if (!isTickThreadFor(entity)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ ++ ++ private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); ++ ++ public TickThread(final String name) { ++ this(null, name); ++ } ++ ++ public TickThread(final Runnable run, final String name) { ++ this(run, name, ID_GENERATOR.incrementAndGet()); ++ } ++ ++ private TickThread(final Runnable run, final String name, final int id) { ++ super(run, name); ++ this.id = id; ++ } ++ ++ public static TickThread getCurrentTickThread() { ++ return (TickThread) Thread.currentThread(); ++ } ++ ++ public static boolean isTickThread() { ++ return Bukkit.isPrimaryThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final Entity entity) { ++ return isTickThread(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java +index bbf0d9d9c44fe8d7add2f978994ec129420814c7..78669fa035b7537ff7e533cf32aaf2995625424f 100644 +--- a/src/main/java/org/spigotmc/AsyncCatcher.java ++++ b/src/main/java/org/spigotmc/AsyncCatcher.java +@@ -9,7 +9,7 @@ public class AsyncCatcher + + public static void catchOp(String reason) + { +- if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread ) ++ if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper + { + throw new IllegalStateException( "Asynchronous " + reason + "!" ); + } diff --git a/patches/server/0025-Further-improve-server-tick-loop.patch b/patches/server/0025-Further-improve-server-tick-loop.patch new file mode 100644 index 0000000000..0a1f62fc08 --- /dev/null +++ b/patches/server/0025-Further-improve-server-tick-loop.patch @@ -0,0 +1,233 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Tue, 1 Mar 2016 23:09:29 -0600 +Subject: [PATCH] Further improve server tick loop + +Improves how the catchup buffer is handled, allowing it to roll both ways +increasing the effeciency of the thread sleep so it only will sleep once. + +Also increases the buffer of the catchup to ensure server stays at 20 TPS unless extreme conditions + +Previous implementation did not calculate TPS correctly. +Switch to a realistic rolling average and factor in std deviation as an extra reporting variable + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 4137cf4d716680ff1b1ab0b8a3e8f7cb4bae7dbe..8fbc764fd2802f735b9d5fac2dabca6a7cebe58e 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -296,7 +296,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + public org.bukkit.craftbukkit.CraftServer server; + public OptionSet options; + public org.bukkit.command.ConsoleCommandSender console; +- public static int currentTick = (int) (System.currentTimeMillis() / 50); ++ public static int currentTick; // Paper - improve tick loop + public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +@@ -305,7 +305,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + // Spigot start + public static final int TPS = 20; + public static final int TICK_TIME = 1000000000 / MinecraftServer.TPS; +- private static final int SAMPLE_INTERVAL = 100; ++ private static final int SAMPLE_INTERVAL = 20; // Paper - improve server tick loop ++ @Deprecated(forRemoval = true) // Paper + public final double[] recentTps = new double[ 3 ]; + // Spigot end + public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files +@@ -1020,6 +1021,57 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + { + return ( avg * exp ) + ( tps * ( 1 - exp ) ); + } ++ ++ // Paper start - Further improve server tick loop ++ private static final long SEC_IN_NANO = 1000000000; ++ private static final long MAX_CATCHUP_BUFFER = TICK_TIME * TPS * 60L; ++ private long lastTick = 0; ++ private long catchupTime = 0; ++ public final RollingAverage tps1 = new RollingAverage(60); ++ public final RollingAverage tps5 = new RollingAverage(60 * 5); ++ public final RollingAverage tps15 = new RollingAverage(60 * 15); ++ ++ public static class RollingAverage { ++ private final int size; ++ private long time; ++ private java.math.BigDecimal total; ++ private int index = 0; ++ private final java.math.BigDecimal[] samples; ++ private final long[] times; ++ ++ RollingAverage(int size) { ++ this.size = size; ++ this.time = size * SEC_IN_NANO; ++ this.total = dec(TPS).multiply(dec(SEC_IN_NANO)).multiply(dec(size)); ++ this.samples = new java.math.BigDecimal[size]; ++ this.times = new long[size]; ++ for (int i = 0; i < size; i++) { ++ this.samples[i] = dec(TPS); ++ this.times[i] = SEC_IN_NANO; ++ } ++ } ++ ++ private static java.math.BigDecimal dec(long t) { ++ return new java.math.BigDecimal(t); ++ } ++ public void add(java.math.BigDecimal x, long t) { ++ time -= times[index]; ++ total = total.subtract(samples[index].multiply(dec(times[index]))); ++ samples[index] = x; ++ times[index] = t; ++ time += t; ++ total = total.add(x.multiply(dec(t))); ++ if (++index == size) { ++ index = 0; ++ } ++ } ++ ++ public double getAverage() { ++ return total.divide(dec(time), 30, java.math.RoundingMode.HALF_UP).doubleValue(); ++ } ++ } ++ private static final java.math.BigDecimal TPS_BASE = new java.math.BigDecimal(1E9).multiply(new java.math.BigDecimal(SAMPLE_INTERVAL)); ++ // Paper end + // Spigot End + + protected void runServer() { +@@ -1034,7 +1086,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + // Spigot start + Arrays.fill( this.recentTps, 20 ); +- long tickSection = Util.getMillis(), tickCount = 1; ++ // Paper start - further improve server tick loop ++ long tickSection = Util.getNanos(); ++ long currentTime; ++ // Paper end - further improve server tick loop + while (this.running) { + long i; + +@@ -1057,15 +1112,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + // Spigot start + ++MinecraftServer.currentTickLong; // Paper - track current tick as a long +- if ( tickCount++ % MinecraftServer.SAMPLE_INTERVAL == 0 ) +- { +- long curTime = Util.getMillis(); +- double currentTps = 1E3 / ( curTime - tickSection ) * MinecraftServer.SAMPLE_INTERVAL; +- this.recentTps[0] = MinecraftServer.calcTps( this.recentTps[0], 0.92, currentTps ); // 1/exp(5sec/1min) +- this.recentTps[1] = MinecraftServer.calcTps( this.recentTps[1], 0.9835, currentTps ); // 1/exp(5sec/5min) +- this.recentTps[2] = MinecraftServer.calcTps( this.recentTps[2], 0.9945, currentTps ); // 1/exp(5sec/15min) +- tickSection = curTime; ++ // Paper start - further improve server tick loop ++ currentTime = Util.getNanos(); ++ if (++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0) { ++ final long diff = currentTime - tickSection; ++ final java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP); ++ tps1.add(currentTps, diff); ++ tps5.add(currentTps, diff); ++ tps15.add(currentTps, diff); ++ ++ // Backwards compat with bad plugins ++ this.recentTps[0] = tps1.getAverage(); ++ this.recentTps[1] = tps5.getAverage(); ++ this.recentTps[2] = tps15.getAverage(); ++ tickSection = currentTime; + } ++ // Paper end - further improve server tick loop + // Spigot end + + boolean flag = i == 0L; +@@ -1075,7 +1137,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount); + } + +- MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit ++ //MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time ++ lastTick = currentTime; + this.nextTickTimeNanos += i; + this.startMetricsRecordingTick(); + this.profiler.push("tick"); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index de55611daeb6d55f69c4cb72137eb7b050e727f7..1cb6c21741408ff4628864b52341965dfbfa5711 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2665,7 +2665,11 @@ public final class CraftServer implements Server { + + @Override + public double[] getTPS() { +- return new double[]{0, 0, 0}; // TODO ++ return new double[] { ++ net.minecraft.server.MinecraftServer.getServer().tps1.getAverage(), ++ net.minecraft.server.MinecraftServer.getServer().tps5.getAverage(), ++ net.minecraft.server.MinecraftServer.getServer().tps15.getAverage() ++ }; + } + + // Paper start - adventure sounds +diff --git a/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +index d9ec48be0fdd2bfea938aa29e36b0f6ffa839ab2..9eb2823cc8f83bad2626fc77578b0162d9ed5782 100644 +--- a/src/main/java/org/spigotmc/TicksPerSecondCommand.java ++++ b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +@@ -15,6 +15,12 @@ public class TicksPerSecondCommand extends Command + this.usageMessage = "/tps"; + this.setPermission( "bukkit.command.tps" ); + } ++ // Paper start ++ private static final net.kyori.adventure.text.Component WARN_MSG = net.kyori.adventure.text.Component.text() ++ .append(net.kyori.adventure.text.Component.text("Warning: ", net.kyori.adventure.text.format.NamedTextColor.RED)) ++ .append(net.kyori.adventure.text.Component.text("Memory usage on modern garbage collectors is not a stable value and it is perfectly normal to see it reach max. Please do not pay it much attention.", net.kyori.adventure.text.format.NamedTextColor.GOLD)) ++ .build(); ++ // Paper end + + @Override + public boolean execute(CommandSender sender, String currentAlias, String[] args) +@@ -24,22 +30,40 @@ public class TicksPerSecondCommand extends Command + return true; + } + +- StringBuilder sb = new StringBuilder( ChatColor.GOLD + "TPS from last 1m, 5m, 15m: " ); +- for ( double tps : MinecraftServer.getServer().recentTps ) +- { +- sb.append( this.format( tps ) ); +- sb.append( ", " ); ++ // Paper start - Further improve tick handling ++ double[] tps = org.bukkit.Bukkit.getTPS(); ++ net.kyori.adventure.text.Component[] tpsAvg = new net.kyori.adventure.text.Component[tps.length]; ++ ++ for ( int i = 0; i < tps.length; i++) { ++ tpsAvg[i] = TicksPerSecondCommand.format( tps[i] ); ++ } ++ ++ net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text(); ++ builder.append(net.kyori.adventure.text.Component.text("TPS from last 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD)); ++ builder.append(net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.commas(true), tpsAvg)); ++ sender.sendMessage(builder.asComponent()); ++ if (args.length > 0 && args[0].equals("mem") && sender.hasPermission("bukkit.command.tpsmemory")) { ++ sender.sendMessage(net.kyori.adventure.text.Component.text() ++ .append(net.kyori.adventure.text.Component.text("Current Memory Usage: ", net.kyori.adventure.text.format.NamedTextColor.GOLD)) ++ .append(net.kyori.adventure.text.Component.text(((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024)) + "/" + (Runtime.getRuntime().totalMemory() / (1024 * 1024)) + " mb (Max: " + (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " mb)", net.kyori.adventure.text.format.NamedTextColor.GREEN)) ++ ); ++ if (!this.hasShownMemoryWarning) { ++ sender.sendMessage(WARN_MSG); ++ this.hasShownMemoryWarning = true; ++ } + } +- sender.sendMessage( sb.substring( 0, sb.length() - 2 ) ); +- 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)"); ++ // Paper end + + return true; + } + +- private String format(double tps) ++ private boolean hasShownMemoryWarning; // Paper ++ private static net.kyori.adventure.text.Component format(double tps) // Paper - Made static + { +- return ( ( tps > 18.0 ) ? ChatColor.GREEN : ( tps > 16.0 ) ? ChatColor.YELLOW : ChatColor.RED ).toString() +- + ( ( tps > 20.0 ) ? "*" : "" ) + Math.min( Math.round( tps * 100.0 ) / 100.0, 20.0 ); ++ // Paper ++ net.kyori.adventure.text.format.TextColor color = ( ( tps > 18.0 ) ? net.kyori.adventure.text.format.NamedTextColor.GREEN : ( tps > 16.0 ) ? net.kyori.adventure.text.format.NamedTextColor.YELLOW : net.kyori.adventure.text.format.NamedTextColor.RED ); ++ String amount = Math.min(Math.round(tps * 100.0) / 100.0, 20.0) + (tps > 21.0 ? "*" : ""); // Paper - only print * at 21, we commonly peak to 20.02 as the tick sleep is not accurate enough, stop the noise ++ return net.kyori.adventure.text.Component.text(amount, color); ++ // Paper end + } + } diff --git a/patches/server/0026-Add-command-line-option-to-load-extra-plugin-jars-no.patch b/patches/server/0026-Add-command-line-option-to-load-extra-plugin-jars-no.patch new file mode 100644 index 0000000000..0f009842b0 --- /dev/null +++ b/patches/server/0026-Add-command-line-option-to-load-extra-plugin-jars-no.patch @@ -0,0 +1,65 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <[email protected]> +Date: Tue, 18 May 2021 14:39:44 -0700 +Subject: [PATCH] Add command line option to load extra plugin jars not in the + plugins folder + +ex: java -jar paperclip.jar nogui -add-plugin=/path/to/plugin.jar -add-plugin=/path/to/another/plugin_jar.jar + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 1cb6c21741408ff4628864b52341965dfbfa5711..a2f784b28c0d974ee45d61d6a3a0096dd7161d3e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -450,6 +450,35 @@ public final class CraftServer implements Server { + io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation + } + ++ // Paper start ++ @Override ++ public File getPluginsFolder() { ++ return this.console.getPluginsFolder(); ++ } ++ ++ private List<File> extraPluginJars() { ++ @SuppressWarnings("unchecked") ++ final List<File> jars = (List<File>) this.console.options.valuesOf("add-plugin"); ++ final List<File> list = new ArrayList<>(); ++ for (final File file : jars) { ++ if (!file.exists()) { ++ net.minecraft.server.MinecraftServer.LOGGER.warn("File '{}' specified through 'add-plugin' argument does not exist, cannot load a plugin from it!", file.getAbsolutePath()); ++ continue; ++ } ++ if (!file.isFile()) { ++ net.minecraft.server.MinecraftServer.LOGGER.warn("File '{}' specified through 'add-plugin' argument is not a file, cannot load a plugin from it!", file.getAbsolutePath()); ++ continue; ++ } ++ if (!file.getName().endsWith(".jar")) { ++ net.minecraft.server.MinecraftServer.LOGGER.warn("File '{}' specified through 'add-plugin' argument is not a jar file, cannot load a plugin from it!", file.getAbsolutePath()); ++ continue; ++ } ++ list.add(file); ++ } ++ return list; ++ } ++ // Paper end ++ + public void enablePlugins(PluginLoadOrder type) { + if (type == PluginLoadOrder.STARTUP) { + this.helpMap.clear(); +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 9de87edb75947382fda114df883fb4b31c1a7141..bc664b090e16ed27ba795c204dc5639679e6eee8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -160,6 +160,12 @@ public class Main { + .ofType(File.class) + .defaultsTo(new File("paper.yml")) + .describedAs("Yml file"); ++ ++ acceptsAll(asList("add-plugin", "add-extra-plugin-jar"), "Specify paths to extra plugin jars to be loaded in addition to those in the plugins folder. This argument can be specified multiple times, once for each extra plugin jar path.") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File[] {}) ++ .describedAs("Jar file"); + // Paper end + } + }; diff --git a/patches/server/0027-Support-components-in-ItemMeta.patch b/patches/server/0027-Support-components-in-ItemMeta.patch new file mode 100644 index 0000000000..1086398de1 --- /dev/null +++ b/patches/server/0027-Support-components-in-ItemMeta.patch @@ -0,0 +1,83 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MiniDigger <[email protected]> +Date: Sat, 6 Jun 2020 18:13:42 +0200 +Subject: [PATCH] Support components in ItemMeta + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +index fd147ea7fb5d143c11e7e2bf09f31bcb6e513f76..7381f0dac6dd0ec716e3b3475bbf8f5aae049e28 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +@@ -923,11 +923,23 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + return CraftChatMessage.fromComponent(this.displayName); + } + ++ // Paper start ++ @Override ++ public net.md_5.bungee.api.chat.BaseComponent[] getDisplayNameComponent() { ++ return displayName == null ? new net.md_5.bungee.api.chat.BaseComponent[0] : net.md_5.bungee.chat.ComponentSerializer.parse(CraftChatMessage.toJSON(displayName)); ++ } ++ // Paper end + @Override + public final void setDisplayName(String name) { + this.displayName = CraftChatMessage.fromStringOrNull(name); + } + ++ // Paper start ++ @Override ++ public void setDisplayNameComponent(net.md_5.bungee.api.chat.BaseComponent[] component) { ++ this.displayName = CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(component)); ++ } ++ // Paper end + @Override + public boolean hasDisplayName() { + return this.displayName != null; +@@ -1101,6 +1113,14 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + return this.lore == null ? null : new ArrayList<String>(Lists.transform(this.lore, CraftChatMessage::fromComponent)); + } + ++ // Paper start ++ @Override ++ public List<net.md_5.bungee.api.chat.BaseComponent[]> getLoreComponents() { ++ return this.lore == null ? null : new ArrayList<>(this.lore.stream().map(entry -> ++ net.md_5.bungee.chat.ComponentSerializer.parse(CraftChatMessage.toJSON(entry)) ++ ).collect(java.util.stream.Collectors.toList())); ++ } ++ // Paper end + @Override + public void setLore(List<String> lore) { + if (lore == null || lore.isEmpty()) { +@@ -1115,6 +1135,21 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + } + } + ++ // Paper start ++ @Override ++ public void setLoreComponents(List<net.md_5.bungee.api.chat.BaseComponent[]> lore) { ++ if (lore == null) { ++ this.lore = null; ++ } else { ++ if (this.lore == null) { ++ safelyAdd(lore, this.lore = new ArrayList<>(lore.size()), false); ++ } else { ++ this.lore.clear(); ++ safelyAdd(lore, this.lore, false); ++ } ++ } ++ } ++ // Paper end + @Override + public boolean hasCustomModelData() { + return this.customModelData != null; +@@ -1810,6 +1845,11 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + } + + for (Object object : addFrom) { ++ // Paper start - support components ++ if(object instanceof net.md_5.bungee.api.chat.BaseComponent[] baseComponentArr) { ++ addTo.add(CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(baseComponentArr))); ++ } else ++ // Paper end + if (!(object instanceof String)) { + if (object != null) { + // SPIGOT-7399: Null check via if is important, diff --git a/patches/server/0028-Configurable-cactus-bamboo-and-reed-growth-height.patch b/patches/server/0028-Configurable-cactus-bamboo-and-reed-growth-height.patch new file mode 100644 index 0000000000..d25f48a93d --- /dev/null +++ b/patches/server/0028-Configurable-cactus-bamboo-and-reed-growth-height.patch @@ -0,0 +1,92 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 13:02:51 -0600 +Subject: [PATCH] Configurable cactus bamboo and reed growth height + +Bamboo - Both the minimum fully-grown height and the maximum are configurable +- Machine_Maker + +diff --git a/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java b/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java +index eda75b316acd09120539c92ff8adb97d92e9523f..e2951dd077441fe9cda461a2d3ef0c0671308316 100644 +--- a/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java +@@ -137,7 +137,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + if (random.nextFloat() < (world.spigotConfig.bambooModifier / (100.0f * 3)) && world.isEmptyBlock(pos.above()) && world.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution + int i = this.getHeightBelowUpToMax(world, pos) + 1; + +- if (i < 16) { ++ if (i < world.paperConfig().maxGrowthHeight.bamboo.max) { // Paper - Configurable cactus/bamboo/reed growth height + this.growBamboo(state, world, pos, random, i); + } + } +@@ -168,7 +168,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + int i = this.getHeightAboveUpToMax(world, pos); + int j = this.getHeightBelowUpToMax(world, pos); + +- return i + j + 1 < 16 && (Integer) world.getBlockState(pos.above(i)).getValue(BambooStalkBlock.STAGE) != 1; ++ return i + j + 1 < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && (Integer) world.getBlockState(pos.above(i)).getValue(BambooStalkBlock.STAGE) != 1; // Paper - Configurable cactus/bamboo/reed growth height + } + + @Override +@@ -187,7 +187,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + BlockPos blockposition1 = pos.above(i); + BlockState iblockdata1 = world.getBlockState(blockposition1); + +- if (k >= 16 || !iblockdata1.is(Blocks.BAMBOO) || (Integer) iblockdata1.getValue(BambooStalkBlock.STAGE) == 1 || !world.isEmptyBlock(blockposition1.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here ++ if (k >= world.paperConfig().maxGrowthHeight.bamboo.max || !iblockdata1.is(Blocks.BAMBOO) || (Integer) iblockdata1.getValue(BambooStalkBlock.STAGE) == 1 || !world.isEmptyBlock(blockposition1.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here // Paper - Configurable cactus/bamboo/reed growth height + return; + } + +@@ -228,7 +228,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + } + + int j = (Integer) state.getValue(BambooStalkBlock.AGE) != 1 && !iblockdata2.is(Blocks.BAMBOO) ? 0 : 1; +- int k = (height < 11 || random.nextFloat() >= 0.25F) && height != 15 ? 0 : 1; ++ int k = (height < world.paperConfig().maxGrowthHeight.bamboo.min || random.nextFloat() >= 0.25F) && height != (world.paperConfig().maxGrowthHeight.bamboo.max - 1) ? 0 : 1; // Paper - Configurable cactus/bamboo/reed growth height + + // CraftBukkit start + if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, pos.above(), (BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(BambooStalkBlock.AGE, j)).setValue(BambooStalkBlock.LEAVES, blockpropertybamboosize)).setValue(BambooStalkBlock.STAGE, k), 3)) { +@@ -243,7 +243,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + protected int getHeightAboveUpToMax(BlockGetter world, BlockPos pos) { + int i; + +- for (i = 0; i < 16 && world.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO); ++i) { ++ for (i = 0; i < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && world.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO); ++i) { // Paper - Configurable cactus/bamboo/reed growth height + ; + } + +@@ -253,7 +253,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + protected int getHeightBelowUpToMax(BlockGetter world, BlockPos pos) { + int i; + +- for (i = 0; i < 16 && world.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO); ++i) { ++ for (i = 0; i < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && world.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO); ++i) { // Paper - Configurable cactus/bamboo/reed growth height + ; + } + +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 c7e462a187196da906aec3b528f7945afec9f6b0..fd344c5cf0d6d523abe34d5e3f8d939106942cbb 100644 +--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +@@ -61,7 +61,7 @@ public class CactusBlock extends Block { + ; + } + +- if (i < 3) { ++ if (i < world.paperConfig().maxGrowthHeight.cactus) { // Paper - Configurable cactus/bamboo/reed growth height + int j = (Integer) state.getValue(CactusBlock.AGE); + + int modifier = world.spigotConfig.cactusModifier; // Spigot - SPIGOT-7159: Better modifier resolution +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 f034a7dbc0124353f8cb9b2c841226e73d83423a..c48c622e92cedeaa46b929c7adfedec98dd5a3fb 100644 +--- a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +@@ -59,7 +59,7 @@ public class SugarCaneBlock extends Block { + ; + } + +- if (i < 3) { ++ if (i < world.paperConfig().maxGrowthHeight.reeds) { // Paper - Configurable cactus/bamboo/reed growth heigh + int j = (Integer) state.getValue(SugarCaneBlock.AGE); + + int modifier = world.spigotConfig.caneModifier; // Spigot - SPIGOT-7159: Better modifier resolution diff --git a/patches/server/0029-Configurable-baby-zombie-movement-speed.patch b/patches/server/0029-Configurable-baby-zombie-movement-speed.patch new file mode 100644 index 0000000000..a55a0a3944 --- /dev/null +++ b/patches/server/0029-Configurable-baby-zombie-movement-speed.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 13:09:16 -0600 +Subject: [PATCH] Configurable baby zombie movement speed + + +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 60a9db4131bcf69a33003b83db6117c9a7a83276..393a9c704f4637a0e8031328d2a0facef4723dd8 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +@@ -75,7 +75,7 @@ import org.bukkit.event.entity.EntityTransformEvent; + public class Zombie extends Monster { + + private static final ResourceLocation SPEED_MODIFIER_BABY_ID = ResourceLocation.withDefaultNamespace("baby"); +- private static final AttributeModifier SPEED_MODIFIER_BABY = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, 0.5D, AttributeModifier.Operation.ADD_MULTIPLIED_BASE); ++ private final AttributeModifier babyModifier = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, this.level().paperConfig().entities.behavior.babyZombieMovementModifier, AttributeModifier.Operation.ADD_MULTIPLIED_BASE); // Paper - Make baby speed configurable + private static final ResourceLocation REINFORCEMENT_CALLER_CHARGE_ID = ResourceLocation.withDefaultNamespace("reinforcement_caller_charge"); + private static final AttributeModifier ZOMBIE_REINFORCEMENT_CALLEE_CHARGE = new AttributeModifier(ResourceLocation.withDefaultNamespace("reinforcement_callee_charge"), -0.05000000074505806D, AttributeModifier.Operation.ADD_VALUE); + private static final ResourceLocation LEADER_ZOMBIE_BONUS_ID = ResourceLocation.withDefaultNamespace("leader_zombie_bonus"); +@@ -188,9 +188,9 @@ public class Zombie extends Monster { + if (this.level() != null && !this.level().isClientSide) { + AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED); + +- attributemodifiable.removeModifier(Zombie.SPEED_MODIFIER_BABY_ID); ++ attributemodifiable.removeModifier(this.babyModifier.id()); // Paper - Make baby speed configurable + if (baby) { +- attributemodifiable.addTransientModifier(Zombie.SPEED_MODIFIER_BABY); ++ attributemodifiable.addTransientModifier(this.babyModifier); // Paper - Make baby speed configurable + } + } + diff --git a/patches/server/0030-Configurable-fishing-time-ranges.patch b/patches/server/0030-Configurable-fishing-time-ranges.patch new file mode 100644 index 0000000000..90d886761f --- /dev/null +++ b/patches/server/0030-Configurable-fishing-time-ranges.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 13:14:11 -0600 +Subject: [PATCH] Configurable fishing time ranges + + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +index 0acb45014039d4392988c7d853595f96e856af4a..ed43ad94ca007a54e3c32d5e17c141048eeb5835 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +@@ -94,6 +94,10 @@ public class FishingHook extends Projectile { + this.noCulling = true; + this.luck = Math.max(0, luckBonus); + this.lureSpeed = Math.max(0, waitTimeReductionTicks); ++ // Paper start - Configurable fishing time ranges ++ minWaitTime = world.paperConfig().fishingTimeRange.minimum; ++ maxWaitTime = world.paperConfig().fishingTimeRange.maximum; ++ // Paper end - Configurable fishing time ranges + } + + public FishingHook(EntityType<? extends FishingHook> type, Level world) { +@@ -411,7 +415,7 @@ public class FishingHook extends Projectile { + } else { + // CraftBukkit start - logic to modify fishing wait time + this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime); +- this.timeUntilLured -= (this.applyLure) ? this.lureSpeed : 0; ++ this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop + // CraftBukkit end + } + } diff --git a/patches/server/0031-Allow-nerfed-mobs-to-jump.patch b/patches/server/0031-Allow-nerfed-mobs-to-jump.patch new file mode 100644 index 0000000000..1c95173e39 --- /dev/null +++ b/patches/server/0031-Allow-nerfed-mobs-to-jump.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 13:24:16 -0600 +Subject: [PATCH] Allow nerfed mobs to jump + + +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index fcc8b66702f761c443fb647a8ab6e1ab49e5acfe..bf905bc1c4918412e3c324a62d2915b57c40d2cc 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -124,6 +124,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + private final BodyRotationControl bodyRotationControl; + protected PathNavigation navigation; + public GoalSelector goalSelector; ++ @Nullable public net.minecraft.world.entity.ai.goal.FloatGoal goalFloat; // Paper - Allow nerfed mobs to jump and float + public GoalSelector targetSelector; + @Nullable + private LivingEntity target; +@@ -890,7 +891,15 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + @Override + protected final void serverAiStep() { + ++this.noActionTime; +- if (!this.aware) return; // CraftBukkit ++ // Paper start - Allow nerfed mobs to jump and float ++ if (!this.aware) { ++ if (goalFloat != null) { ++ if (goalFloat.canUse()) goalFloat.tick(); ++ this.getJumpControl().tick(); ++ } ++ return; ++ } ++ // Paper end - Allow nerfed mobs to jump and float + ProfilerFiller gameprofilerfiller = this.level().getProfiler(); + + gameprofilerfiller.push("sensing"); +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java +index 5fbb5d2bf8945a361babfe50f5f92fa46b9e8b5f..7eb0e0486203d9ad6ce89d17a4da96a7563088a4 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java +@@ -9,6 +9,7 @@ public class FloatGoal extends Goal { + + public FloatGoal(Mob mob) { + this.mob = mob; ++ if (mob.getCommandSenderWorld().paperConfig().entities.behavior.spawnerNerfedMobsShouldJump) mob.goalFloat = this; // Paper - Allow nerfed mobs to jump and float + this.setFlags(EnumSet.of(Goal.Flag.JUMP)); + mob.getNavigation().setCanFloat(true); + } diff --git a/patches/server/0032-Add-configurable-entity-despawn-distances.patch b/patches/server/0032-Add-configurable-entity-despawn-distances.patch new file mode 100644 index 0000000000..348a1c4e40 --- /dev/null +++ b/patches/server/0032-Add-configurable-entity-despawn-distances.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Suddenly <[email protected]> +Date: Tue, 1 Mar 2016 13:51:54 -0600 +Subject: [PATCH] Add configurable entity despawn distances + + +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index bf905bc1c4918412e3c324a62d2915b57c40d2cc..51b33ea29af0f4010dfb8a1a7503cb7ca463209e 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -866,14 +866,14 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + + if (entityhuman != null) { + double d0 = entityhuman.distanceToSqr((Entity) this); +- int i = this.getType().getCategory().getDespawnDistance(); ++ int i = this.level().paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory()).hard(); // Paper - Configurable despawn distances + int j = i * i; + + if (d0 > (double) j && this.removeWhenFarAway(d0)) { + this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause + } + +- int k = this.getType().getCategory().getNoDespawnDistance(); ++ int k = this.level().paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory()).soft(); // Paper - Configurable despawn distances + int l = k * k; + + if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && d0 > (double) l && this.removeWhenFarAway(d0)) { diff --git a/patches/server/0033-Drop-falling-block-and-tnt-entities-at-the-specified.patch b/patches/server/0033-Drop-falling-block-and-tnt-entities-at-the-specified.patch new file mode 100644 index 0000000000..ce9c290891 --- /dev/null +++ b/patches/server/0033-Drop-falling-block-and-tnt-entities-at-the-specified.patch @@ -0,0 +1,62 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Byteflux <[email protected]> +Date: Tue, 1 Mar 2016 14:14:15 -0600 +Subject: [PATCH] Drop falling block and tnt entities at the specified height + +Co-authored-by: Jake Potrebic <[email protected]> + +diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +index a4e8fa5267b8853603e4683bf9d002db7465e4b6..c3a16691e8a843c02e0aea6469822cd8869ad593 100644 +--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -149,6 +149,16 @@ public class FallingBlockEntity extends Entity { + ++this.time; + this.applyGravity(); + this.move(MoverType.SELF, this.getDeltaMovement()); ++ // Paper start - Configurable falling blocks height nerf ++ if (this.level().paperConfig().fixes.fallingBlockHeightNerf.test(v -> this.getY() > v)) { ++ if (this.dropItem && this.level().getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) { ++ this.spawnAtLocation(block); ++ } ++ ++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD); ++ return; ++ } ++ // Paper end - Configurable falling blocks height nerf + this.handlePortal(); + if (!this.level().isClientSide && (this.isAlive() || this.forceTickAfterTeleportToDuplicate)) { + BlockPos blockposition = this.blockPosition(); +diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +index 8f693bb45099124bca62849528c81d717131a48c..15432b512fc0d0d38bf28499e2afa5e48fec7aaa 100644 +--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +@@ -99,6 +99,12 @@ public class PrimedTnt extends Entity implements TraceableEntity { + this.handlePortal(); + this.applyGravity(); + this.move(MoverType.SELF, this.getDeltaMovement()); ++ // Paper start - Configurable TNT height nerf ++ if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) { ++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD); ++ return; ++ } ++ // Paper end - Configurable TNT height nerf + this.setDeltaMovement(this.getDeltaMovement().scale(0.98D)); + if (this.onGround()) { + this.setDeltaMovement(this.getDeltaMovement().multiply(0.7D, -0.5D, 0.7D)); +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java +index c112c4b103c34e05f4de973eec94a969f60b085e..b7036f8399e2500ba01736c6006b972f7ca4838e 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java +@@ -54,6 +54,12 @@ public class MinecartTNT extends AbstractMinecart { + public void tick() { + super.tick(); + if (this.fuse > 0) { ++ // Paper start - Configurable TNT height nerf ++ if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) { ++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD); ++ return; ++ } ++ // Paper end - Configurable TNT height nerf + --this.fuse; + this.level().addParticle(ParticleTypes.SMOKE, this.getX(), this.getY() + 0.5D, this.getZ(), 0.0D, 0.0D, 0.0D); + } else if (this.fuse == 0) { diff --git a/patches/server/0034-Expose-server-build-information.patch b/patches/server/0034-Expose-server-build-information.patch new file mode 100644 index 0000000000..7b10d6d232 --- /dev/null +++ b/patches/server/0034-Expose-server-build-information.patch @@ -0,0 +1,702 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 14:32:43 -0600 +Subject: [PATCH] Expose server build information + +Co-authored-by: Zach Brown <[email protected]> +Co-authored-by: Kyle Wood <[email protected]> +Co-authored-by: Mark Vainomaa <[email protected]> +Co-authored-by: Riley Park <[email protected]> +Co-authored-by: Jake Potrebic <[email protected]> +Co-authored-by: masmc05 <[email protected]> + +diff --git a/build.gradle.kts b/build.gradle.kts +index 708448e071ddb5558658d3096b1d7ee2593ec8c5..7b002d28932e21878dbab248abf85066a8a80a9c 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -1,4 +1,5 @@ + import io.papermc.paperweight.util.* ++import java.time.Instant + + plugins { + java +@@ -64,18 +65,24 @@ tasks.jar { + + manifest { + val git = Git(rootProject.layout.projectDirectory.path) ++ val mcVersion = rootProject.providers.gradleProperty("mcVersion").get() ++ val build = System.getenv("BUILD_NUMBER") ?: null + val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim() +- val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\"" ++ val implementationVersion = "$mcVersion-${build ?: "DEV"}-$gitHash" + val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper + val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", +- "Implementation-Title" to "CraftBukkit", +- "Implementation-Version" to "git-Paper-$implementationVersion", ++ "Implementation-Title" to "Paper", ++ "Implementation-Version" to implementationVersion, + "Implementation-Vendor" to date, // Paper +- "Specification-Title" to "Bukkit", ++ "Specification-Title" to "Paper", + "Specification-Version" to project.version, +- "Specification-Vendor" to "Bukkit Team", ++ "Specification-Vendor" to "Paper Team", ++ "Brand-Id" to "papermc:paper", ++ "Brand-Name" to "Paper", ++ "Build-Number" to (build ?: ""), ++ "Build-Time" to Instant.now().toString(), + "Git-Branch" to gitBranch, // Paper + "Git-Commit" to gitHash, // Paper + "CraftBukkit-Package-Version" to paperweight.craftBukkitPackageVersion.get(), // Paper +diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +new file mode 100644 +index 0000000000000000000000000000000000000000..532306cacd52579cdf37e4aca25887b1ed3ba6a1 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -0,0 +1,146 @@ ++package com.destroystokyo.paper; ++ ++import com.destroystokyo.paper.util.VersionFetcher; ++import com.google.common.base.Charsets; ++import com.google.common.io.Resources; ++import com.google.gson.Gson; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonSyntaxException; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.ServerBuildInfo; ++import java.io.BufferedReader; ++import java.io.IOException; ++import java.io.InputStreamReader; ++import java.net.HttpURLConnection; ++import java.net.URI; ++import java.util.Optional; ++import java.util.OptionalInt; ++import java.util.stream.StreamSupport; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.slf4j.Logger; ++ ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.TextColor.color; ++ ++@DefaultQualifier(NonNull.class) ++public class PaperVersionFetcher implements VersionFetcher { ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private static final int DISTANCE_ERROR = -1; ++ private static final int DISTANCE_UNKNOWN = -2; ++ private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/paper"; ++ ++ @Override ++ public long getCacheTime() { ++ return 720000; ++ } ++ ++ @Override ++ public Component getVersionMessage(final String serverVersion) { ++ final Component updateMessage; ++ final ServerBuildInfo build = ServerBuildInfo.buildInfo(); ++ if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) { ++ updateMessage = text("You are running a development version without access to version information", color(0xFF5300)); ++ } else { ++ updateMessage = getUpdateStatusMessage("PaperMC/Paper", build); ++ } ++ final @Nullable Component history = this.getHistory(); ++ ++ return history != null ? Component.textOfChildren(updateMessage, Component.newline(), history) : updateMessage; ++ } ++ ++ private static Component getUpdateStatusMessage(final String repo, final ServerBuildInfo build) { ++ int distance = DISTANCE_ERROR; ++ ++ final OptionalInt buildNumber = build.buildNumber(); ++ if (buildNumber.isPresent()) { ++ distance = fetchDistanceFromSiteApi(build, buildNumber.getAsInt()); ++ } else { ++ final Optional<String> gitBranch = build.gitBranch(); ++ final Optional<String> gitCommit = build.gitCommit(); ++ if (gitBranch.isPresent() && gitCommit.isPresent()) { ++ distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get()); ++ } ++ } ++ ++ return switch (distance) { ++ case DISTANCE_ERROR -> text("Error obtaining version information", NamedTextColor.YELLOW); ++ case 0 -> text("You are running the latest version", NamedTextColor.GREEN); ++ case DISTANCE_UNKNOWN -> text("Unknown version", NamedTextColor.YELLOW); ++ default -> text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) ++ .append(Component.newline()) ++ .append(text("Download the new version at: ") ++ .append(text(DOWNLOAD_PAGE, NamedTextColor.GOLD) ++ .hoverEvent(text("Click to open", NamedTextColor.WHITE)) ++ .clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE)))); ++ }; ++ } ++ ++ private static int fetchDistanceFromSiteApi(final ServerBuildInfo build, final int jenkinsBuild) { ++ try { ++ try (final BufferedReader reader = Resources.asCharSource( ++ URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(), ++ Charsets.UTF_8 ++ ).openBufferedStream()) { ++ final JsonObject json = new Gson().fromJson(reader, JsonObject.class); ++ final JsonArray builds = json.getAsJsonArray("builds"); ++ final int latest = StreamSupport.stream(builds.spliterator(), false) ++ .mapToInt(JsonElement::getAsInt) ++ .max() ++ .orElseThrow(); ++ return latest - jenkinsBuild; ++ } catch (final JsonSyntaxException ex) { ++ LOGGER.error("Error parsing json from Paper's downloads API", ex); ++ return DISTANCE_ERROR; ++ } ++ } catch (final IOException e) { ++ LOGGER.error("Error while parsing version", e); ++ return DISTANCE_ERROR; ++ } ++ } ++ ++ // Contributed by Techcable <[email protected]> in GH-65 ++ private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) { ++ try { ++ final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(repo, branch, hash)).toURL().openConnection(); ++ connection.connect(); ++ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return DISTANCE_UNKNOWN; // Unknown commit ++ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) { ++ final JsonObject obj = new Gson().fromJson(reader, JsonObject.class); ++ final String status = obj.get("status").getAsString(); ++ return switch (status) { ++ case "identical" -> 0; ++ case "behind" -> obj.get("behind_by").getAsInt(); ++ default -> DISTANCE_ERROR; ++ }; ++ } catch (final JsonSyntaxException | NumberFormatException e) { ++ LOGGER.error("Error parsing json from GitHub's API", e); ++ return DISTANCE_ERROR; ++ } ++ } catch (final IOException e) { ++ LOGGER.error("Error while parsing version", e); ++ return DISTANCE_ERROR; ++ } ++ } ++ ++ private @Nullable Component getHistory() { ++ final VersionHistoryManager.@Nullable VersionData data = VersionHistoryManager.INSTANCE.getVersionData(); ++ if (data == null) { ++ return null; ++ } ++ ++ final @Nullable String oldVersion = data.getOldVersion(); ++ if (oldVersion == null) { ++ return null; ++ } ++ ++ return text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..660b2ec6b63a4ceffee44ab11f54dfa7c0d0996f +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java +@@ -0,0 +1,153 @@ ++package com.destroystokyo.paper; ++ ++import com.google.common.base.MoreObjects; ++import com.google.gson.Gson; ++import com.google.gson.JsonSyntaxException; ++import java.io.BufferedReader; ++import java.io.BufferedWriter; ++import java.io.IOException; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.Paths; ++import java.nio.file.StandardOpenOption; ++import java.util.Objects; ++import java.util.logging.Level; ++import java.util.logging.Logger; ++import org.bukkit.Bukkit; ++ ++import javax.annotation.Nonnull; ++import javax.annotation.Nullable; ++ ++public enum VersionHistoryManager { ++ INSTANCE; ++ ++ private final Gson gson = new Gson(); ++ ++ private final Logger logger = Bukkit.getLogger(); ++ ++ private VersionData currentData = null; ++ ++ VersionHistoryManager() { ++ final Path path = Paths.get("version_history.json"); ++ ++ if (Files.exists(path)) { ++ // Basic file santiy checks ++ if (!Files.isRegularFile(path)) { ++ if (Files.isDirectory(path)) { ++ logger.severe(path + " is a directory, cannot be used for version history"); ++ } else { ++ logger.severe(path + " is not a regular file, cannot be used for version history"); ++ } ++ // We can't continue ++ return; ++ } ++ ++ try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { ++ currentData = gson.fromJson(reader, VersionData.class); ++ } catch (final IOException e) { ++ logger.log(Level.SEVERE, "Failed to read version history file '" + path + "'", e); ++ return; ++ } catch (final JsonSyntaxException e) { ++ logger.log(Level.SEVERE, "Invalid json syntax for file '" + path + "'", e); ++ return; ++ } ++ ++ final String version = Bukkit.getVersion(); ++ if (version == null) { ++ logger.severe("Failed to retrieve current version"); ++ return; ++ } ++ ++ if (currentData == null) { ++ // Empty file ++ currentData = new VersionData(); ++ currentData.setCurrentVersion(version); ++ writeFile(path); ++ return; ++ } ++ ++ if (!version.equals(currentData.getCurrentVersion())) { ++ // The version appears to have changed ++ currentData.setOldVersion(currentData.getCurrentVersion()); ++ currentData.setCurrentVersion(version); ++ writeFile(path); ++ } ++ } else { ++ // File doesn't exist, start fresh ++ currentData = new VersionData(); ++ // oldVersion is null ++ currentData.setCurrentVersion(Bukkit.getVersion()); ++ writeFile(path); ++ } ++ } ++ ++ private void writeFile(@Nonnull final Path path) { ++ try (final BufferedWriter writer = Files.newBufferedWriter( ++ path, ++ StandardCharsets.UTF_8, ++ StandardOpenOption.WRITE, ++ StandardOpenOption.CREATE, ++ StandardOpenOption.TRUNCATE_EXISTING ++ )) { ++ gson.toJson(currentData, writer); ++ } catch (final IOException e) { ++ logger.log(Level.SEVERE, "Failed to write to version history file", e); ++ } ++ } ++ ++ @Nullable ++ public VersionData getVersionData() { ++ return currentData; ++ } ++ ++ public static class VersionData { ++ private String oldVersion; ++ ++ private String currentVersion; ++ ++ @Nullable ++ public String getOldVersion() { ++ return oldVersion; ++ } ++ ++ public void setOldVersion(@Nullable String oldVersion) { ++ this.oldVersion = oldVersion; ++ } ++ ++ @Nullable ++ public String getCurrentVersion() { ++ return currentVersion; ++ } ++ ++ public void setCurrentVersion(@Nullable String currentVersion) { ++ this.currentVersion = currentVersion; ++ } ++ ++ @Override ++ public String toString() { ++ return MoreObjects.toStringHelper(this) ++ .add("oldVersion", oldVersion) ++ .add("currentVersion", currentVersion) ++ .toString(); ++ } ++ ++ @Override ++ public boolean equals(@Nullable Object o) { ++ if (this == o) { ++ return true; ++ } ++ if (o == null || getClass() != o.getClass()) { ++ return false; ++ } ++ final VersionData versionData = (VersionData) o; ++ return Objects.equals(oldVersion, versionData.oldVersion) && ++ Objects.equals(currentVersion, versionData.currentVersion); ++ } ++ ++ @Override ++ public int hashCode() { ++ return Objects.hash(oldVersion, currentVersion); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..790bad0494454ca12ee152e3de6da3da634d9b20 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +@@ -0,0 +1,104 @@ ++package io.papermc.paper; ++ ++import com.google.common.base.Strings; ++import io.papermc.paper.util.JarManifests; ++import java.time.Instant; ++import java.time.temporal.ChronoUnit; ++import java.util.Optional; ++import java.util.OptionalInt; ++import java.util.jar.Manifest; ++import net.kyori.adventure.key.Key; ++import net.minecraft.SharedConstants; ++import org.bukkit.craftbukkit.CraftServer; ++import org.bukkit.craftbukkit.Main; ++import org.jetbrains.annotations.NotNull; ++ ++public record ServerBuildInfoImpl( ++ Key brandId, ++ String brandName, ++ String minecraftVersionId, ++ String minecraftVersionName, ++ OptionalInt buildNumber, ++ Instant buildTime, ++ Optional<String> gitBranch, ++ Optional<String> gitCommit ++) implements ServerBuildInfo { ++ private static final String ATTRIBUTE_BRAND_ID = "Brand-Id"; ++ private static final String ATTRIBUTE_BRAND_NAME = "Brand-Name"; ++ private static final String ATTRIBUTE_BUILD_TIME = "Build-Time"; ++ private static final String ATTRIBUTE_BUILD_NUMBER = "Build-Number"; ++ private static final String ATTRIBUTE_GIT_BRANCH = "Git-Branch"; ++ private static final String ATTRIBUTE_GIT_COMMIT = "Git-Commit"; ++ ++ private static final String BRAND_PAPER_NAME = "Paper"; ++ ++ private static final String BUILD_DEV = "DEV"; ++ ++ public ServerBuildInfoImpl() { ++ this(JarManifests.manifest(CraftServer.class)); ++ } ++ ++ private ServerBuildInfoImpl(final Manifest manifest) { ++ this( ++ getManifestAttribute(manifest, ATTRIBUTE_BRAND_ID) ++ .map(Key::key) ++ .orElse(BRAND_PAPER_ID), ++ getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME) ++ .orElse(BRAND_PAPER_NAME), ++ SharedConstants.getCurrentVersion().getId(), ++ SharedConstants.getCurrentVersion().getName(), ++ getManifestAttribute(manifest, ATTRIBUTE_BUILD_NUMBER) ++ .map(Integer::parseInt) ++ .map(OptionalInt::of) ++ .orElse(OptionalInt.empty()), ++ getManifestAttribute(manifest, ATTRIBUTE_BUILD_TIME) ++ .map(Instant::parse) ++ .orElse(Main.BOOT_TIME), ++ getManifestAttribute(manifest, ATTRIBUTE_GIT_BRANCH), ++ getManifestAttribute(manifest, ATTRIBUTE_GIT_COMMIT) ++ ); ++ } ++ ++ @Override ++ public boolean isBrandCompatible(final @NotNull Key brandId) { ++ return brandId.equals(this.brandId); ++ } ++ ++ @Override ++ public @NotNull String asString(final @NotNull StringRepresentation representation) { ++ final StringBuilder sb = new StringBuilder(); ++ sb.append(this.minecraftVersionId); ++ sb.append('-'); ++ if (this.buildNumber.isPresent()) { ++ sb.append(this.buildNumber.getAsInt()); ++ } else { ++ sb.append(BUILD_DEV); ++ } ++ final boolean hasGitBranch = this.gitBranch.isPresent(); ++ final boolean hasGitCommit = this.gitCommit.isPresent(); ++ if (hasGitBranch || hasGitCommit) { ++ sb.append('-'); ++ } ++ if (hasGitBranch && representation == StringRepresentation.VERSION_FULL) { ++ sb.append(this.gitBranch.get()); ++ if (hasGitCommit) { ++ sb.append('@'); ++ } ++ } ++ if (hasGitCommit) { ++ sb.append(this.gitCommit.get()); ++ } ++ if (representation == StringRepresentation.VERSION_FULL) { ++ sb.append(' '); ++ sb.append('('); ++ sb.append(this.buildTime.truncatedTo(ChronoUnit.SECONDS)); ++ sb.append(')'); ++ } ++ return sb.toString(); ++ } ++ ++ private static Optional<String> getManifestAttribute(final Manifest manifest, final String name) { ++ final String value = manifest != null ? manifest.getMainAttributes().getValue(name) : null; ++ return Optional.ofNullable(Strings.emptyToNull(value)); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 8fbc764fd2802f735b9d5fac2dabca6a7cebe58e..3d9c7105bfdb1b0c63a036ba3213d51b2fb6425f 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -43,7 +43,6 @@ import java.util.Set; + import java.util.UUID; + import java.util.concurrent.CompletableFuture; + import java.util.concurrent.Executor; +-import java.util.concurrent.RejectedExecutionException; + import java.util.concurrent.atomic.AtomicReference; + import java.util.function.BooleanSupplier; + import java.util.function.Consumer; +@@ -191,8 +190,6 @@ import net.minecraft.world.phys.Vec2; + import net.minecraft.world.phys.Vec3; + import org.bukkit.Bukkit; + import org.bukkit.craftbukkit.CraftRegistry; +-import org.bukkit.craftbukkit.CraftServer; +-import org.bukkit.craftbukkit.Main; + import org.bukkit.event.server.ServerLoadEvent; + // CraftBukkit end + +@@ -1708,7 +1705,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + @DontObfuscate + public String getServerModName() { +- return "Spigot"; // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper + } + + public SystemReport fillSystemReport(SystemReport details) { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index b51c3f8c485496734ea58c15377a1215a334c765..48107f8eb50483430053b990496862d71c9f8a3e 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -218,6 +218,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + // Paper end - initialize global and world-defaults configuration + io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics ++ com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now + + this.setPvpAllowed(dedicatedserverproperties.pvp); + this.setFlightAllowed(dedicatedserverproperties.allowFlight); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java +index f077b8ff0bf0d96628db3569132696b68fd79921..5f11f5b16766f9d1d5640ae037e259bed9020384 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java +@@ -18,8 +18,10 @@ public class CraftCrashReport implements Supplier<String> { + + @Override + public String get() { ++ final io.papermc.paper.ServerBuildInfo build = io.papermc.paper.ServerBuildInfo.buildInfo(); // Paper + StringWriter value = new StringWriter(); + try { ++ value.append("\n BrandInfo: ").append(String.format("%s (%s) version %s", build.brandName(), build.brandId(), build.asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_FULL))); // Paper + value.append("\n Running: ").append(Bukkit.getName()).append(" version ").append(Bukkit.getVersion()).append(" (Implementing API version ").append(Bukkit.getBukkitVersion()).append(") ").append(String.valueOf(MinecraftServer.getServer().usesAuthentication())); + value.append("\n Plugins: {"); + for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index a2f784b28c0d974ee45d61d6a3a0096dd7161d3e..7c97ec4aa57562a8383a40e493eaa8a3697208bb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -11,8 +11,6 @@ import com.google.common.collect.MapMaker; + import com.mojang.authlib.GameProfile; + import com.mojang.brigadier.StringReader; + import com.mojang.brigadier.exceptions.CommandSyntaxException; +-import com.mojang.brigadier.tree.CommandNode; +-import com.mojang.brigadier.tree.LiteralCommandNode; + import com.mojang.serialization.Dynamic; + import com.mojang.serialization.Lifecycle; + import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +@@ -27,7 +25,6 @@ import java.net.InetAddress; + import java.util.ArrayList; + import java.util.Collections; + import java.util.Date; +-import java.util.HashMap; + import java.util.HashSet; + import java.util.Iterator; + import java.util.LinkedHashMap; +@@ -155,7 +152,6 @@ import org.bukkit.craftbukkit.ban.CraftProfileBanList; + import org.bukkit.craftbukkit.block.data.CraftBlockData; + import org.bukkit.craftbukkit.boss.CraftBossBar; + import org.bukkit.craftbukkit.boss.CraftKeyedBossbar; +-import org.bukkit.craftbukkit.command.BukkitCommandWrapper; + import org.bukkit.craftbukkit.command.CraftCommandMap; + import org.bukkit.craftbukkit.command.VanillaCommandWrapper; + import org.bukkit.craftbukkit.entity.CraftEntityFactory; +@@ -251,7 +247,6 @@ import org.bukkit.plugin.PluginManager; + import org.bukkit.plugin.ServicesManager; + import org.bukkit.plugin.SimplePluginManager; + import org.bukkit.plugin.SimpleServicesManager; +-import org.bukkit.plugin.java.JavaPluginLoader; + import org.bukkit.plugin.messaging.Messenger; + import org.bukkit.plugin.messaging.StandardMessenger; + import org.bukkit.profile.PlayerProfile; +@@ -268,7 +263,7 @@ import org.yaml.snakeyaml.error.MarkedYAMLException; + import net.md_5.bungee.api.chat.BaseComponent; // Spigot + + public final class CraftServer implements Server { +- private final String serverName = "CraftBukkit"; ++ private final String serverName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper + private final String serverVersion; + private final String bukkitVersion = Versioning.getBukkitVersion(); + private final Logger logger = Logger.getLogger("Minecraft"); +@@ -324,7 +319,7 @@ public final class CraftServer implements Server { + return player.getBukkitEntity(); + } + })); +- this.serverVersion = CraftServer.class.getPackage().getImplementationVersion(); ++ this.serverVersion = io.papermc.paper.ServerBuildInfo.buildInfo().asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); // Paper - improve version + this.structureManager = new CraftStructureManager(console.getStructureManager(), console.registryAccess()); + this.dataPackManager = new CraftDataPackManager(this.getServer().getPackRepository()); + this.serverTickManager = new CraftServerTickManager(console.tickRateManager()); +@@ -598,6 +593,13 @@ public final class CraftServer implements Server { + return this.bukkitVersion; + } + ++ // Paper start - expose game version ++ @Override ++ public String getMinecraftVersion() { ++ return console.getServerVersion(); ++ } ++ // Paper end ++ + @Override + public List<CraftPlayer> getOnlinePlayers() { + return this.playerView; +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index bc664b090e16ed27ba795c204dc5639679e6eee8..9dc72b01092783c436bc1fa8ce29ff7cdaa39b19 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -15,6 +15,7 @@ import joptsimple.OptionSet; + import joptsimple.util.PathConverter; + + public class Main { ++ public static final java.time.Instant BOOT_TIME = java.time.Instant.now(); // Paper - track initial start time + public static boolean useJline = true; + public static boolean useConsole = true; + +@@ -252,13 +253,26 @@ public class Main { + deadline.add(Calendar.DAY_OF_YEAR, -3); + if (buildDate.before(deadline.getTime())) { + System.err.println("*** Error, this build is outdated ***"); +- System.err.println("*** Please download a new build as per instructions from https://www.spigotmc.org/go/outdated-spigot ***"); ++ System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads/paper ***"); // Paper + System.err.println("*** Server will start in 20 seconds ***"); + Thread.sleep(TimeUnit.SECONDS.toMillis(20)); + } + } + + System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows ++ // Paper start - Log Java and OS versioning to help with debugging plugin issues ++ java.lang.management.RuntimeMXBean runtimeMX = java.lang.management.ManagementFactory.getRuntimeMXBean(); ++ java.lang.management.OperatingSystemMXBean osMX = java.lang.management.ManagementFactory.getOperatingSystemMXBean(); ++ if (runtimeMX != null && osMX != null) { ++ String javaInfo = "Java " + runtimeMX.getSpecVersion() + " (" + runtimeMX.getVmName() + " " + runtimeMX.getVmVersion() + ")"; ++ String osInfo = "Host: " + osMX.getName() + " " + osMX.getVersion() + " (" + osMX.getArch() + ")"; ++ ++ System.out.println("System Info: " + javaInfo + " " + osInfo); ++ } else { ++ System.out.println("Unable to read system info"); ++ } ++ // Paper end - Log Java and OS versioning to help with debugging plugin issues ++ + System.out.println("Loading libraries, please wait..."); + net.minecraft.server.Main.main(options); + } catch (Throwable t) { +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 99f134d03b76e867a3e5084c5c89e53c74d8e7af..7800e0a5aa381181d6a19d55f90490154de05c04 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -478,6 +478,11 @@ public final class CraftMagicNumbers implements UnsafeValues { + public String getTimingsServerName() { + return io.papermc.paper.configuration.GlobalConfiguration.get().timings.serverName; + } ++ ++ @Override ++ public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { ++ return new com.destroystokyo.paper.PaperVersionFetcher(); ++ } + // Paper end + + @Override +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index f697d45e0ac4e9cdc8a46121510a04c0f294d91f..e086765dec32241bc5a77afcf072c77a40c6d785 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -19,7 +19,7 @@ public class WatchdogThread extends Thread + + private WatchdogThread(long timeoutTime, boolean restart) + { +- super( "Spigot Watchdog Thread" ); ++ super( "Paper Watchdog Thread" ); + this.timeoutTime = timeoutTime; + this.restart = restart; + } +@@ -65,14 +65,14 @@ public class WatchdogThread extends Thread + { + Logger log = Bukkit.getServer().getLogger(); + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Spigot bug." ); ++ log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper + 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 Spigot bug, please report to https://www.spigotmc.org/" ); ++ 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, "Be sure to include ALL relevant console errors and Minecraft crash reports" ); +- log.log( Level.SEVERE, "Spigot version: " + Bukkit.getServer().getVersion() ); ++ log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() ); + // + if ( net.minecraft.world.level.Level.lastPhysicsProblem != null ) + { +@@ -82,7 +82,7 @@ public class WatchdogThread extends Thread + } + // + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Spigot!):" ); ++ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // +diff --git a/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo +new file mode 100644 +index 0000000000000000000000000000000000000000..79b4b25784cfeabd5f619ed5454ef843f35041db +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo +@@ -0,0 +1 @@ ++io.papermc.paper.ServerBuildInfoImpl diff --git a/patches/server/0035-Player-affects-spawning-API.patch b/patches/server/0035-Player-affects-spawning-API.patch new file mode 100644 index 0000000000..6b8ea7ffca --- /dev/null +++ b/patches/server/0035-Player-affects-spawning-API.patch @@ -0,0 +1,158 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jedediah Smith <[email protected]> +Date: Tue, 1 Mar 2016 14:47:52 -0600 +Subject: [PATCH] Player affects spawning API + + +diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java +index 3126e8cab3c40e3af47f4c8925e1c6a9523309ba..3207166061bf9c4d7bf3f38e5a9f7aff23ccd5c1 100644 +--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java ++++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java +@@ -30,6 +30,11 @@ public final class EntitySelector { + public static final Predicate<Entity> CAN_BE_COLLIDED_WITH = EntitySelector.NO_SPECTATORS.and(Entity::canBeCollidedWith); + + private EntitySelector() {} ++ // Paper start - Affects Spawning API ++ public static final Predicate<Entity> PLAYER_AFFECTS_SPAWNING = (entity) -> { ++ return !entity.isSpectator() && entity.isAlive() && entity instanceof Player player && player.affectsSpawning; ++ }; ++ // Paper end - Affects Spawning API + + public static Predicate<Entity> withinDistance(double x, double y, double z, double max) { + double d4 = max * max; +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 51b33ea29af0f4010dfb8a1a7503cb7ca463209e..e81a25462239dbb3993f31ff927bd809aef0a124 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -862,7 +862,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) { + this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause + } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) { +- Player entityhuman = this.level().getNearestPlayer(this, -1.0D); ++ Player entityhuman = this.level().findNearbyPlayer(this, -1.0D, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper - Affects Spawning API + + if (entityhuman != null) { + double d0 = entityhuman.distanceToSqr((Entity) this); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java b/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java +index 3b7fc11b7832a72fb9b0806fe9847f4e30759e7b..3cb84856c10347162a8736ae1ef65165183ec8fe 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java +@@ -27,7 +27,7 @@ public class SkeletonTrapGoal extends Goal { + + @Override + public boolean canUse() { +- return this.horse.level().hasNearbyAlivePlayer(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0D); ++ return this.horse.level().hasNearbyAlivePlayerThatAffectsSpawning(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0D); // Paper - Affects Spawning API + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java +index 95e9d38dbccbd1c43ababd707e18dfe6779256c1..9ff42b0ae2b82dc3092e38e1439d89b4ab554b17 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java +@@ -118,7 +118,7 @@ public class Silverfish extends Monster { + if (checkAnyLightMonsterSpawnRules(type, world, spawnReason, pos, random)) { + Player entityhuman = world.getNearestPlayer((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, 5.0D, true); + +- return entityhuman == null; ++ return !(entityhuman != null && !entityhuman.affectsSpawning) && entityhuman == null; // Paper - Affects Spawning API + } else { + return false; + } +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 393a9c704f4637a0e8031328d2a0facef4723dd8..d97c3c139f10a45febc0cfb1057ff6e33266228e 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +@@ -328,7 +328,7 @@ public class Zombie extends Monster { + + if (SpawnPlacements.isSpawnPositionOk(entitytypes, this.level(), blockposition) && SpawnPlacements.checkSpawnRules(entitytypes, worldserver, MobSpawnType.REINFORCEMENT, blockposition, this.level().random)) { + entityzombie.setPos((double) i1, (double) j1, (double) k1); +- if (!this.level().hasNearbyAlivePlayer((double) i1, (double) j1, (double) k1, 7.0D) && this.level().isUnobstructed(entityzombie) && this.level().noCollision((Entity) entityzombie) && !this.level().containsAnyLiquid(entityzombie.getBoundingBox())) { ++ if (!this.level().hasNearbyAlivePlayerThatAffectsSpawning((double) i1, (double) j1, (double) k1, 7.0D) && this.level().isUnobstructed(entityzombie) && this.level().noCollision((Entity) entityzombie) && !this.level().containsAnyLiquid(entityzombie.getBoundingBox())) { // Paper - Affects Spawning API + entityzombie.setTarget(entityliving, EntityTargetEvent.TargetReason.REINFORCEMENT_TARGET, true); // CraftBukkit + entityzombie.finalizeSpawn(worldserver, this.level().getCurrentDifficultyAt(entityzombie.blockPosition()), MobSpawnType.REINFORCEMENT, (SpawnGroupData) null); + worldserver.addFreshEntityWithPassengers(entityzombie, CreatureSpawnEvent.SpawnReason.REINFORCEMENTS); // CraftBukkit +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 f652859457171759f72800df7a8d5475a0486759..0d1d8ad353ff2f2bd478cfdc2f6bebad5203e190 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -194,6 +194,7 @@ public abstract class Player extends LivingEntity { + public Entity currentExplosionCause; + private boolean ignoreFallDamageFromCurrentImpulse; + private int currentImpulseContextResetGraceTime; ++ public boolean affectsSpawning = true; // Paper - Affects Spawning API + + // CraftBukkit start + public boolean fauxSleeping; +diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java +index cc54da2987fafcbd69153c33033a6f272dd3be66..418e29971326008ebca0cc4b696a41a49c1c7bb7 100644 +--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java ++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java +@@ -58,7 +58,7 @@ public abstract class BaseSpawner { + } + + public boolean isNearPlayer(Level world, BlockPos pos) { +- return world.hasNearbyAlivePlayer((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); ++ 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 + } + + public void clientTick(Level world, BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index f38f62e777d88a783e1e3b7e1a48da921cc67cf4..77ae7882a08441d9a80b50492be5e48487a2fdab 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -74,6 +74,11 @@ public interface EntityGetter { + } + } + ++ // Paper start - Affects Spawning API ++ default @Nullable Player findNearbyPlayer(Entity entity, double maxDistance, @Nullable Predicate<Entity> predicate) { ++ return this.getNearestPlayer(entity.getX(), entity.getY(), entity.getZ(), maxDistance, predicate); ++ } ++ // Paper end - Affects Spawning API + @Nullable + default Player getNearestPlayer(double x, double y, double z, double maxDistance, @Nullable Predicate<Entity> targetPredicate) { + double d = -1.0; +@@ -103,6 +108,20 @@ public interface EntityGetter { + return this.getNearestPlayer(x, y, z, maxDistance, predicate); + } + ++ // Paper start - Affects Spawning API ++ default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { ++ for (Player player : this.players()) { ++ if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check ++ double distanceSqr = player.distanceToSqr(x, y, z); ++ if (range < 0.0D || distanceSqr < range * range) { ++ return true; ++ } ++ } ++ } ++ return false; ++ } ++ // Paper end - Affects Spawning API ++ + 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)) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 0bea424418984e17193ff107d2b4cf13278a3d3e..276b12f8451fd3e7922175980b24fb26a56572ef 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2424,6 +2424,17 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.getHandle().language; + } + ++ // Paper start ++ public void setAffectsSpawning(boolean affects) { ++ this.getHandle().affectsSpawning = affects; ++ } ++ ++ @Override ++ public boolean getAffectsSpawning() { ++ return this.getHandle().affectsSpawning; ++ } ++ // Paper end ++ + @Override + public void updateCommands() { + if (this.getHandle().connection == null) return; diff --git a/patches/server/0036-Only-refresh-abilities-if-needed.patch b/patches/server/0036-Only-refresh-abilities-if-needed.patch new file mode 100644 index 0000000000..4843159987 --- /dev/null +++ b/patches/server/0036-Only-refresh-abilities-if-needed.patch @@ -0,0 +1,25 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 23:12:03 -0600 +Subject: [PATCH] Only refresh abilities if needed + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 276b12f8451fd3e7922175980b24fb26a56572ef..153bc2407de2acc4452aa7959b39669ddb66b4da 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2092,12 +2092,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setFlying(boolean value) { ++ boolean needsUpdate = getHandle().getAbilities().flying != value; // Paper - Only refresh abilities if needed + if (!this.getAllowFlight()) { + Preconditions.checkArgument(!value, "Player is not allowed to fly (check #getAllowFlight())"); + } + + this.getHandle().getAbilities().flying = value; +- this.getHandle().onUpdateAbilities(); ++ if (needsUpdate) this.getHandle().onUpdateAbilities(); // Paper - Only refresh abilities if needed + } + + @Override diff --git a/patches/server/0037-Entity-Origin-API.patch b/patches/server/0037-Entity-Origin-API.patch new file mode 100644 index 0000000000..eeca12666e --- /dev/null +++ b/patches/server/0037-Entity-Origin-API.patch @@ -0,0 +1,121 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Byteflux <[email protected]> +Date: Tue, 1 Mar 2016 23:45:08 -0600 +Subject: [PATCH] Entity Origin API + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 9675d91e4e7ed46147c3f7a11dd65122fe998dc2..711318ddc706e72dbd8cea1c541058c881086f21 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -2141,6 +2141,15 @@ public class ServerLevel extends Level implements WorldGenLevel { + entity.updateDynamicGameEventListener(DynamicGameEventListener::add); + entity.inWorld = true; // CraftBukkit - Mark entity as in world + entity.valid = true; // CraftBukkit ++ // Paper start - Entity origin API ++ if (entity.getOriginVector() == null) { ++ entity.setOrigin(entity.getBukkitEntity().getLocation()); ++ } ++ // Default to current world if unknown, gross assumption but entities rarely change world ++ if (entity.getOriginWorld() == null) { ++ entity.setOrigin(entity.getOriginVector().toLocation(getWorld())); ++ } ++ // Paper end - Entity origin API + } + + public void onTrackingEnd(Entity entity) { +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 4f321f13352636999c3abc5332e50c747fb45cc9..c355e8d71b4941b2ad43740763209927a3279336 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -328,7 +328,27 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public long activatedTick = Integer.MIN_VALUE; + public void inactiveTick() { } + // Spigot end ++ // Paper start - Entity origin API ++ @javax.annotation.Nullable ++ private org.bukkit.util.Vector origin; ++ @javax.annotation.Nullable ++ private UUID originWorld; + ++ public void setOrigin(@javax.annotation.Nonnull Location location) { ++ this.origin = location.toVector(); ++ this.originWorld = location.getWorld().getUID(); ++ } ++ ++ @javax.annotation.Nullable ++ public org.bukkit.util.Vector getOriginVector() { ++ return this.origin != null ? this.origin.clone() : null; ++ } ++ ++ @javax.annotation.Nullable ++ public UUID getOriginWorld() { ++ return this.originWorld; ++ } ++ // Paper end - Entity origin API + public float getBukkitYaw() { + return this.yRot; + } +@@ -2153,6 +2173,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.bukkitEntity.storeBukkitValues(nbttagcompound); + } + // CraftBukkit end ++ // Paper start ++ if (this.origin != null) { ++ UUID originWorld = this.originWorld != null ? this.originWorld : this.level != null ? this.level.getWorld().getUID() : null; ++ if (originWorld != null) { ++ nbttagcompound.putUUID("Paper.OriginWorld", originWorld); ++ } ++ nbttagcompound.put("Paper.Origin", this.newDoubleList(origin.getX(), origin.getY(), origin.getZ())); ++ } ++ // Paper end + return nbttagcompound; + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); +@@ -2280,6 +2309,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + // CraftBukkit end + ++ // Paper start ++ ListTag originTag = nbt.getList("Paper.Origin", net.minecraft.nbt.Tag.TAG_DOUBLE); ++ if (!originTag.isEmpty()) { ++ UUID originWorld = null; ++ if (nbt.contains("Paper.OriginWorld")) { ++ originWorld = nbt.getUUID("Paper.OriginWorld"); ++ } else if (this.level != null) { ++ originWorld = this.level.getWorld().getUID(); ++ } ++ this.originWorld = originWorld; ++ origin = new org.bukkit.util.Vector(originTag.getDouble(0), originTag.getDouble(1), originTag.getDouble(2)); ++ } ++ // Paper end ++ + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); + CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being loaded"); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 269326e7689eba91bcfd3475006e8cbf8f5694ef..7b45a1216ff824f1b528bb5759d10b70858832a1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -963,4 +963,21 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return this.spigot; + } + // Spigot end ++ ++ // Paper start - entity origin API ++ @Override ++ public Location getOrigin() { ++ Vector originVector = this.getHandle().getOriginVector(); ++ if (originVector == null) { ++ return null; ++ } ++ World world = this.getWorld(); ++ if (this.getHandle().getOriginWorld() != null) { ++ world = org.bukkit.Bukkit.getWorld(this.getHandle().getOriginWorld()); ++ } ++ ++ //noinspection ConstantConditions ++ return originVector.toLocation(world); ++ } ++ // Paper end - entity origin API + } diff --git a/patches/server/0038-Prevent-block-entity-and-entity-crashes.patch b/patches/server/0038-Prevent-block-entity-and-entity-crashes.patch new file mode 100644 index 0000000000..c4f8050de2 --- /dev/null +++ b/patches/server/0038-Prevent-block-entity-and-entity-crashes.patch @@ -0,0 +1,66 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Tue, 1 Mar 2016 23:52:34 -0600 +Subject: [PATCH] Prevent block entity and entity crashes + + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 1702cd6aa8b4fc29b8f2539604f6e203bf95d020..e19ee11905417918c7ec142fd2016ab3f000e4e2 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -731,11 +731,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + try { + tickConsumer.accept(entity); + } catch (Throwable throwable) { +- CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking entity"); +- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being ticked"); +- +- entity.fillCrashReportCategory(crashreportsystemdetails); +- throw new ReportedException(crashreport); ++ // Paper start - Prevent block 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); ++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ // Paper end - Prevent block entity and entity crashes + } + } + +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 6e35709f2a7c32050908e7e5af5529c9f342b787..d20f71a2098b327423cbdbbc096aa9e358cb809d 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 +@@ -263,7 +263,12 @@ public abstract class BlockEntity { + return s + " // " + this.getClass().getCanonicalName(); + }); + if (this.level != null) { +- CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.getBlockState()); ++ // Paper start - Prevent block entity and entity crashes ++ BlockState block = this.getBlockState(); ++ if (block != null) { ++ CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, block); ++ } ++ // Paper end - Prevent block entity and entity crashes + CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.level.getBlockState(this.worldPosition)); + } + } +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 05d959ccc424aaaa465ec256213f2ec4d44ef8b5..329f2210b73a75fc91a5ba06a1ed7f66c5aa2680 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -1073,11 +1073,11 @@ public class LevelChunk extends ChunkAccess { + + gameprofilerfiller.pop(); + } catch (Throwable throwable) { +- CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking block entity"); +- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block entity being ticked"); +- +- this.blockEntity.fillCrashReportCategory(crashreportsystemdetails); +- throw new ReportedException(crashreport); ++ // Paper start - Prevent block entity and entity crashes ++ final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ()); ++ net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); ++ LevelChunk.this.removeBlockEntity(this.getPos()); ++ // Paper end - Prevent block entity and entity crashes + // Spigot start + } finally { + this.blockEntity.tickTimer.stopTiming(); diff --git a/patches/server/0039-Configurable-top-of-nether-void-damage.patch b/patches/server/0039-Configurable-top-of-nether-void-damage.patch new file mode 100644 index 0000000000..e3e87945ee --- /dev/null +++ b/patches/server/0039-Configurable-top-of-nether-void-damage.patch @@ -0,0 +1,49 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Tue, 1 Mar 2016 23:58:50 -0600 +Subject: [PATCH] Configurable top of nether void damage + +Co-authored-by: Jake Potrebic <[email protected]> + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index c355e8d71b4941b2ad43740763209927a3279336..fafccc1c98cbc630dc71db623184a62f08618b03 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -706,7 +706,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public void checkBelowWorld() { +- if (this.getY() < (double) (this.level().getMinBuildHeight() - 64)) { ++ // Paper start - Configurable nether ceiling damage ++ if (this.getY() < (double) (this.level.getMinBuildHeight() - 64) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER ++ && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v) ++ && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) { ++ // Paper end - Configurable nether ceiling damage + this.onBelowWorld(); + } + +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index 46448259cd60ea5e2e08fb58cd2b2b7f8a4ec3cc..aef1dd28da7e0c0a13a0a7a5b52daa27635c48ea 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -58,7 +58,7 @@ public class PortalForcer { + }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos); + + Objects.requireNonNull(worldborder); +- return stream.filter(worldborder::isWithinBounds).filter((blockposition1) -> { ++ return stream.filter(worldborder::isWithinBounds).filter(pos -> !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))).filter((blockposition1) -> { // Paper - Configurable nether ceiling damage + return this.level.getBlockState(blockposition1).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); + }).min(Comparator.comparingDouble((BlockPos blockposition1) -> { // CraftBukkit - decompile error + return blockposition1.distSqr(blockposition); +@@ -79,6 +79,11 @@ public class PortalForcer { + BlockPos blockposition2 = null; + WorldBorder worldborder = this.level.getWorldBorder(); + int i = Math.min(this.level.getMaxBuildHeight(), this.level.getMinBuildHeight() + this.level.getLogicalHeight()) - 1; ++ // Paper start - Configurable nether ceiling damage; make sure the max height doesn't exceed the void damage height ++ if (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.enabled()) { ++ i = Math.min(i, this.level.paperConfig().environment.netherCeilingVoidDamageHeight.intValue() - 1); ++ } ++ // Paper end - Configurable nether ceiling damage + boolean flag = true; + BlockPos.MutableBlockPos blockposition_mutableblockposition = blockposition.mutable(); + Iterator iterator = BlockPos.spiralAround(blockposition, createRadius, Direction.EAST, Direction.SOUTH).iterator(); // CraftBukkit diff --git a/patches/server/0040-Check-online-mode-before-converting-and-renaming-pla.patch b/patches/server/0040-Check-online-mode-before-converting-and-renaming-pla.patch new file mode 100644 index 0000000000..f89cf885a5 --- /dev/null +++ b/patches/server/0040-Check-online-mode-before-converting-and-renaming-pla.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Wed, 2 Mar 2016 00:03:55 -0600 +Subject: [PATCH] Check online mode before converting and renaming player data + + +diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java +index 79222a04ef104bf2eed85684479384b7c350ea19..e11c8523e633d2a8e3cea7ecd54978b2958b9684 100644 +--- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java ++++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java +@@ -78,7 +78,7 @@ public class PlayerDataStorage { + File file1 = new File(file, s1 + s); + // Spigot Start + boolean usingWrongFile = false; +- if ( !file1.exists() ) ++ if ( org.bukkit.Bukkit.getOnlineMode() && !file.exists() ) // Paper - Check online mode first + { + file1 = new File( file, java.util.UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + name ).getBytes( java.nio.charset.StandardCharsets.UTF_8 ) ).toString() + s ); + if ( file1.exists() ) diff --git a/patches/server/0041-Add-more-entities-to-activation-range-ignore-list.patch b/patches/server/0041-Add-more-entities-to-activation-range-ignore-list.patch new file mode 100644 index 0000000000..c402bdafa2 --- /dev/null +++ b/patches/server/0041-Add-more-entities-to-activation-range-ignore-list.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Wed, 2 Mar 2016 00:32:25 -0600 +Subject: [PATCH] Add more entities to activation range ignore list + + +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index a5da6c1cae0afbde684be250e2fc3c0c32a1265b..6d51464f6368151e8acc532414ee223714584e96 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -92,6 +92,9 @@ public class ActivationRange + || entity instanceof AbstractHurtingProjectile + || entity instanceof LightningBolt + || entity instanceof PrimedTnt ++ || entity instanceof net.minecraft.world.entity.item.FallingBlockEntity // Paper - Always tick falling blocks ++ || entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart // Paper ++ || entity instanceof net.minecraft.world.entity.vehicle.Boat // Paper + || entity instanceof EndCrystal + || entity instanceof FireworkRocketEntity + || entity instanceof ThrownTrident ) diff --git a/patches/server/0042-Configurable-end-credits.patch b/patches/server/0042-Configurable-end-credits.patch new file mode 100644 index 0000000000..7d2becba09 --- /dev/null +++ b/patches/server/0042-Configurable-end-credits.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: DoctorDark <[email protected]> +Date: Wed, 16 Mar 2016 02:21:39 -0500 +Subject: [PATCH] Configurable end credits + + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index a8a7f1fcf235508d9437ec7c550c3a3853321b78..2022667e631d0ae1c7e7ffcfeca81091aa7c57c7 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -1164,6 +1164,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + this.unRide(); + this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); + if (!this.wonGame) { ++ if (this.level().paperConfig().misc.disableEndCredits) this.seenCredits = true; // Paper - Option to disable end credits + this.wonGame = true; + this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.0F)); + this.seenCredits = true; diff --git a/patches/server/0043-Fix-lag-from-explosions-processing-dead-entities.patch b/patches/server/0043-Fix-lag-from-explosions-processing-dead-entities.patch new file mode 100644 index 0000000000..ae3fb7c15b --- /dev/null +++ b/patches/server/0043-Fix-lag-from-explosions-processing-dead-entities.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Iceee <[email protected]> +Date: Wed, 2 Mar 2016 01:39:52 -0600 +Subject: [PATCH] Fix lag from explosions processing dead entities + + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 4a30928cd11f528f8ac06950b8052ebb7f2dd33c..458020575050284544761ec61c52abac7bfd15be 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -227,7 +227,7 @@ public class Explosion { + int i1 = Mth.floor(this.y + (double) f2 + 1.0D); + int j1 = Mth.floor(this.z - (double) f2 - 1.0D); + int k1 = Mth.floor(this.z + (double) f2 + 1.0D); +- List<Entity> list = this.level.getEntities(this.source, new AABB((double) i, (double) l, (double) j1, (double) j, (double) i1, (double) k1)); ++ List<Entity> list = this.level.getEntities(this.source, new AABB((double) i, (double) l, (double) j1, (double) j, (double) i1, (double) k1), (com.google.common.base.Predicate<Entity>) entity -> entity.isAlive() && !entity.isSpectator()); // Paper - Fix lag from explosions processing dead entities + Vec3 vec3d = new Vec3(this.x, this.y, this.z); + Iterator iterator = list.iterator(); + diff --git a/patches/server/0044-Optimize-explosions.patch b/patches/server/0044-Optimize-explosions.patch new file mode 100644 index 0000000000..e3fc8cb992 --- /dev/null +++ b/patches/server/0044-Optimize-explosions.patch @@ -0,0 +1,133 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Byteflux <[email protected]> +Date: Wed, 2 Mar 2016 11:59:48 -0600 +Subject: [PATCH] Optimize explosions + +The process of determining an entity's exposure from explosions can be +expensive when there are hundreds or more entities in range. + +This patch adds a per-tick cache that is used for storing and retrieving +an entity's exposure during an explosion. + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 3d9c7105bfdb1b0c63a036ba3213d51b2fb6425f..5c82bf57575bbe8bea8d39bead51c6860ac86e37 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1579,6 +1579,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + this.profiler.pop(); + this.profiler.pop(); ++ worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions + } + + this.profiler.popPush("connection"); +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 458020575050284544761ec61c52abac7bfd15be..55d66aa8264d5b444a23e2132206bcc9835cfe00 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -279,7 +279,7 @@ public class Explosion { + // CraftBukkit end + } + +- double d12 = (1.0D - d7) * (double) Explosion.getSeenPercent(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); ++ double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions + double d13; + + if (entity instanceof LivingEntity) { +@@ -539,4 +539,84 @@ public class Explosion { + + private BlockInteraction() {} + } ++ // Paper start - Optimize explosions ++ private float getBlockDensity(Vec3 vec3d, Entity entity) { ++ if (!this.level.paperConfig().environment.optimizeExplosions) { ++ return getSeenPercent(vec3d, entity); ++ } ++ CacheKey key = new CacheKey(this, entity.getBoundingBox()); ++ Float blockDensity = this.level.explosionDensityCache.get(key); ++ if (blockDensity == null) { ++ blockDensity = getSeenPercent(vec3d, entity); ++ this.level.explosionDensityCache.put(key, blockDensity); ++ } ++ ++ return blockDensity; ++ } ++ ++ static class CacheKey { ++ private final Level world; ++ private final double posX, posY, posZ; ++ private final double minX, minY, minZ; ++ private final double maxX, maxY, maxZ; ++ ++ public CacheKey(Explosion explosion, AABB aabb) { ++ this.world = explosion.level; ++ this.posX = explosion.x; ++ this.posY = explosion.y; ++ this.posZ = explosion.z; ++ this.minX = aabb.minX; ++ this.minY = aabb.minY; ++ this.minZ = aabb.minZ; ++ this.maxX = aabb.maxX; ++ this.maxY = aabb.maxY; ++ this.maxZ = aabb.maxZ; ++ } ++ ++ @Override ++ public boolean equals(Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ ++ CacheKey cacheKey = (CacheKey) o; ++ ++ if (Double.compare(cacheKey.posX, posX) != 0) return false; ++ if (Double.compare(cacheKey.posY, posY) != 0) return false; ++ if (Double.compare(cacheKey.posZ, posZ) != 0) return false; ++ if (Double.compare(cacheKey.minX, minX) != 0) return false; ++ if (Double.compare(cacheKey.minY, minY) != 0) return false; ++ if (Double.compare(cacheKey.minZ, minZ) != 0) return false; ++ if (Double.compare(cacheKey.maxX, maxX) != 0) return false; ++ if (Double.compare(cacheKey.maxY, maxY) != 0) return false; ++ if (Double.compare(cacheKey.maxZ, maxZ) != 0) return false; ++ return world.equals(cacheKey.world); ++ } ++ ++ @Override ++ public int hashCode() { ++ int result; ++ long temp; ++ result = world.hashCode(); ++ temp = Double.doubleToLongBits(posX); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(posY); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(posZ); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(minX); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(minY); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(minZ); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(maxX); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(maxY); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ temp = Double.doubleToLongBits(maxZ); ++ result = 31 * result + (int) (temp ^ (temp >>> 32)); ++ return result; ++ } ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index e19ee11905417918c7ec142fd2016ab3f000e4e2..da7b1b705da9f17de858f72a20d3a932cd8f7fad 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -168,6 +168,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + private org.spigotmc.TickLimiter entityLimiter; + private org.spigotmc.TickLimiter tileLimiter; + private int tileTickPosition; ++ public final Map<Explosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions + + public CraftWorld getWorld() { + return this.world; diff --git a/patches/server/0045-Disable-explosion-knockback.patch b/patches/server/0045-Disable-explosion-knockback.patch new file mode 100644 index 0000000000..233aa30581 --- /dev/null +++ b/patches/server/0045-Disable-explosion-knockback.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sudzzy <[email protected]> +Date: Wed, 2 Mar 2016 14:48:03 -0600 +Subject: [PATCH] Disable explosion knockback + + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 55d66aa8264d5b444a23e2132206bcc9835cfe00..d93ed33d5ae72e9dd3e6cf044ef79e4b9689dc1c 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -285,7 +285,7 @@ public class Explosion { + if (entity instanceof LivingEntity) { + LivingEntity entityliving = (LivingEntity) entity; + +- d13 = d12 * (1.0D - entityliving.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE)); ++ d13 = entity instanceof Player && this.level.paperConfig().environment.disableExplosionKnockback ? 0 : d12 * (1.0D - entityliving.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE)); + } else { + d13 = d12; + } +@@ -310,7 +310,7 @@ public class Explosion { + if (entity instanceof Player) { + Player entityhuman = (Player) entity; + +- if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying)) { ++ if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying) && !level.paperConfig().environment.disableExplosionKnockback) { // Paper - Option to disable explosion knockback + this.hitPlayers.put(entityhuman, vec3d1); + } + } diff --git a/patches/server/0046-Disable-thunder.patch b/patches/server/0046-Disable-thunder.patch new file mode 100644 index 0000000000..3dea5304a7 --- /dev/null +++ b/patches/server/0046-Disable-thunder.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sudzzy <[email protected]> +Date: Wed, 2 Mar 2016 14:52:43 -0600 +Subject: [PATCH] Disable thunder + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 711318ddc706e72dbd8cea1c541058c881086f21..ff80e95bf91b8f0e60dfc1183dd9fba2dba2719d 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -616,7 +616,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + gameprofilerfiller.push("thunder"); +- if (flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot ++ if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder + BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); + + if (this.isRainingAt(blockposition)) { diff --git a/patches/server/0047-Disable-ice-and-snow.patch b/patches/server/0047-Disable-ice-and-snow.patch new file mode 100644 index 0000000000..f693d7666f --- /dev/null +++ b/patches/server/0047-Disable-ice-and-snow.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sudzzy <[email protected]> +Date: Wed, 2 Mar 2016 14:57:24 -0600 +Subject: [PATCH] Disable ice and snow + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index ff80e95bf91b8f0e60dfc1183dd9fba2dba2719d..d0788c4223891117c042aa4243e6804b4ed33aea 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -646,11 +646,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + + gameprofilerfiller.popPush("iceandsnow"); + ++ if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow + for (int l = 0; l < randomTickSpeed; ++l) { + if (this.random.nextInt(48) == 0) { + this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15)); + } + } ++ } // Paper - Option to disable ice and snow + + gameprofilerfiller.popPush("tickBlocks"); + timings.chunkTicksBlocks.startTiming(); // Paper diff --git a/patches/server/0048-Configurable-mob-spawner-tick-rate.patch b/patches/server/0048-Configurable-mob-spawner-tick-rate.patch new file mode 100644 index 0000000000..deb1ddde3f --- /dev/null +++ b/patches/server/0048-Configurable-mob-spawner-tick-rate.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sudzzy <[email protected]> +Date: Wed, 2 Mar 2016 15:03:53 -0600 +Subject: [PATCH] Configurable mob spawner tick rate + + +diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java +index 418e29971326008ebca0cc4b696a41a49c1c7bb7..aa54237205989f619ac6a3faa2e4285427b9e31d 100644 +--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java ++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java +@@ -49,6 +49,7 @@ public abstract class BaseSpawner { + public int maxNearbyEntities = 6; + public int requiredPlayerRange = 16; + public int spawnRange = 4; ++ private int tickDelay = 0; // Paper - Configurable mob spawner tick rate + + public BaseSpawner() {} + +@@ -83,13 +84,18 @@ public abstract class BaseSpawner { + } + + public void serverTick(ServerLevel world, BlockPos pos) { ++ // Paper start - Configurable mob spawner tick rate ++ if (spawnDelay > 0 && --tickDelay > 0) return; ++ tickDelay = world.paperConfig().tickRates.mobSpawner; ++ if (tickDelay == -1) { return; } // If disabled ++ // Paper end - Configurable mob spawner tick rate + if (this.isNearPlayer(world, pos)) { +- if (this.spawnDelay == -1) { ++ if (this.spawnDelay < -tickDelay) { // Paper - Configurable mob spawner tick rate + this.delay(world, pos); + } + + if (this.spawnDelay > 0) { +- --this.spawnDelay; ++ this.spawnDelay -= tickDelay; // Paper - Configurable mob spawner tick rate + } else { + boolean flag = false; + RandomSource randomsource = world.getRandom(); diff --git a/patches/server/0049-Implement-PlayerLocaleChangeEvent.patch b/patches/server/0049-Implement-PlayerLocaleChangeEvent.patch new file mode 100644 index 0000000000..4c300bff09 --- /dev/null +++ b/patches/server/0049-Implement-PlayerLocaleChangeEvent.patch @@ -0,0 +1,56 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Isaac Moore <[email protected]> +Date: Tue, 19 Apr 2016 14:09:31 -0500 +Subject: [PATCH] Implement PlayerLocaleChangeEvent + + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 2022667e631d0ae1c7e7ffcfeca81091aa7c57c7..de55fbbc92d180ca49427b134e6fc8e20f52607c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -236,7 +236,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + private int levitationStartTime; + private boolean disconnected; + private int requestedViewDistance; +- public String language = "en_us"; // CraftBukkit - default ++ public String language = null; // CraftBukkit - default // Paper - default to null + public java.util.Locale adventure$locale = java.util.Locale.US; // Paper + @Nullable + private Vec3 startingToFallPosition; +@@ -293,7 +293,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + this.lastActionTime = Util.getMillis(); + this.recipeBook = new ServerRecipeBook(); + this.requestedViewDistance = 2; +- this.language = "en_us"; ++ this.language = null; // Paper - default to null + this.lastSectionPos = SectionPos.of(0, 0, 0); + this.chunkTrackingView = ChunkTrackingView.EMPTY; + this.respawnDimension = Level.OVERWORLD; +@@ -2054,9 +2054,10 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + PlayerChangedMainHandEvent event = new PlayerChangedMainHandEvent(this.getBukkitEntity(), this.getMainArm() == HumanoidArm.LEFT ? MainHand.LEFT : MainHand.RIGHT); + this.server.server.getPluginManager().callEvent(event); + } +- if (!this.language.equals(clientOptions.language())) { ++ if (this.language == null || !this.language.equals(clientOptions.language())) { // Paper + PlayerLocaleChangeEvent event = new PlayerLocaleChangeEvent(this.getBukkitEntity(), clientOptions.language()); + this.server.server.getPluginManager().callEvent(event); ++ this.server.server.getPluginManager().callEvent(new com.destroystokyo.paper.event.player.PlayerLocaleChangeEvent(this.getBukkitEntity(), this.language, clientOptions.language())); // Paper + } + // CraftBukkit end + this.language = clientOptions.language(); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 153bc2407de2acc4452aa7959b39669ddb66b4da..e42b928d7019b636d4fd092d4a222208a7787bcb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2422,7 +2422,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public String getLocale() { +- return this.getHandle().language; ++ // Paper start - Locale change event ++ final String locale = this.getHandle().language; ++ return locale != null ? locale : "en_us"; ++ // Paper end + } + + // Paper start diff --git a/patches/server/0050-Add-BeaconEffectEvent.patch b/patches/server/0050-Add-BeaconEffectEvent.patch new file mode 100644 index 0000000000..f26dbf1ae4 --- /dev/null +++ b/patches/server/0050-Add-BeaconEffectEvent.patch @@ -0,0 +1,66 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Byteflux <[email protected]> +Date: Wed, 2 Mar 2016 23:30:53 -0600 +Subject: [PATCH] Add BeaconEffectEvent + + +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 9fc5f72ac2d23a03584d3c0357bc1a55ea40bab3..fc915797d2a085447747d9ce23a5a354fb3eb6b6 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 +@@ -1,5 +1,6 @@ + package net.minecraft.world.level.block.entity; + ++import com.destroystokyo.paper.event.block.BeaconEffectEvent; + import com.google.common.collect.ImmutableList; + import com.google.common.collect.Lists; + import java.util.Collection; +@@ -46,6 +47,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.levelgen.Heightmap; + import net.minecraft.world.phys.AABB; + // CraftBukkit start ++import org.bukkit.craftbukkit.event.CraftEventFactory; + import org.bukkit.craftbukkit.potion.CraftPotionUtil; + import org.bukkit.potion.PotionEffect; + // CraftBukkit end +@@ -295,15 +297,23 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + } + } + +- private static void applyEffect(List list, @Nullable Holder<MobEffect> holder, int j, int b0) { +- { ++ private static void applyEffect(List list, @Nullable Holder<MobEffect> holder, int j, int b0, boolean isPrimary, BlockPos worldPosition) { // Paper - BeaconEffectEvent ++ if (!list.isEmpty()) { // Paper - BeaconEffectEvent + Iterator iterator = list.iterator(); + + Player entityhuman; ++ // Paper start - BeaconEffectEvent ++ org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(((Player) list.get(0)).level(), worldPosition); ++ PotionEffect effect = CraftPotionUtil.toBukkit(new MobEffectInstance(holder, j, b0, true, true)); ++ // Paper end - BeaconEffectEvent + + while (iterator.hasNext()) { +- entityhuman = (Player) iterator.next(); +- entityhuman.addEffect(new MobEffectInstance(holder, j, b0, true, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.BEACON); ++ // Paper start - BeaconEffectEvent ++ entityhuman = (ServerPlayer) iterator.next(); ++ BeaconEffectEvent event = new BeaconEffectEvent(block, effect, (org.bukkit.entity.Player) entityhuman.getBukkitEntity(), isPrimary); ++ if (CraftEventFactory.callEvent(event).isCancelled()) continue; ++ entityhuman.addEffect(new MobEffectInstance(CraftPotionUtil.fromBukkit(event.getEffect())), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.BEACON); ++ // Paper end - BeaconEffectEvent + } + } + } +@@ -326,10 +336,10 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + int j = BeaconBlockEntity.getLevel(beaconLevel); + List list = BeaconBlockEntity.getHumansInRange(world, pos, beaconLevel); + +- BeaconBlockEntity.applyEffect(list, primaryEffect, j, b0); ++ BeaconBlockEntity.applyEffect(list, primaryEffect, j, b0, true, pos); // Paper - BeaconEffectEvent + + if (BeaconBlockEntity.hasSecondaryEffect(beaconLevel, primaryEffect, secondaryEffect)) { +- BeaconBlockEntity.applyEffect(list, secondaryEffect, j, 0); ++ BeaconBlockEntity.applyEffect(list, secondaryEffect, j, 0, false, pos); // Paper - BeaconEffectEvent + } + } + diff --git a/patches/server/0051-Configurable-container-update-tick-rate.patch b/patches/server/0051-Configurable-container-update-tick-rate.patch new file mode 100644 index 0000000000..9a0819ff78 --- /dev/null +++ b/patches/server/0051-Configurable-container-update-tick-rate.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sudzzy <[email protected]> +Date: Wed, 2 Mar 2016 23:34:44 -0600 +Subject: [PATCH] Configurable container update tick rate + + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index de55fbbc92d180ca49427b134e6fc8e20f52607c..916ba7c09b077bc3c9ed9fad579e607e4c065e06 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -267,6 +267,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + public final Object object; + private int containerCounter; + public boolean wonGame; ++ private int containerUpdateDelay; // Paper - Configurable container update tick rate + + // CraftBukkit start + public CraftPlayer.TransferCookieConnection transferCookieConnection; +@@ -698,7 +699,12 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + --this.invulnerableTime; + } + +- this.containerMenu.broadcastChanges(); ++ // Paper start - Configurable container update tick rate ++ if (--containerUpdateDelay <= 0) { ++ this.containerMenu.broadcastChanges(); ++ containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate; ++ } ++ // Paper end - Configurable container update tick rate + if (!this.level().isClientSide && !this.containerMenu.stillValid(this)) { + this.closeContainer(); + this.containerMenu = this.inventoryMenu; diff --git a/patches/server/0052-Use-UserCache-for-player-heads.patch b/patches/server/0052-Use-UserCache-for-player-heads.patch new file mode 100644 index 0000000000..f13d9d9f3e --- /dev/null +++ b/patches/server/0052-Use-UserCache-for-player-heads.patch @@ -0,0 +1,25 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Techcable <[email protected]> +Date: Wed, 2 Mar 2016 23:42:37 -0600 +Subject: [PATCH] Use UserCache for player heads + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java +index 06dc866071742b513109cf065a312d3e58d2d66d..b5a287349a3a200cc030ef6c2e76c24727ccfb5b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java +@@ -202,7 +202,13 @@ class CraftMetaSkull extends CraftMetaItem implements SkullMeta { + if (name == null) { + this.setProfile(null); + } else { +- this.setProfile(new GameProfile(Util.NIL_UUID, name)); ++ // Paper start - Use Online Players Skull ++ GameProfile newProfile = null; ++ net.minecraft.server.level.ServerPlayer player = net.minecraft.server.MinecraftServer.getServer().getPlayerList().getPlayerByName(name); ++ if (player != null) newProfile = player.getGameProfile(); ++ if (newProfile == null) newProfile = new GameProfile(Util.NIL_UUID, name); ++ this.setProfile(newProfile); ++ // Paper end + } + + return true; diff --git a/patches/server/0053-Disable-spigot-tick-limiters.patch b/patches/server/0053-Disable-spigot-tick-limiters.patch new file mode 100644 index 0000000000..2e19fda1e1 --- /dev/null +++ b/patches/server/0053-Disable-spigot-tick-limiters.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Wed, 2 Mar 2016 23:45:17 -0600 +Subject: [PATCH] Disable spigot tick limiters + + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index da7b1b705da9f17de858f72a20d3a932cd8f7fad..68436413645e0e33f22cdee0ea101ca01b343d75 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -704,9 +704,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + boolean flag = this.tickRateManager().runsNormally(); + + int tilesThisCycle = 0; +- for (this.tileLimiter.initTick(); +- tilesThisCycle < this.blockEntityTickers.size() && (tilesThisCycle % 10 != 0 || this.tileLimiter.shouldContinue()); +- this.tileTickPosition++, tilesThisCycle++) { ++ for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters + this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; + TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition); + // Spigot end diff --git a/patches/server/0054-Add-PlayerInitialSpawnEvent.patch b/patches/server/0054-Add-PlayerInitialSpawnEvent.patch new file mode 100644 index 0000000000..cc08d58fcb --- /dev/null +++ b/patches/server/0054-Add-PlayerInitialSpawnEvent.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Steve Anton <[email protected]> +Date: Thu, 3 Mar 2016 00:09:38 -0600 +Subject: [PATCH] Add PlayerInitialSpawnEvent + +For modifying a player's initial spawn location as they join the server + +This is a duplicate API from spigot, so use our duplicate subclass and +improve setPosition to use raw + +== AT == +public net.minecraft.world.entity.Entity setRot(FF)V + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 62174dae20bd9ff092238f1437f7e2b0114ef83b..a3e695c321b9164822506e787bb5c54813aad7c2 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -226,7 +226,7 @@ public abstract class PlayerList { + + // Spigot start - spawn location event + Player spawnPlayer = player.getBukkitEntity(); +- org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); ++ org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new com.destroystokyo.paper.event.player.PlayerInitialSpawnEvent(spawnPlayer, spawnPlayer.getLocation()); // Paper use our duplicate event + this.cserver.getPluginManager().callEvent(ev); + + Location loc = ev.getSpawnLocation(); +@@ -234,7 +234,10 @@ public abstract class PlayerList { + + player.spawnIn(worldserver1); + player.gameMode.setLevel((ServerLevel) player.level()); +- player.absMoveTo(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch()); ++ // Paper start - set raw so we aren't fully joined to the world (not added to chunk or world) ++ player.setPosRaw(loc.getX(), loc.getY(), loc.getZ()); ++ player.setRot(loc.getYaw(), loc.getPitch()); ++ // Paper end - set raw so we aren't fully joined to the world + // Spigot end + + // CraftBukkit - Moved message to after join diff --git a/patches/server/0055-Configurable-Disabling-Cat-Chest-Detection.patch b/patches/server/0055-Configurable-Disabling-Cat-Chest-Detection.patch new file mode 100644 index 0000000000..232eb8f5f4 --- /dev/null +++ b/patches/server/0055-Configurable-Disabling-Cat-Chest-Detection.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Thu, 3 Mar 2016 01:13:45 -0600 +Subject: [PATCH] Configurable Disabling Cat Chest Detection + +Offers a gameplay feature to stop cats from blocking chests + +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 e197891f61580f92787d9400ff486439a92a54c7..8fbfd18b3caeed769396b3ffb1b1778b2f38edc0 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +@@ -349,6 +349,11 @@ public class ChestBlock extends AbstractChestBlock<ChestBlockEntity> implements + } + + private static boolean isCatSittingOnChest(LevelAccessor world, BlockPos pos) { ++ // Paper start - Option to disable chest cat detection ++ if (world.getMinecraftWorld().paperConfig().entities.behavior.disableChestCatDetection) { ++ return false; ++ } ++ // Paper end - Option to disable chest cat detection + List<Cat> list = world.getEntitiesOfClass(Cat.class, new AABB((double) pos.getX(), (double) (pos.getY() + 1), (double) pos.getZ(), (double) (pos.getX() + 1), (double) (pos.getY() + 2), (double) (pos.getZ() + 1))); + + if (!list.isEmpty()) { diff --git a/patches/server/0056-Improve-Player-chat-API-handling.patch b/patches/server/0056-Improve-Player-chat-API-handling.patch new file mode 100644 index 0000000000..ddf0a3129b --- /dev/null +++ b/patches/server/0056-Improve-Player-chat-API-handling.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <[email protected]> +Date: Thu, 3 Mar 2016 01:17:12 -0600 +Subject: [PATCH] Improve Player chat API handling + +Properly split up the chat and command handling to reflect the server now +having separate packets for both, and the client always using the correct packet. Text +from a chat packet should never be parsed into a command, even if it starts with the `/` +character. + +Add a missing async catcher and improve Spigot's async catcher error message. + +== AT == +public net.minecraft.server.network.ServerGamePacketListenerImpl isChatMessageIllegal(Ljava/lang/String;)Z + +Co-authored-by: Jake Potrebic <[email protected]> +Co-authored-by: SoSeDiK <[email protected]> + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 4f50e2f5de529813c269b7670a47e06306575b60..f9050a1d255780d5131200e8b31bd91154907af6 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2013,7 +2013,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + OutgoingChatMessage outgoing = OutgoingChatMessage.create(original); + +- if (!async && s.startsWith("/")) { ++ if (false && !async && s.startsWith("/")) { // Paper - Don't handle commands in chat logic + this.handleCommand(s); + } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { + // Do nothing, this is coming from a plugin +@@ -2100,7 +2100,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + +- private void handleCommand(String s) { ++ public void handleCommand(String s) { // Paper - private -> public ++ org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + s); // Paper - Add async catcher + co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper + if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot + this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 7c97ec4aa57562a8383a40e493eaa8a3697208bb..78193f0d66c2755ed238824bcd24ced9f9052188 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -925,7 +925,7 @@ public final class CraftServer implements Server { + public boolean dispatchCommand(CommandSender sender, String commandLine) { + Preconditions.checkArgument(sender != null, "sender cannot be null"); + Preconditions.checkArgument(commandLine != null, "commandLine cannot be null"); +- org.spigotmc.AsyncCatcher.catchOp("command dispatch"); // Spigot ++ org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + commandLine); // Spigot // Paper - Include command in error message + + if (this.commandMap.dispatch(sender, commandLine)) { + return true; +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index e42b928d7019b636d4fd092d4a222208a7787bcb..783db0dc65725c49ffc4af0f2ca7357ab051d12c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -546,7 +546,20 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + if (this.getHandle().connection == null) return; + +- this.getHandle().connection.chat(msg, PlayerChatMessage.system(msg), false); ++ // Paper start - Improve chat handling ++ if (ServerGamePacketListenerImpl.isChatMessageIllegal(msg)) { ++ this.getHandle().connection.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters")); ++ } else { ++ if (msg.startsWith("/")) { ++ this.getHandle().connection.handleCommand(msg); ++ } else { ++ final PlayerChatMessage playerChatMessage = PlayerChatMessage.system(msg).withUnsignedContent(Component.literal(msg)); ++ // TODO chat decorating ++ // TODO text filtering ++ this.getHandle().connection.chat(msg, playerChatMessage, false); ++ } ++ } ++ // Paper end - Improve chat handling + } + + @Override diff --git a/patches/server/0057-All-chunks-are-slime-spawn-chunks-toggle.patch b/patches/server/0057-All-chunks-are-slime-spawn-chunks-toggle.patch new file mode 100644 index 0000000000..3cded231dd --- /dev/null +++ b/patches/server/0057-All-chunks-are-slime-spawn-chunks-toggle.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: vemacs <[email protected]> +Date: Thu, 3 Mar 2016 01:19:22 -0600 +Subject: [PATCH] All chunks are slime spawn chunks toggle + + +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 b7e6f6e195d8b947c06e2bf58f4c644bda8eba99..b1f7ea02e533660322675e1bddb070f0a41084f2 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Slime.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Slime.java +@@ -350,7 +350,7 @@ public class Slime extends Mob implements Enemy { + } + + ChunkPos chunkcoordintpair = new ChunkPos(pos); +- boolean flag = WorldgenRandom.seedSlimeChunk(chunkcoordintpair.x, chunkcoordintpair.z, ((WorldGenLevel) world).getSeed(), world.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Spigot ++ boolean flag = world.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(chunkcoordintpair.x, chunkcoordintpair.z, ((WorldGenLevel) world).getSeed(), world.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Spigot // Paper + + if (random.nextInt(10) == 0 && flag && pos.getY() < 40) { + return checkMobSpawnRules(type, world, spawnReason, pos, random); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 5db1be69f0c4ef6976789587866c7a9bb6d19b0d..e37dae711e7059834612ead5f4fcea9f28ad436f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -218,7 +218,7 @@ public class CraftChunk implements Chunk { + @Override + public boolean isSlimeChunk() { + // 987234911L is deterimined in EntitySlime when seeing if a slime can spawn in a chunk +- return WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), this.worldServer.spigotConfig.slimeSeed).nextInt(10) == 0; ++ return this.worldServer.paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), worldServer.spigotConfig.slimeSeed).nextInt(10) == 0; // Paper + } + + @Override diff --git a/patches/server/0058-Expose-server-CommandMap.patch b/patches/server/0058-Expose-server-CommandMap.patch new file mode 100644 index 0000000000..7ef3e6c199 --- /dev/null +++ b/patches/server/0058-Expose-server-CommandMap.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kashike <[email protected]> +Date: Thu, 3 Mar 2016 02:15:57 -0600 +Subject: [PATCH] Expose server CommandMap + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 78193f0d66c2755ed238824bcd24ced9f9052188..4bfb836513d5194be271f4a82990ace98de69640 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2169,6 +2169,7 @@ public final class CraftServer implements Server { + return this.helpMap; + } + ++ @Override // Paper - add override + public SimpleCommandMap getCommandMap() { + return this.commandMap; + } diff --git a/patches/server/0059-Be-a-bit-more-informative-in-maxHealth-exception.patch b/patches/server/0059-Be-a-bit-more-informative-in-maxHealth-exception.patch new file mode 100644 index 0000000000..489c881a1a --- /dev/null +++ b/patches/server/0059-Be-a-bit-more-informative-in-maxHealth-exception.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kashike <[email protected]> +Date: Thu, 3 Mar 2016 02:18:39 -0600 +Subject: [PATCH] Be a bit more informative in maxHealth exception + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +index adeb3b9a3d65d2ef71ea4fd6f0f4679a88d0ba87..e6c68fd9a5fcd374cd7feca081189c9ba9225743 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +@@ -102,7 +102,12 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + @Override + public void setHealth(double health) { + health = (float) health; +- Preconditions.checkArgument(health >= 0 && health <= this.getMaxHealth(), "Health value (%s) must be between 0 and %s", health, this.getMaxHealth()); ++ // Paper start - Be more informative ++ Preconditions.checkArgument(health >= 0 && health <= this.getMaxHealth(), ++ "Health value (%s) must be between 0 and %s. (attribute base value: %s%s)", ++ health, this.getMaxHealth(), this.getHandle().getAttribute(Attributes.MAX_HEALTH).getBaseValue(), this instanceof CraftPlayer ? ", player: " + this.getName() : "" ++ ); ++ // Paper end + + // during world generation, we don't want to run logic for dropping items and xp + if (this.getHandle().generation && health == 0) { diff --git a/patches/server/0060-Player-Tab-List-and-Title-APIs.patch b/patches/server/0060-Player-Tab-List-and-Title-APIs.patch new file mode 100644 index 0000000000..dc13ad83d8 --- /dev/null +++ b/patches/server/0060-Player-Tab-List-and-Title-APIs.patch @@ -0,0 +1,109 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Techcable <[email protected]> +Date: Thu, 3 Mar 2016 02:32:10 -0600 +Subject: [PATCH] Player Tab List and Title APIs + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 783db0dc65725c49ffc4af0f2ca7357ab051d12c..ecb17cffbec7d2630602b265660f7b9b3f317906 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -386,6 +386,98 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + } + ++ // Paper start ++ @Override ++ public void setPlayerListHeaderFooter(BaseComponent[] header, BaseComponent[] footer) { ++ if (header != null) { ++ String headerJson = net.md_5.bungee.chat.ComponentSerializer.toString(header); ++ playerListHeader = net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize(headerJson); ++ } else { ++ playerListHeader = null; ++ } ++ ++ if (footer != null) { ++ String footerJson = net.md_5.bungee.chat.ComponentSerializer.toString(footer); ++ playerListFooter = net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize(footerJson); ++ } else { ++ playerListFooter = null; ++ } ++ ++ updatePlayerListHeaderFooter(); ++ } ++ ++ @Override ++ public void setPlayerListHeaderFooter(BaseComponent header, BaseComponent footer) { ++ this.setPlayerListHeaderFooter(header == null ? null : new BaseComponent[]{header}, ++ footer == null ? null : new BaseComponent[]{footer}); ++ } ++ ++ ++ @Override ++ public void setTitleTimes(int fadeInTicks, int stayTicks, int fadeOutTicks) { ++ getHandle().connection.send(new ClientboundSetTitlesAnimationPacket(fadeInTicks, stayTicks, fadeOutTicks)); ++ } ++ ++ @Override ++ public void setSubtitle(BaseComponent[] subtitle) { ++ final ClientboundSetSubtitleTextPacket packet = new ClientboundSetSubtitleTextPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(subtitle))); ++ getHandle().connection.send(packet); ++ } ++ ++ @Override ++ public void setSubtitle(BaseComponent subtitle) { ++ setSubtitle(new BaseComponent[]{subtitle}); ++ } ++ ++ @Override ++ public void showTitle(BaseComponent[] title) { ++ final ClientboundSetTitleTextPacket packet = new ClientboundSetTitleTextPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(title))); ++ getHandle().connection.send(packet); ++ } ++ ++ @Override ++ public void showTitle(BaseComponent title) { ++ showTitle(new BaseComponent[]{title}); ++ } ++ ++ @Override ++ public void showTitle(BaseComponent[] title, BaseComponent[] subtitle, int fadeInTicks, int stayTicks, int fadeOutTicks) { ++ setTitleTimes(fadeInTicks, stayTicks, fadeOutTicks); ++ setSubtitle(subtitle); ++ showTitle(title); ++ } ++ ++ @Override ++ public void showTitle(BaseComponent title, BaseComponent subtitle, int fadeInTicks, int stayTicks, int fadeOutTicks) { ++ setTitleTimes(fadeInTicks, stayTicks, fadeOutTicks); ++ setSubtitle(subtitle); ++ showTitle(title); ++ } ++ ++ @Override ++ public void sendTitle(com.destroystokyo.paper.Title title) { ++ Preconditions.checkNotNull(title, "Title is null"); ++ setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut()); ++ setSubtitle(title.getSubtitle() == null ? new BaseComponent[0] : title.getSubtitle()); ++ showTitle(title.getTitle()); ++ } ++ ++ @Override ++ public void updateTitle(com.destroystokyo.paper.Title title) { ++ Preconditions.checkNotNull(title, "Title is null"); ++ setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut()); ++ if (title.getSubtitle() != null) { ++ setSubtitle(title.getSubtitle()); ++ } ++ showTitle(title.getTitle()); ++ } ++ ++ @Override ++ public void hideTitle() { ++ getHandle().connection.send(new ClientboundClearTitlesPacket(false)); ++ } ++ // Paper end ++ + @Override + public String getDisplayName() { + if(true) return io.papermc.paper.adventure.DisplayNames.getLegacy(this); // Paper diff --git a/patches/server/0061-Add-configurable-portal-search-radius.patch b/patches/server/0061-Add-configurable-portal-search-radius.patch new file mode 100644 index 0000000000..4a5099223d --- /dev/null +++ b/patches/server/0061-Add-configurable-portal-search-radius.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Joseph Hirschfeld <[email protected]> +Date: Thu, 3 Mar 2016 02:46:17 -0600 +Subject: [PATCH] Add configurable portal search radius + + +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 462afb22cce2376789e44283032e63a6264cf851..19c813ab9e71eed150ae569f903287e9283d9292 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -139,8 +139,14 @@ public class NetherPortalBlock extends Block implements Portal { + WorldBorder worldborder = worldserver1.getWorldBorder(); + double d0 = DimensionType.getTeleportationScale(world.dimensionType(), worldserver1.dimensionType()); + BlockPos blockposition1 = worldborder.clampToBounds(entity.getX() * d0, entity.getY(), entity.getZ() * d0); ++ // Paper start - Configurable portal search radius ++ int portalSearchRadius = worldserver1.paperConfig().environment.portalSearchRadius; ++ if (entity.level().paperConfig().environment.portalSearchVanillaDimensionScaling && flag) { // flag = is going to nether ++ portalSearchRadius = (int) (portalSearchRadius / worldserver1.dimensionType().coordinateScale()); ++ } ++ // Paper end - Configurable portal search radius + // CraftBukkit start +- CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(blockposition1, worldserver1.getWorld()), PlayerTeleportEvent.TeleportCause.NETHER_PORTAL, flag ? 16 : 128, 16); ++ CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(blockposition1, worldserver1.getWorld()), PlayerTeleportEvent.TeleportCause.NETHER_PORTAL, portalSearchRadius, 16); // Paper - use custom portal search radius + if (event == null) { + return null; + } +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index aef1dd28da7e0c0a13a0a7a5b52daa27635c48ea..5c4b2a33d4007c36aef68604bca40a4eba510b4e 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -42,6 +42,7 @@ public class PortalForcer { + this.level = world; + } + ++ @io.papermc.paper.annotation.DoNotUse // Paper + public Optional<BlockPos> findClosestPortalPosition(BlockPos pos, boolean destIsNether, WorldBorder worldBorder) { + // CraftBukkit start + return this.findClosestPortalPosition(pos, worldBorder, destIsNether ? 16 : 128); // Search Radius diff --git a/patches/server/0062-Add-velocity-warnings.patch b/patches/server/0062-Add-velocity-warnings.patch new file mode 100644 index 0000000000..996e71c322 --- /dev/null +++ b/patches/server/0062-Add-velocity-warnings.patch @@ -0,0 +1,85 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Joseph Hirschfeld <[email protected]> +Date: Thu, 3 Mar 2016 02:48:12 -0600 +Subject: [PATCH] Add velocity warnings + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 4bfb836513d5194be271f4a82990ace98de69640..fd31d0e76d1a953b128e777b1bc27e24b1e03ed7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -302,6 +302,7 @@ public final class CraftServer implements Server { + private final List<CraftPlayer> playerView; + public int reloadCount; + public Set<String> activeCompatibilities = Collections.emptySet(); ++ public static Exception excessiveVelEx; // Paper - Velocity warnings + + static { + ConfigurationSerialization.registerClass(CraftOfflinePlayer.class); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 7b45a1216ff824f1b528bb5759d10b70858832a1..df6da730134da754d0ff23bd1b57c82486b9ab73 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -131,10 +131,40 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + public void setVelocity(Vector velocity) { + Preconditions.checkArgument(velocity != null, "velocity"); + velocity.checkFinite(); ++ // Paper start - Warn server owners when plugins try to set super high velocities ++ if (!(this instanceof org.bukkit.entity.Projectile || this instanceof org.bukkit.entity.Minecart) && isUnsafeVelocity(velocity)) { ++ CraftServer.excessiveVelEx = new Exception("Excessive velocity set detected: tried to set velocity of entity " + entity.getScoreboardName() + " id #" + getEntityId() + " to (" + velocity.getX() + "," + velocity.getY() + "," + velocity.getZ() + ")."); ++ } ++ // Paper end + this.entity.setDeltaMovement(CraftVector.toNMS(velocity)); + this.entity.hurtMarked = true; + } + ++ // Paper start ++ /** ++ * Checks if the given velocity is not necessarily safe in all situations. ++ * This function returning true does not mean the velocity is dangerous or to be avoided, only that it may be ++ * a detriment to performance on the server. ++ * ++ * It is not to be used as a hard rule of any sort. ++ * Paper only uses it to warn server owners in watchdog crashes. ++ * ++ * @param vel incoming velocity to check ++ * @return if the velocity has the potential to be a performance detriment ++ */ ++ private static boolean isUnsafeVelocity(Vector vel) { ++ final double x = vel.getX(); ++ final double y = vel.getY(); ++ final double z = vel.getZ(); ++ ++ if (x > 4 || x < -4 || y > 4 || y < -4 || z > 4 || z < -4) { ++ return true; ++ } ++ ++ return false; ++ } ++ // Paper end ++ + @Override + public double getHeight() { + return this.getHandle().getBbHeight(); +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index e086765dec32241bc5a77afcf072c77a40c6d785..35c90aaee30980610d168c01cbe9abfe04331cb8 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -81,6 +81,17 @@ public class WatchdogThread extends Thread + log.log( Level.SEVERE, "near " + net.minecraft.world.level.Level.lastPhysicsProblem ); + } + // ++ // Paper start - Warn in watchdog if an excessive velocity was ever set ++ if (org.bukkit.craftbukkit.CraftServer.excessiveVelEx != null) { ++ log.log(Level.SEVERE, "------------------------------"); ++ log.log(Level.SEVERE, "During the run of the server, a plugin set an excessive velocity on an entity"); ++ log.log(Level.SEVERE, "This may be the cause of the issue, or it may be entirely unrelated"); ++ log.log(Level.SEVERE, org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getMessage()); ++ for (StackTraceElement stack : org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getStackTrace()) { ++ log.log( Level.SEVERE, "\t\t" + stack ); ++ } ++ } ++ // Paper end + log.log( Level.SEVERE, "------------------------------" ); + log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); |