diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 250a0d838..0a12f95aa 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,15 +6,17 @@ development, so keep changes focused and preserve existing behavior unless a bre ## Building ProtocolLib -Use the Gradle wrapper from the repository root. The build uses the Java 25 toolchain and resolves the current Spigot -server dependency from the CodeMC repository. +Use the Gradle wrapper from the repository root. The root project builds the platform-neutral library published to +Maven. The `paper` module compiles and tests it against Paper's development bundle and produces the primary plugin, +`build/libs/ProtocolLib.jar`. The `spigot` compatibility module produces `build/libs/ProtocolLib-Spigot.jar`. ```shell ./gradlew test ./gradlew build shadowJar ``` -`shadowJar` produces `build/libs/ProtocolLib.jar` with the required Byte Buddy classes included. +`shadowJar` builds both plugin distributions with the required Byte Buddy classes included. Neither platform provider +nor plugin descriptor is included in the Maven core artifact. ProtocolLib uses the Java 25 toolchain to compile against the current server, but production classes target Java 17 for compatibility with older supported servers. @@ -26,7 +28,7 @@ server classes. The server packet classes and codecs are authoritative when docu ### 1. Update version metadata -- Update `mcVersion` in `build.gradle.kts`. +- Update `mcVersion` in `gradle.properties`. - Add the release to `MinecraftVersion` and update `MinecraftVersion.LATEST`. - Update `ProtocolLibrary.MAXIMUM_MINECRAFT_VERSION` and `ProtocolLibrary.MINECRAFT_LAST_RELEASE_DATE`. - Update `MinecraftProtocolVersion` if the release changed the protocol number. @@ -57,7 +59,7 @@ serverbound and clientbound packet classes and codecs against every protocol pha ### 4. Add regression coverage - Initialize the test environment with `BukkitInitialization` when tests access CraftBukkit or NMS classes. -- Exercise the actual packet structure supplied by the current Spigot dependency. +- Exercise the actual packet structure supplied by the current Paper dependency. - Cover both sides of a version guard when feasible. - Re-enable previously disabled tests when the underlying incompatibility is fixed. - Run targeted tests while developing, followed by `./gradlew test`. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 629cc9bd6..252fdac84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' + java-version: '25' cache: 'gradle' - name: Run gradle build lifecycle @@ -27,4 +27,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: ProtocolLib - path: build/libs/ProtocolLib.jar + path: | + build/libs/ProtocolLib.jar + build/libs/ProtocolLib-Spigot.jar diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 3d8454896..7ccabe5f6 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -16,7 +16,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' + java-version: '25' cache: 'gradle' - name: Run gradle build lifecycle @@ -27,7 +27,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: ProtocolLib - path: build/libs/ProtocolLib.jar + path: | + build/libs/ProtocolLib.jar + build/libs/ProtocolLib-Spigot.jar - name: Create or update GitHub pre-release env: @@ -41,3 +43,4 @@ jobs: NEW_NOTES="$RELEASE_NOTES"$'\n'"* [$SHORT_HASH] $COMMIT_MSG" gh release edit dev-build --notes "$NEW_NOTES" --repo ${{ github.repository }} gh release upload dev-build build/libs/ProtocolLib.jar#ProtocolLib.jar --clobber --repo ${{ github.repository }} + gh release upload dev-build build/libs/ProtocolLib-Spigot.jar#ProtocolLib-Spigot.jar --clobber --repo ${{ github.repository }} diff --git a/Readme.md b/Readme.md index 3cb27f231..4bfe8695a 100644 --- a/Readme.md +++ b/Readme.md @@ -18,12 +18,15 @@ lead to more subtle bugs. * [JavaDoc](https://dmulloy2.net/ProtocolLib/javadoc/) * [Protocol Wiki](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol) +`ProtocolLib.jar` is the primary Paper plugin. Spigot servers must use the compatibility artifact, +`ProtocolLib-Spigot.jar`. The Maven artifact is the platform-neutral library shared by both distributions. + ### Compilation ProtocolLib is built with [Gradle](https://gradle.org/). If you have it installed, just run `./gradlew build` in the root project folder. Other gradle targets you may be interested in include `clean`, `test`, and `shadowJar`. `shadowJar` will create a jar with all dependencies -(ByteBuddy) included. +(ByteBuddy) included for both Paper and Spigot. ### A new API diff --git a/build.gradle.kts b/build.gradle.kts index d5dbeaf89..686c4ade1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,7 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat - plugins { `java-library` `maven-publish` signing - id("com.gradleup.shadow") version "9.4.0" id("io.github.patrick.remapper") version "1.4.3" id("com.vanniktech.maven.publish") version "0.36.0" } @@ -12,7 +9,7 @@ plugins { group = "net.dmulloy2" description = "Provides access to the Minecraft protocol" -val mcVersion = "26.2" +val mcVersion: String by project val isSnapshot = version.toString().endsWith("-SNAPSHOT") val isJitPack = System.getenv("JITPACK")?.equals("true", ignoreCase = true) ?: false val commitHash = System.getenv("COMMIT_SHA") ?: "" @@ -46,20 +43,11 @@ repositories { dependencies { implementation("net.bytebuddy:byte-buddy:1.18.2") compileOnly("org.spigotmc:spigot-api:${mcVersion}-R0.1-SNAPSHOT") - compileOnly("org.spigotmc:spigot:${mcVersion}-R0.1-SNAPSHOT")//:remapped-mojang") + compileOnly("org.spigotmc:spigot:${mcVersion}-R0.1-SNAPSHOT") compileOnly("io.netty:netty-all:4.2.8.Final") compileOnly("net.kyori:adventure-text-serializer-gson:4.25.0") compileOnly("com.googlecode.json-simple:json-simple:1.1.1") - - testImplementation("org.junit.jupiter:junit-jupiter-engine:6.0.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1") - testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.1") - testImplementation("org.mockito:mockito-core:5.21.0") - testImplementation("io.netty:netty-common:4.2.8.Final") - testImplementation("io.netty:netty-transport:4.2.8.Final") - testImplementation("org.spigotmc:spigot:${mcVersion}-R0.1-SNAPSHOT")//:remapped-mojang") - testImplementation("net.kyori:adventure-text-serializer-gson:4.25.0") - testImplementation("net.kyori:adventure-text-serializer-plain:4.25.0") + compileOnly("commons-lang:commons-lang:2.6") } java { @@ -68,49 +56,22 @@ java { } } -tasks { - processResources { - val fullVersion = if (isSnapshot && isCI) "${version}-${commitHash.take(7)}" else version - - eachFile { - expand("version" to fullVersion) - } - } - +sourceSets { test { - useJUnitPlatform() - testLogging { - exceptionFormat = TestExceptionFormat.FULL - } + java.setSrcDirs(emptyList()) + resources.setSrcDirs(emptyList()) } +} - shadowJar { - dependencies { - include(dependency("net.bytebuddy:byte-buddy:.*")) - } - relocate("net.bytebuddy", "com.comphenix.net.bytebuddy") - - manifest { - attributes( - "paperweight-mappings-namespace" to "mojang" - ) - } - - archiveFileName = "ProtocolLib.jar" +tasks { + test { + dependsOn(":paper:test") } - /*remap { - dependsOn("shadowJar") - - inputTask.set(getByName("shadowJar")) - version.set(mcVersion) - action.set(RemapTask.Action.MOJANG_TO_SPIGOT) + check { + dependsOn(":paper:check", ":spigot:check") } - assemble { - dependsOn("remap") - }*/ - javadoc { options.encoding = "UTF-8" } @@ -121,6 +82,12 @@ tasks { } } +tasks.register("shadowJar") { + group = "build" + description = "Builds the Paper and Spigot plugin distributions." + dependsOn(":paper:shadowJar", ":spigot:shadowJar") +} + mavenPublishing { publishToMavenCentral() if (!isSnapshot && !isJitPack) { diff --git a/gradle.properties b/gradle.properties index 20c8d5122..997b75946 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ version=5.5.0-SNAPSHOT +mcVersion=26.2 mavenCentralUsername= mavenCentralPassword= signing.keyId= diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts new file mode 100644 index 000000000..90e7cb2ba --- /dev/null +++ b/paper/build.gradle.kts @@ -0,0 +1,93 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + +plugins { + `java-library` + id("com.gradleup.shadow") version "9.4.0" + id("io.papermc.paperweight.userdev") version "2.0.0-beta.21" +} + +group = "net.dmulloy2" +version = rootProject.version + +val mcVersion: String by project +val isSnapshot = version.toString().endsWith("-SNAPSHOT") +val commitHash = System.getenv("COMMIT_SHA") ?: "" +val isCI = commitHash.isNotEmpty() + +repositories { + mavenLocal() + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.codemc.io/repository/nms/") + maven("https://hub.spigotmc.org/nexus/content/groups/public/") + maven("https://libraries.minecraft.net/") { + metadataSources { + mavenPom() + artifact() + ignoreGradleMetadataRedirection() + } + } +} + +dependencies { + implementation(project(":")) + paperweight.paperDevBundle("$mcVersion.build.+") + + compileOnly("io.netty:netty-all:4.2.8.Final") + compileOnly("net.kyori:adventure-text-serializer-gson:4.25.0") + compileOnly("commons-lang:commons-lang:2.6") + + testImplementation("org.junit.jupiter:junit-jupiter-engine:6.0.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.1") + testImplementation("org.mockito:mockito-core:5.21.0") + testImplementation("io.netty:netty-common:4.2.8.Final") + testImplementation("io.netty:netty-transport:4.2.8.Final") + testImplementation("net.kyori:adventure-text-serializer-gson:4.25.0") + testImplementation("net.kyori:adventure-text-serializer-plain:4.25.0") + testImplementation("commons-lang:commons-lang:2.6") +} + +paperweight.reobfArtifactConfiguration = + io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + +sourceSets { + test { + java.srcDir(rootProject.file("src/test/java")) + resources.srcDir(rootProject.file("src/test/resources")) + } +} + +tasks { + processResources { + val fullVersion = if (isSnapshot && isCI) "${version}-${commitHash.take(7)}" else version + + eachFile { + expand("version" to fullVersion) + } + } + + test { + useJUnitPlatform() + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } + } + + shadowJar { + relocate("net.bytebuddy", "com.comphenix.net.bytebuddy") + + manifest { + attributes("paperweight-mappings-namespace" to "mojang") + } + + destinationDirectory.set(rootProject.layout.buildDirectory.dir("libs")) + archiveFileName.set("ProtocolLib.jar") + } +} diff --git a/paper/src/main/java/com/comphenix/protocol/paper/PaperPlatformProvider.java b/paper/src/main/java/com/comphenix/protocol/paper/PaperPlatformProvider.java new file mode 100644 index 000000000..403bf5a55 --- /dev/null +++ b/paper/src/main/java/com/comphenix/protocol/paper/PaperPlatformProvider.java @@ -0,0 +1,105 @@ +package com.comphenix.protocol.paper; + +import java.util.List; +import java.util.function.Consumer; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +import com.comphenix.protocol.ProtocolLib; +import com.comphenix.protocol.internal.PlatformProvider; + +import io.netty.channel.Channel; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.network.ChannelInitializeListenerHolder; +import net.kyori.adventure.key.Key; + +public final class PaperPlatformProvider implements PlatformProvider { + + public static final Key CHANNEL_INITIALIZER_KEY = Key.key("protocollib", "channel-initializer"); + + @Override + public boolean hasEarlyChannelInitialization() { + return true; + } + + @Override + public Runnable registerChannelInitializer(Consumer channelInitializer) { + ChannelInitializeListenerHolder.addListener(CHANNEL_INITIALIZER_KEY, channelInitializer::accept); + return () -> ChannelInitializeListenerHolder.removeListener(CHANNEL_INITIALIZER_KEY); + } + + @Override + public void registerCommand(ProtocolLib plugin, String name, CommandExecutor executor) { + CommandMetadata metadata = getMetadata(name); + Command command = new RegisteredCommand(name, metadata); + + plugin.registerCommand( + name, + metadata.description(), + metadata.aliases(), + new RegisteredBasicCommand(name, metadata, command, executor)); + } + + private static CommandMetadata getMetadata(String name) { + return switch (name) { + case "protocol" -> new CommandMetadata( + "Performs administrative tasks regarding ProtocolLib.", + "/ config|check|update|timings|listeners|version|dump", + "protocol.admin", + List.of()); + case "packet" -> new CommandMetadata( + "Add or remove a simple packet listener.", + "/ add|remove|names client|server [ID start]-[ID stop] [detailed]", + "protocol.admin", + List.of()); + case "filter" -> new CommandMetadata( + "Add or remove programmable filters to the packet listeners.", + "/ add|remove name [ID start]-[ID stop]", + "protocol.admin", + List.of("packet_filter")); + case "packetlog" -> new CommandMetadata( + "Logs hex representations of packets to a file or console", + "/ [location]", + "protocol.admin", + List.of()); + default -> throw new IllegalArgumentException("Unknown ProtocolLib command " + name); + }; + } + + private record CommandMetadata(String description, String usage, String permission, List aliases) { + } + + private record RegisteredBasicCommand( + String name, + CommandMetadata metadata, + Command command, + CommandExecutor executor) implements BasicCommand { + + @Override + public void execute(CommandSourceStack source, String[] arguments) { + if (!this.executor.onCommand(source.getSender(), this.command, this.name, arguments)) { + source.getSender().sendMessage(this.metadata.usage().replace("", this.name)); + } + } + + @Override + public String permission() { + return this.metadata.permission(); + } + } + + private static final class RegisteredCommand extends Command { + + private RegisteredCommand(String name, CommandMetadata metadata) { + super(name, metadata.description(), metadata.usage(), metadata.aliases()); + } + + @Override + public boolean execute(CommandSender sender, String commandLabel, String[] args) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/paper/src/main/resources/META-INF/services/com.comphenix.protocol.internal.PlatformProvider b/paper/src/main/resources/META-INF/services/com.comphenix.protocol.internal.PlatformProvider new file mode 100644 index 000000000..2686f079e --- /dev/null +++ b/paper/src/main/resources/META-INF/services/com.comphenix.protocol.internal.PlatformProvider @@ -0,0 +1 @@ +com.comphenix.protocol.paper.PaperPlatformProvider diff --git a/paper/src/main/resources/paper-plugin.yml b/paper/src/main/resources/paper-plugin.yml new file mode 100644 index 000000000..e5558924f --- /dev/null +++ b/paper/src/main/resources/paper-plugin.yml @@ -0,0 +1,22 @@ +name: ProtocolLib +version: ${version} +description: Provides read/write access to the Minecraft protocol. +authors: [dmulloy2] + +main: com.comphenix.protocol.ProtocolLib +load: STARTUP +api-version: "26.2" +folia-supported: true + +permissions: + protocol.*: + description: Gives access to everything. + children: + protocol.admin: true + protocol.info: true + protocol.admin: + description: Able to initiate the update process and configure debug mode. + default: op + protocol.info: + description: Can read update notifications and error reports. + default: op diff --git a/settings.gradle b/settings.gradle index dba5d8e18..3df8a276b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,3 +3,5 @@ */ rootProject.name = 'ProtocolLib' + +include 'paper', 'spigot' diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts new file mode 100644 index 000000000..067ac2c83 --- /dev/null +++ b/spigot/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + `java-library` + id("com.gradleup.shadow") version "9.4.0" +} + +group = "net.dmulloy2" +version = rootProject.version + +val mcVersion: String by project + +repositories { + mavenLocal() + mavenCentral() + maven("https://repo.codemc.io/repository/nms/") + maven("https://hub.spigotmc.org/nexus/content/groups/public/") + maven("https://libraries.minecraft.net/") { + metadataSources { + mavenPom() + artifact() + ignoreGradleMetadataRedirection() + } + } +} + +dependencies { + implementation(project(":")) + compileOnly("org.spigotmc:spigot-api:${mcVersion}-R0.1-SNAPSHOT") + compileOnly("org.spigotmc:spigot:${mcVersion}-R0.1-SNAPSHOT") + compileOnly("io.netty:netty-all:4.2.8.Final") + compileOnly("net.kyori:adventure-text-serializer-gson:4.25.0") + compileOnly("com.googlecode.json-simple:json-simple:1.1.1") + + testImplementation("org.junit.jupiter:junit-jupiter-engine:6.0.1") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.1") + testImplementation("org.mockito:mockito-core:5.21.0") + testImplementation("io.netty:netty-transport:4.2.8.Final") + testImplementation("org.spigotmc:spigot:${mcVersion}-R0.1-SNAPSHOT") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + +tasks { + processResources { + eachFile { + expand("version" to project.version) + } + } + + test { + useJUnitPlatform() + } + + shadowJar { + relocate("net.bytebuddy", "com.comphenix.net.bytebuddy") + + manifest { + attributes("paperweight-mappings-namespace" to "mojang") + } + + destinationDirectory.set(rootProject.layout.buildDirectory.dir("libs")) + archiveFileName.set("ProtocolLib-Spigot.jar") + } +} diff --git a/spigot/src/main/java/com/comphenix/protocol/spigot/SpigotPlatformProvider.java b/spigot/src/main/java/com/comphenix/protocol/spigot/SpigotPlatformProvider.java new file mode 100644 index 000000000..bbab409f0 --- /dev/null +++ b/spigot/src/main/java/com/comphenix/protocol/spigot/SpigotPlatformProvider.java @@ -0,0 +1,34 @@ +package com.comphenix.protocol.spigot; + +import java.util.function.Consumer; + +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.PluginCommand; + +import com.comphenix.protocol.ProtocolLib; +import com.comphenix.protocol.internal.PlatformProvider; + +import io.netty.channel.Channel; + +public final class SpigotPlatformProvider implements PlatformProvider { + + @Override + public boolean hasEarlyChannelInitialization() { + return false; + } + + @Override + public Runnable registerChannelInitializer(Consumer channelInitializer) { + throw new UnsupportedOperationException("Early channel initialization is unavailable"); + } + + @Override + public void registerCommand(ProtocolLib plugin, String name, CommandExecutor executor) { + PluginCommand command = plugin.getCommand(name); + if (command == null) { + throw new IllegalStateException("plugin.yml might be corrupt."); + } + + command.setExecutor(executor); + } +} diff --git a/spigot/src/main/resources/META-INF/services/com.comphenix.protocol.internal.PlatformProvider b/spigot/src/main/resources/META-INF/services/com.comphenix.protocol.internal.PlatformProvider new file mode 100644 index 000000000..f7835f2a1 --- /dev/null +++ b/spigot/src/main/resources/META-INF/services/com.comphenix.protocol.internal.PlatformProvider @@ -0,0 +1 @@ +com.comphenix.protocol.spigot.SpigotPlatformProvider diff --git a/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml similarity index 97% rename from src/main/resources/plugin.yml rename to spigot/src/main/resources/plugin.yml index f499d3746..5150361a4 100644 --- a/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: ProtocolLib version: ${version} description: Provides read/write access to the Minecraft protocol. -authors: [dmulloy2, comphenix] +authors: [dmulloy2] main: com.comphenix.protocol.ProtocolLib load: STARTUP @@ -44,4 +44,4 @@ permissions: default: op protocol.info: description: Can read update notifications and error reports. - default: op \ No newline at end of file + default: op diff --git a/spigot/src/test/java/com/comphenix/protocol/PlatformCommandRegistrarTest.java b/spigot/src/test/java/com/comphenix/protocol/PlatformCommandRegistrarTest.java new file mode 100644 index 000000000..8fed60d4f --- /dev/null +++ b/spigot/src/test/java/com/comphenix/protocol/PlatformCommandRegistrarTest.java @@ -0,0 +1,26 @@ +package com.comphenix.protocol; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.PluginCommand; +import org.junit.jupiter.api.Test; + +import com.comphenix.protocol.spigot.SpigotPlatformProvider; + +class PlatformCommandRegistrarTest { + + @Test + void registersLegacyExecutorFromPluginDescriptor() { + ProtocolLib plugin = mock(ProtocolLib.class); + CommandExecutor executor = mock(CommandExecutor.class); + PluginCommand command = mock(PluginCommand.class); + when(plugin.getCommand("protocol")).thenReturn(command); + + new SpigotPlatformProvider().registerCommand(plugin, "protocol", executor); + + verify(command).setExecutor(executor); + } +} diff --git a/spigot/src/test/java/com/comphenix/protocol/injector/netty/manager/PlatformChannelInitializerTest.java b/spigot/src/test/java/com/comphenix/protocol/injector/netty/manager/PlatformChannelInitializerTest.java new file mode 100644 index 000000000..eec3e4d83 --- /dev/null +++ b/spigot/src/test/java/com/comphenix/protocol/injector/netty/manager/PlatformChannelInitializerTest.java @@ -0,0 +1,17 @@ +package com.comphenix.protocol.injector.netty.manager; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import org.junit.jupiter.api.Test; + +import com.comphenix.protocol.internal.PlatformProvider; +class PlatformChannelInitializerTest { + + @Test + void reportsPaperInitializerUnavailable() { + PlatformProvider provider = PlatformProvider.get(); + assertInstanceOf(com.comphenix.protocol.spigot.SpigotPlatformProvider.class, provider); + assertFalse(provider.hasEarlyChannelInitialization()); + } +} diff --git a/src/main/java/com/comphenix/protocol/ProtocolLib.java b/src/main/java/com/comphenix/protocol/ProtocolLib.java index f352d8fce..597207f95 100644 --- a/src/main/java/com/comphenix/protocol/ProtocolLib.java +++ b/src/main/java/com/comphenix/protocol/ProtocolLib.java @@ -49,10 +49,11 @@ import org.bukkit.Server; import org.bukkit.command.CommandExecutor; -import org.bukkit.command.PluginCommand; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import com.comphenix.protocol.internal.PlatformProvider; + /** * The main entry point for ProtocolLib. * @@ -444,14 +445,7 @@ private void registerCommand(String name, CommandExecutor executor) { return; } - PluginCommand command = this.getCommand(name); - - // Try to load the command - if (command != null) { - command.setExecutor(executor); - } else { - throw new RuntimeException("plugin.yml might be corrupt."); - } + PlatformProvider.get().registerCommand(this, name, executor); } catch (RuntimeException e) { reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_REGISTER_COMMAND).messageParam(name, e.getMessage()).error(e)); diff --git a/src/main/java/com/comphenix/protocol/events/PacketEvent.java b/src/main/java/com/comphenix/protocol/events/PacketEvent.java index df3f3c208..57744cb54 100644 --- a/src/main/java/com/comphenix/protocol/events/PacketEvent.java +++ b/src/main/java/com/comphenix/protocol/events/PacketEvent.java @@ -391,6 +391,8 @@ public Player getPlayer() { *
  • getPlayer
  • *
  • getAddress
  • *
  • getServer
  • + *
  • getName, after the login start packet
  • + *
  • getUniqueId, when supplied by the login protocol
  • *
  • chat
  • *
  • sendMessage
  • *
  • kickPlayer
  • diff --git a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java index 722ea5428..f3fbf0eb0 100644 --- a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java +++ b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java @@ -22,8 +22,10 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.time.Duration; import java.time.Instant; import java.util.Date; @@ -68,7 +70,7 @@ * * @author Kristian */ -class SerializedOfflinePlayer implements OfflinePlayer, Serializable { +class SerializedOfflinePlayer implements Serializable { /** * Generated by Eclipse. @@ -122,22 +124,18 @@ public SerializedOfflinePlayer(OfflinePlayer offline) { } } - @Override public boolean isOp() { return operator; } - @Override public void setOp(boolean operator) { this.operator = operator; } - @Override public Map serialize() { throw new UnsupportedOperationException(); } - @Override public Location getBedSpawnLocation() { return bedSpawnLocation; } @@ -152,12 +150,10 @@ public long getLastSeen() { return lastSeen; } - @Override public Location getRespawnLocation() { return null; } - @Override public Location getLocation() { return null; } @@ -221,42 +217,34 @@ public void decrementStatistic(Statistic statistic, EntityType entityType, int i public void setStatistic(Statistic statistic, EntityType entityType, int i) { } - @Override public Location getLastDeathLocation() { return null; } - @Override public long getFirstPlayed() { return firstPlayed; } - @Override public long getLastPlayed() { return lastPlayed; } - @Override public UUID getUniqueId() { return uuid; } - @Override public @NotNull PlayerProfile getPlayerProfile() { return null; } - @Override public String getName() { return name; } - @Override public boolean hasPlayedBefore() { return playedBefore; } - @Override public boolean isBanned() { return banned; } @@ -283,17 +271,14 @@ public void setBanned(boolean banned) { this.banned = banned; } - @Override public boolean isOnline() { return online; } - @Override public boolean isWhitelisted() { return whitelisted; } - @Override public void setWhitelisted(boolean whitelisted) { this.whitelisted = whitelisted; } @@ -330,7 +315,6 @@ private World getWorld(String name) { } } - @Override public Player getPlayer() { try { // Try to get the real player underneath @@ -349,7 +333,7 @@ public Player getPlayer() { */ public Player getProxyPlayer() { try { - return (Player) proxyPlayerConstructor.newInstance(this); + return (Player) proxyPlayerConstructor.newInstance(this.createOfflinePlayerProxy()); } catch (IllegalAccessException e) { throw new RuntimeException("Cannot access reflection.", e); } catch (InstantiationException e) { @@ -359,6 +343,38 @@ public Player getProxyPlayer() { } } + private OfflinePlayer createOfflinePlayerProxy() { + return (OfflinePlayer) Proxy.newProxyInstance( + OfflinePlayer.class.getClassLoader(), + new Class[]{OfflinePlayer.class}, + (proxy, method, arguments) -> { + if (method.getDeclaringClass() == Object.class) { + return switch (method.getName()) { + case "equals" -> proxy == arguments[0]; + case "hashCode" -> System.identityHashCode(proxy); + case "toString" -> "SerializedOfflinePlayer[" + this.name + "]"; + default -> throw new UnsupportedOperationException("Unsupported Object method " + method); + }; + } + + try { + Method target = SerializedOfflinePlayer.class.getMethod( + method.getName(), + method.getParameterTypes()); + return target.invoke(this, arguments); + } catch (NoSuchMethodException exception) { + if (method.isDefault()) { + return InvocationHandler.invokeDefault(proxy, method, arguments); + } + throw new UnsupportedOperationException( + "The method " + method.getName() + " is not supported for offline players.", + exception); + } catch (InvocationTargetException exception) { + throw exception.getCause(); + } + }); + } + private static Constructor setupProxyPlayerConstructor() { final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods(); final String[] methodNames = new String[offlinePlayerMethods.length]; diff --git a/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 37e43d805..92f16fa5a 100644 --- a/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -426,14 +426,20 @@ public void registerEvents(PluginManager manager, Plugin plugin) { this.injected = true; this.networkManagerInjector.inject(); + // Paper disables reconfiguration when any PlayerLoginEvent listener is registered. + if (!this.networkManagerInjector.isUsingPaperChannelInitializer()) { + manager.registerEvents(new Listener() { + + @EventHandler(priority = EventPriority.LOWEST) + public void handleLogin(PlayerLoginEvent event) { + networkManagerInjector.getInjector(event.getPlayer()).inject(); + } + }, plugin); + } + // all listeners we need, this is a bit messy, but it makes the job correctly manager.registerEvents(new Listener() { - @EventHandler(priority = EventPriority.LOWEST) - public void handleLogin(PlayerLoginEvent event) { - networkManagerInjector.getInjector(event.getPlayer()).inject(); - } - @EventHandler(priority = EventPriority.LOWEST) public void handleJoin(PlayerJoinEvent event) { networkManagerInjector.getInjector(event.getPlayer()).inject(); diff --git a/src/main/java/com/comphenix/protocol/injector/netty/Injector.java b/src/main/java/com/comphenix/protocol/injector/netty/Injector.java index ce8eec128..5e7a83f52 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/Injector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/Injector.java @@ -1,6 +1,7 @@ package com.comphenix.protocol.injector.netty; import java.net.SocketAddress; +import java.util.UUID; import org.bukkit.entity.Player; @@ -67,6 +68,24 @@ public interface Injector { */ Player getPlayer(); + /** + * Retrieve the player name captured during login. + * + * @return The player name, or NULL if it is not known yet. + */ + default String getPlayerName() { + return null; + } + + /** + * Retrieve the player UUID captured during login. + * + * @return The player UUID, or NULL if it is not available. + */ + default UUID getPlayerUniqueId() { + return null; + } + /** * Set the current player instance. * diff --git a/src/main/java/com/comphenix/protocol/injector/netty/channel/EmptyInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/channel/EmptyInjector.java index 59077d9bf..b71f5b468 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/channel/EmptyInjector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/channel/EmptyInjector.java @@ -1,6 +1,7 @@ package com.comphenix.protocol.injector.netty.channel; import java.net.SocketAddress; +import java.util.UUID; import org.bukkit.entity.Player; @@ -67,6 +68,16 @@ public Player getPlayer() { return this.player; } + @Override + public String getPlayerName() { + return this.player != null ? this.player.getName() : null; + } + + @Override + public UUID getPlayerUniqueId() { + return this.player != null ? this.player.getUniqueId() : null; + } + @Override public void setPlayer(Player player) { this.player = player; diff --git a/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java index 7984955d5..9809795a9 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java @@ -4,7 +4,9 @@ import java.lang.reflect.Modifier; import java.net.SocketAddress; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -31,6 +33,7 @@ import com.comphenix.protocol.injector.packet.PacketRegistry; import com.comphenix.protocol.injector.temporary.TemporaryPlayer; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.FieldAccessor; import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; @@ -100,6 +103,7 @@ private static String getRandomKey() { // information about the player belonging to this injector private String playerName; + private UUID playerUniqueId; private Player player; // lazy initialized fields, if we don't need them we don't bother about them @@ -115,6 +119,10 @@ public NettyChannelInjector( ) { // bukkit stuff this.player = player; + if (player != null && !(player instanceof TemporaryPlayer)) { + this.playerName = player.getName(); + this.playerUniqueId = player.getUniqueId(); + } // protocol lib stuff this.errorReporter = errorReporter; @@ -361,12 +369,23 @@ public Player getPlayer() { return this.player; } + @Override + public String getPlayerName() { + return this.playerName; + } + + @Override + public UUID getPlayerUniqueId() { + return this.playerUniqueId; + } + @Override public void setPlayer(Player player) { this.injectionFactory.invalidate(this.player, this.playerName); this.player = player; this.playerName = player.getName(); + this.playerUniqueId = player.getUniqueId(); this.injectionFactory.cacheInjector(player, this); this.injectionFactory.cacheInjector(player.getName(), this); @@ -421,14 +440,19 @@ void processInbound(ChannelHandlerContext ctx, PacketContainer packet) { // process login packets for the player name if (packet.getType() == PacketType.Login.Client.START) { String username; + UUID uniqueId; if (MinecraftVersion.WILD_UPDATE.atOrAbove()) { // 1.19 replaced the gameprofile with username and uuid field username = packet.getStrings().readSafely(0); + uniqueId = readLoginUniqueId(packet); } else { WrappedGameProfile profile = packet.getGameProfiles().readSafely(0); username = profile != null ? profile.getName() : null; + uniqueId = profile != null ? profile.getUUID() : null; } + this.playerUniqueId = uniqueId; + if (username != null) { this.playerName = username; this.injectionFactory.cacheInjector(username, this); @@ -443,6 +467,22 @@ void processInbound(ChannelHandlerContext ctx, PacketContainer packet) { } } + private static UUID readLoginUniqueId(PacketContainer packet) { + UUID uniqueId = packet.getUUIDs().readSafely(0); + if (uniqueId != null) { + return uniqueId; + } + + StructureModifier optionals = packet.getModifier().withType(Optional.class); + for (int i = 0; i < optionals.size(); i++) { + Optional optional = optionals.readSafely(i); + if (optional != null && optional.orElse(null) instanceof UUID optionalUniqueId) { + return optionalUniqueId; + } + } + return null; + } + private void processInboundInternal(ChannelHandlerContext ctx, PacketContainer packetContainer) { if (this.listenerManager.hasMainThreadListener(packetContainer.getType()) && !Bukkit.isPrimaryThread()) { // not on the main thread but we are required to be reschedule the packet on the diff --git a/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java index 17b98d4bf..88bff3c4e 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java @@ -15,6 +15,7 @@ import com.comphenix.protocol.injector.ListenerManager; import com.comphenix.protocol.injector.netty.Injector; import com.comphenix.protocol.injector.netty.channel.InjectionFactory; +import com.comphenix.protocol.internal.PlatformProvider; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.FieldAccessor; @@ -23,6 +24,7 @@ import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.Pair; +import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; public class NetworkManagerInjector { @@ -36,7 +38,9 @@ public class NetworkManagerInjector { private final InjectionFactory injectionFactory; // netty handler + private final InjectionChannelInboundHandler injectionHandler; private final InjectionChannelInitializer pipelineInjectorHandler; + private Runnable paperChannelInitializerCleanup; // status of this injector private boolean closed = false; @@ -47,22 +51,35 @@ public NetworkManagerInjector(Plugin plugin, ListenerManager listenerManager, Er this.injectionFactory = new InjectionFactory(plugin, reporter, listenerManager); // hooking netty handlers - InjectionChannelInboundHandler injectionHandler = new InjectionChannelInboundHandler( + this.injectionHandler = new InjectionChannelInboundHandler( this.errorReporter, this.injectionFactory); - this.pipelineInjectorHandler = new InjectionChannelInitializer(INBOUND_INJECT_HANDLER_NAME, injectionHandler); + this.pipelineInjectorHandler = new InjectionChannelInitializer( + INBOUND_INJECT_HANDLER_NAME, + this.injectionHandler); } public Injector getInjector(Player player) { return this.injectionFactory.fromPlayer(player); } + public boolean isUsingPaperChannelInitializer() { + return this.paperChannelInitializerCleanup != null; + } + @SuppressWarnings("unchecked") public void inject() { if (this.closed || this.injected) { return; } + PlatformProvider platformProvider = PlatformProvider.get(); + if (platformProvider.hasEarlyChannelInitialization()) { + this.paperChannelInitializerCleanup = platformProvider.registerChannelInitializer(this::injectChannel); + this.injected = true; + return; + } + // get all "server connections" defined in the minecraft server class FuzzyReflection server = FuzzyReflection.fromClass(MinecraftReflection.getMinecraftServerClass()); List serverConnectionGetter = server.getMethodList(FuzzyMethodContract.newBuilder() @@ -125,6 +142,10 @@ public void inject() { this.injected = true; } + private void injectChannel(Channel channel) { + channel.pipeline().addLast(INBOUND_INJECT_HANDLER_NAME, this.injectionHandler); + } + public void close() { if (this.closed || !this.injected) { return; @@ -153,6 +174,13 @@ public void close() { // clear up this.overriddenLists.clear(); - this.injectionFactory.close(); + try { + if (this.paperChannelInitializerCleanup != null) { + this.paperChannelInitializerCleanup.run(); + } + } finally { + this.paperChannelInitializerCleanup = null; + this.injectionFactory.close(); + } } } diff --git a/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactory.java b/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactory.java index ed05bc79f..357fec2f9 100644 --- a/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactory.java +++ b/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactory.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -132,7 +133,16 @@ else if (methodName.equals("getPlayer")) { if (methodName.equals("isOnline")) { return injector.isConnected(); } else if (methodName.equals("getName")) { + String playerName = injector.getPlayerName(); + if (playerName != null) { + return playerName; + } return "UNKNOWN[" + injector.getAddress() + "]"; + } else if (methodName.equals("getUniqueId")) { + UUID playerUniqueId = injector.getPlayerUniqueId(); + if (playerUniqueId != null) { + return playerUniqueId; + } } // Ignore all other methods @@ -184,14 +194,16 @@ private static Object sendMessage(Injector injector, String message) { *
  • getPlayer()
  • *
  • getAddress()
  • *
  • getServer()
  • + *
  • getName()
  • + *
  • getUniqueId(), when supplied by the login protocol
  • *
  • chat(String)
  • *
  • sendMessage(String)
  • *
  • sendMessage(String[])
  • *
  • kickPlayer(String)
  • * *

    - * Note that a temporary player has not yet been assigned a name, and thus cannot be - * uniquely identified. Use the address instead. + * Before the login start packet is received, the player has no profile information + * and can only be identified by its address. * * @return A temporary player instance. */ diff --git a/src/main/java/com/comphenix/protocol/internal/PlatformProvider.java b/src/main/java/com/comphenix/protocol/internal/PlatformProvider.java new file mode 100644 index 000000000..df87e234d --- /dev/null +++ b/src/main/java/com/comphenix/protocol/internal/PlatformProvider.java @@ -0,0 +1,35 @@ +package com.comphenix.protocol.internal; + +import java.util.ServiceLoader; +import java.util.function.Consumer; + +import org.bukkit.command.CommandExecutor; + +import com.comphenix.protocol.ProtocolLib; + +import io.netty.channel.Channel; + +public interface PlatformProvider { + + static PlatformProvider get() { + return Holder.INSTANCE; + } + + boolean hasEarlyChannelInitialization(); + + Runnable registerChannelInitializer(Consumer channelInitializer); + + void registerCommand(ProtocolLib plugin, String name, CommandExecutor executor); + + final class Holder { + + private static final PlatformProvider INSTANCE = ServiceLoader.load( + PlatformProvider.class, + PlatformProvider.class.getClassLoader()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No ProtocolLib platform provider is available")); + + private Holder() { + } + } +} diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java b/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java index 433a45056..90daebc5a 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java @@ -57,21 +57,25 @@ private MinecraftMethods() { } public static Function getFriendlyBufBufConstructor() { - if (friendlyBufBufConstructor == null) { - Optional> registryByteBuf = MinecraftReflection.getRegistryFriendlyByteBufClass(); - - if (registryByteBuf.isPresent()) { - ConstructorAccessor accessor = Accessors.getConstructorAccessor(FuzzyReflection.fromClass(registryByteBuf.get()).getConstructor(FuzzyMethodContract.newBuilder() - .parameterDerivedOf(ByteBuf.class) - .parameterDerivedOf(MinecraftReflection.getRegistryAccessClass()) - .build())); - friendlyBufBufConstructor = (byteBuf) -> accessor.invoke(byteBuf, MinecraftRegistryAccess.get()); - } else { - ConstructorAccessor accessor = Accessors.getConstructorAccessor(MinecraftReflection.getPacketDataSerializerClass(), ByteBuf.class); - friendlyBufBufConstructor = (byteBuf) -> accessor.invoke(byteBuf); - } - } - return friendlyBufBufConstructor; + if (friendlyBufBufConstructor == null) { + Optional> registryByteBuf = MinecraftReflection.getRegistryFriendlyByteBufClass(); + + if (registryByteBuf.isPresent()) { + ConstructorAccessor accessor = Accessors.getConstructorAccessor( + FuzzyReflection.fromClass(registryByteBuf.get()).getConstructor( + FuzzyMethodContract.newBuilder() + .parameterExactType(ByteBuf.class, 0) + .parameterExactType(MinecraftReflection.getRegistryAccessClass(), 1) + .build())); + friendlyBufBufConstructor = byteBuf -> accessor.invoke(byteBuf, MinecraftRegistryAccess.get()); + } else { + ConstructorAccessor accessor = Accessors.getConstructorAccessor( + MinecraftReflection.getPacketDataSerializerClass(), + ByteBuf.class); + friendlyBufBufConstructor = byteBuf -> accessor.invoke(byteBuf); + } + } + return friendlyBufBufConstructor; } /** diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 9c33fd2b3..569f7b49b 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -928,6 +928,10 @@ public static Class getMobEffectListClass() { return getMinecraftClass("world.effect.MobEffectList", "MobEffectList", "world.effect.MobEffect", "world.effect.MobEffects"); } + public static Class getDamageTypeClass() { + return getMinecraftClass("world.damagesource.DamageType"); + } + public static Class getSoundEffectClass() { return getNullableNMS("sounds.SoundEffect", "sounds.SoundEvent", "SoundEffect", "sounds.SoundEvents"); } diff --git a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 9159e88d9..a4438b8ad 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -1436,6 +1436,142 @@ public PotionEffectType getSpecific(Object generic) { }); } + // Lazily-resolved CraftDamageType reflection. volatile + DCL keeps the publish safe + // and avoids re-running fuzzy lookups in concurrent injection paths. + private static volatile MethodAccessor damageTypeToNms = null; + private static volatile MethodAccessor damageTypeFromNms = null; + private static volatile MethodAccessor damageTypeHolderValue = null; + private static boolean damageTypeToNmsReturnsHolder; + private static boolean damageTypeFromNmsAcceptsHolder; + private static final Map damageTypeHolders = new ConcurrentHashMap<>(); + + private static FuzzyReflection craftDamageTypeFuzzy() { + return FuzzyReflection.fromClass(getCraftBukkitClass("damage.CraftDamageType"), false); + } + + private static MethodAccessor resolveDamageTypeToNms() { + MethodAccessor accessor = damageTypeToNms; + if (accessor != null) return accessor; + synchronized (BukkitConverters.class) { + accessor = damageTypeToNms; + if (accessor != null) return accessor; + try { + accessor = Accessors.getMethodAccessor(craftDamageTypeFuzzy().getMethod(FuzzyMethodContract.newBuilder() + .parameterExactArray(org.bukkit.damage.DamageType.class) + .returnTypeExact(MinecraftReflection.getDamageTypeClass()) + .requireModifier(Modifier.STATIC) + .build())); + } catch (IllegalArgumentException exception) { + accessor = Accessors.getMethodAccessor(craftDamageTypeFuzzy().getMethod(FuzzyMethodContract.newBuilder() + .parameterExactArray(org.bukkit.damage.DamageType.class) + .returnTypeExact(MinecraftReflection.getHolderClass()) + .requireModifier(Modifier.STATIC) + .build())); + damageTypeToNmsReturnsHolder = true; + } + damageTypeToNms = accessor; + return accessor; + } + } + + private static MethodAccessor resolveDamageTypeFromNms() { + MethodAccessor accessor = damageTypeFromNms; + if (accessor != null) return accessor; + synchronized (BukkitConverters.class) { + accessor = damageTypeFromNms; + if (accessor != null) return accessor; + try { + accessor = Accessors.getMethodAccessor(craftDamageTypeFuzzy().getMethod(FuzzyMethodContract.newBuilder() + .parameterExactArray(MinecraftReflection.getDamageTypeClass()) + .returnTypeExact(org.bukkit.damage.DamageType.class) + .requireModifier(Modifier.STATIC) + .build())); + } catch (IllegalArgumentException exception) { + accessor = Accessors.getMethodAccessor(craftDamageTypeFuzzy().getMethod(FuzzyMethodContract.newBuilder() + .parameterExactArray(MinecraftReflection.getHolderClass()) + .returnTypeExact(org.bukkit.damage.DamageType.class) + .requireModifier(Modifier.STATIC) + .build())); + damageTypeFromNmsAcceptsHolder = true; + } + damageTypeFromNms = accessor; + return accessor; + } + } + + private static Object getHolderValue(Object holder) { + MethodAccessor accessor = damageTypeHolderValue; + if (accessor == null) { + synchronized (BukkitConverters.class) { + accessor = damageTypeHolderValue; + if (accessor == null) { + accessor = Accessors.getMethodAccessor( + FuzzyReflection.fromClass(MinecraftReflection.getHolderClass(), false) + .getMethod(FuzzyMethodContract.newBuilder() + .parameterCount(0) + .banModifier(Modifier.STATIC) + .returnTypeExact(Object.class) + .build())); + damageTypeHolderValue = accessor; + } + } + } + return accessor.invoke(holder); + } + + private static Object getDamageTypeHolder(Object generic) { + Object holder = damageTypeHolders.get(generic); + if (holder != null) { + return holder; + } + + MethodAccessor accessor = resolveDamageTypeToNms(); + if (!damageTypeToNmsReturnsHolder) { + throw new IllegalStateException("CraftDamageType accepts holders but does not produce them"); + } + + for (org.bukkit.damage.DamageType damageType : org.bukkit.Registry.DAMAGE_TYPE) { + Object candidate = accessor.invoke(null, damageType); + Object candidateValue = getHolderValue(candidate); + damageTypeHolders.putIfAbsent(candidateValue, candidate); + if (Objects.equal(candidateValue, generic)) { + return candidate; + } + } + + throw new IllegalArgumentException("Unable to resolve a holder for damage type " + generic); + } + + public static EquivalentConverter getDamageTypeConverter() { + return ignoreNull(new EquivalentConverter() { + + @Override + public Class getSpecificType() { + return org.bukkit.damage.DamageType.class; + } + + @Override + public Object getGeneric(org.bukkit.damage.DamageType specific) { + Object generic = resolveDamageTypeToNms().invoke(null, specific); + if (damageTypeToNmsReturnsHolder) { + Object value = getHolderValue(generic); + damageTypeHolders.putIfAbsent(value, generic); + return value; + } + return generic; + } + + @Override + public org.bukkit.damage.DamageType getSpecific(Object generic) { + MethodAccessor accessor = resolveDamageTypeFromNms(); + if (damageTypeFromNmsAcceptsHolder) { + generic = getDamageTypeHolder(generic); + } + return (org.bukkit.damage.DamageType) accessor.invoke(null, generic); + } + }); + } + private static Class dimensionManager; private static FauxEnumConverter dimensionConverter; private static FauxEnumConverter dimensionImplConverter; diff --git a/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java b/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java index 4fdd823f8..1ff81569d 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java +++ b/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java @@ -33,8 +33,8 @@ * by default. Constructing a new wrapper instance will give out a handle to a completely new implemented type, that * allows to set a key and some kind of data of any choice. *

    - * Note that constructing this class from a generic handle is only possible for the spigot-specific UnknownPayload type. - * All other payloads should be accessed via a structure modifier directly. + * Constructing this class from a generic handle requires a payload that retains its raw byte data, such as + * {@code DiscardedPayload}. Typed payloads without raw bytes should be accessed through a structure modifier directly. * * @author Pasqual Koschmieder */ @@ -46,8 +46,14 @@ public final class CustomPacketPayloadWrapper { private static final ConstructorAccessor PAYLOAD_WRAPPER_CONSTRUCTOR; private static final MethodAccessor GET_ID_PAYLOAD_METHOD; + private static final MethodAccessor GET_TYPE_PAYLOAD_METHOD; + private static final MethodAccessor GET_ID_PAYLOAD_TYPE_METHOD; private static final MethodAccessor SERIALIZE_PAYLOAD_METHOD; + /** True when running on MC 1.21.5+ where DiscardedPayload is used instead of the ByteBuddy proxy. */ + private static final boolean USE_DISCARDED_PAYLOAD; + private static final boolean DISCARDED_PAYLOAD_USES_BYTE_ARRAY; + private static final EquivalentConverter CONVERTER; static { @@ -55,22 +61,62 @@ public final class CustomPacketPayloadWrapper { MINECRAFT_KEY_CLASS = MinecraftReflection.getMinecraftKeyClass(); CUSTOM_PACKET_PAYLOAD_CLASS = MinecraftReflection.getMinecraftClass("network.protocol.common.custom.CustomPacketPayload"); - Method getPayloadId = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder() - .banModifier(Modifier.STATIC) - .returnTypeExact(MINECRAFT_KEY_CLASS) - .parameterCount(0) - .build()); - GET_ID_PAYLOAD_METHOD = Accessors.getMethodAccessor(getPayloadId); - - Method serializePayloadData = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder() - .banModifier(Modifier.STATIC) - .returnTypeVoid() - .parameterCount(1) - .parameterDerivedOf(ByteBuf.class, 0) - .build()); - SERIALIZE_PAYLOAD_METHOD = Accessors.getMethodAccessor(serializePayloadData); - - Constructor payloadWrapperConstructor = makePayloadWrapper(); + // MC 1.21.5+: CustomPacketPayload no longer has id(); DiscardedPayload is the fallback payload + // MC < 1.21.5: CustomPacketPayload.id() returns Identifier directly + Method getPayloadId = null; + Method getPayloadType = null; + Method getPayloadTypeId = null; + boolean useDiscardedPayload = false; + try { + getPayloadId = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder() + .banModifier(Modifier.STATIC) + .returnTypeExact(MINECRAFT_KEY_CLASS) + .parameterCount(0) + .build()); + } catch (IllegalArgumentException ignored) { + // New API: CustomPacketPayload.type().id() exposes the identifier for every payload type. + getPayloadType = CUSTOM_PACKET_PAYLOAD_CLASS.getMethod("type"); + getPayloadTypeId = getPayloadType.getReturnType().getMethod("id"); + useDiscardedPayload = true; + } + GET_ID_PAYLOAD_METHOD = getPayloadId != null ? Accessors.getMethodAccessor(getPayloadId) : null; + GET_TYPE_PAYLOAD_METHOD = getPayloadType != null ? Accessors.getMethodAccessor(getPayloadType) : null; + GET_ID_PAYLOAD_TYPE_METHOD = getPayloadTypeId != null ? Accessors.getMethodAccessor(getPayloadTypeId) : null; + USE_DISCARDED_PAYLOAD = useDiscardedPayload; + + // serialize method may not exist in new API + Method serializePayloadData = null; + try { + serializePayloadData = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder() + .banModifier(Modifier.STATIC) + .returnTypeVoid() + .parameterCount(1) + .parameterDerivedOf(ByteBuf.class, 0) + .build()); + } catch (IllegalArgumentException ignored) { + // Not present in new API + } + SERIALIZE_PAYLOAD_METHOD = serializePayloadData != null + ? Accessors.getMethodAccessor(serializePayloadData) : null; + + Constructor payloadWrapperConstructor; + boolean discardedPayloadUsesByteArray = false; + if (useDiscardedPayload) { + // The discarded payload changed from ByteBuf to byte[] in 26.2. + Class discardedPayloadClass = MinecraftReflection.getMinecraftClass( + "network.protocol.common.custom.DiscardedPayload"); + try { + payloadWrapperConstructor = discardedPayloadClass.getConstructor( + MINECRAFT_KEY_CLASS, byte[].class); + discardedPayloadUsesByteArray = true; + } catch (NoSuchMethodException exception) { + payloadWrapperConstructor = discardedPayloadClass.getConstructor( + MINECRAFT_KEY_CLASS, ByteBuf.class); + } + } else { + payloadWrapperConstructor = makePayloadWrapper(); + } + DISCARDED_PAYLOAD_USES_BYTE_ARRAY = discardedPayloadUsesByteArray; PAYLOAD_WRAPPER_CONSTRUCTOR = Accessors.getConstructorAccessor(payloadWrapperConstructor); CONVERTER = new EquivalentConverter() { @@ -171,29 +217,50 @@ public static EquivalentConverter getConverter() { * @return a wrapper holding the minecraft key and payload of the given custom payload instance. */ public static CustomPacketPayloadWrapper fromUnknownPayload(Object payload) { - Object messageId = GET_ID_PAYLOAD_METHOD.invoke(payload); - MinecraftKey id = MinecraftKey.getConverter().getSpecific(messageId); + MinecraftKey id = getPayloadId(payload); // we read and retain the underlying buffer in case the class uses a buffer to store the data // this way, when passing the packet to further handling, the buffer is not released and can be re-used StructureModifier modifier = new StructureModifier<>(payload.getClass()).withTarget(payload); - byte[] messagePayload = modifier.withType(ByteBuf.class).optionRead(0) - .map(buffer -> { - ByteBuf buf = (ByteBuf) buffer; - byte[] data = StreamSerializer.getDefault().getBytesAndRelease(buf.markReaderIndex().retain()); - buf.resetReaderIndex(); - return data; - }) - .orElseGet(() -> { - ByteBuf buffer = Unpooled.buffer(); - Object serializer = MinecraftReflection.getPacketDataSerializer(buffer); - SERIALIZE_PAYLOAD_METHOD.invoke(payload, serializer); - return StreamSerializer.getDefault().getBytesAndRelease(buffer); - }); + byte[] messagePayload = (byte[]) modifier.withType(byte[].class).readSafely(0); + if (messagePayload != null) { + messagePayload = messagePayload.clone(); + } else { + messagePayload = modifier.withType(ByteBuf.class).optionRead(0) + .map(buffer -> { + ByteBuf buf = (ByteBuf) buffer; + byte[] data = StreamSerializer.getDefault().getBytesAndRelease(buf.markReaderIndex().retain()); + buf.resetReaderIndex(); + return data; + }) + .orElseGet(() -> { + if (SERIALIZE_PAYLOAD_METHOD == null) { + throw new UnsupportedOperationException( + "Cannot extract raw custom payload bytes from " + payload.getClass().getName() + + ": payload has no byte array or ByteBuf field and this Minecraft version " + + "does not expose CustomPacketPayload serialization"); + } + ByteBuf buffer = Unpooled.buffer(); + Object serializer = MinecraftReflection.getPacketDataSerializer(buffer); + SERIALIZE_PAYLOAD_METHOD.invoke(payload, serializer); + return StreamSerializer.getDefault().getBytesAndRelease(buffer); + }); + } return new CustomPacketPayloadWrapper(messagePayload, id); } + private static MinecraftKey getPayloadId(Object payload) { + Object messageId; + if (GET_ID_PAYLOAD_METHOD != null) { + messageId = GET_ID_PAYLOAD_METHOD.invoke(payload); + } else { + Object payloadType = GET_TYPE_PAYLOAD_METHOD.invoke(payload); + messageId = GET_ID_PAYLOAD_TYPE_METHOD.invoke(payloadType); + } + return MinecraftKey.getConverter().getSpecific(messageId); + } + /** * Get the generic id of the wrapped message id. * @@ -230,6 +297,13 @@ public MinecraftKey getId() { * @return a new payload wrapper instance using the provided message id and payload. */ public Object newHandle() { + if (USE_DISCARDED_PAYLOAD) { + if (DISCARDED_PAYLOAD_USES_BYTE_ARRAY) { + return PAYLOAD_WRAPPER_CONSTRUCTOR.invoke(this.getGenericId(), this.payload); + } + ByteBuf buf = Unpooled.copiedBuffer(this.payload); + return PAYLOAD_WRAPPER_CONSTRUCTOR.invoke(this.getGenericId(), buf); + } return PAYLOAD_WRAPPER_CONSTRUCTOR.invoke(this.getGenericId(), this.payload); } diff --git a/src/test/java/com/comphenix/protocol/BukkitInitialization.java b/src/test/java/com/comphenix/protocol/BukkitInitialization.java index 981e82f28..4f443dcf8 100644 --- a/src/test/java/com/comphenix/protocol/BukkitInitialization.java +++ b/src/test/java/com/comphenix/protocol/BukkitInitialization.java @@ -3,16 +3,19 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; import java.util.stream.Stream; import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; import com.comphenix.protocol.reflect.accessors.FieldAccessor; import com.comphenix.protocol.utility.MinecraftReflectionTestUtil; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; +import io.papermc.paper.configuration.GlobalConfiguration; +import io.papermc.paper.configuration.WorldConfiguration; +import io.papermc.paper.registry.PaperRegistryAccess; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; import net.minecraft.SharedConstants; import net.minecraft.commands.Commands; import net.minecraft.core.HolderLookup; @@ -23,12 +26,12 @@ import net.minecraft.resources.RegistryDataLoader; import net.minecraft.resources.Identifier; import net.minecraft.server.Bootstrap; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.RegistryLayer; import net.minecraft.server.ReloadableServerResources; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.packs.PackType; -import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.packs.repository.PackRepository; import net.minecraft.server.packs.repository.ServerPacksSource; import net.minecraft.server.packs.resources.MultiPackResourceManager; @@ -37,10 +40,13 @@ import net.minecraft.server.permissions.PermissionSet; import net.minecraft.tags.TagKey; import net.minecraft.tags.TagLoader; +import net.minecraft.util.Util; import net.minecraft.world.entity.EntityType; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.item.Item; import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.DataPackConfig; +import net.minecraft.world.level.WorldDataConfiguration; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.material.Fluid; import org.apache.logging.log4j.LogManager; @@ -126,13 +132,25 @@ private void initialize() { // Minecraft Data Init SharedConstants.tryDetectVersion(); // .tryDetectVersion() Bootstrap.bootStrap(); // .bootStrap() + Bootstrap.validate(); PackRepository repo = ServerPacksSource.createVanillaTrustedRepository(); - repo.reload(); + MinecraftServer.configurePackRepository( + repo, + new WorldDataConfiguration( + new DataPackConfig( + FeatureFlags.REGISTRY.toNames(FeatureFlags.REGISTRY.allFlags()) + .stream() + .map(Identifier::getPath) + .toList(), + List.of()), + FeatureFlags.REGISTRY.allFlags()), + true, + false); ResourceManager resourceManager = new MultiPackResourceManager( PackType.SERVER_DATA, - repo.getAvailablePacks().stream().map(Pack::open).collect(Collectors.toList()) + repo.openAllSelected() ); LayeredRegistryAccess layeredAccess1 = RegistryLayer.createRegistryAccess(); @@ -143,13 +161,40 @@ private void initialize() { RegistryAccess.Frozen access2; try { - access2 = RegistryDataLoader.load(resourceManager, list1, RegistryDataLoader.WORLDGEN_REGISTRIES, Executors.newSingleThreadExecutor()).get(); + access2 = RegistryDataLoader.load( + resourceManager, + list1, + RegistryDataLoader.WORLDGEN_REGISTRIES, + Util.backgroundExecutor()).get(); } catch (Exception ex) { throw new RuntimeException(ex); } - - LayeredRegistryAccess layeredAccess2 = layeredAccess1.replaceFrom(RegistryLayer.WORLDGEN, access2); + + LayeredRegistryAccess layeredAccess2 = + layeredAccess1.replaceFrom(RegistryLayer.WORLDGEN, access2); + List> dimensionLookups = + Stream.concat(list1.stream(), access2.listRegistries()).toList(); + RegistryAccess.Frozen dimensions = RegistryDataLoader.load( + resourceManager, + dimensionLookups, + RegistryDataLoader.DIMENSION_REGISTRIES, + Util.backgroundExecutor()).join(); + layeredAccess2 = layeredAccess2.replaceFrom(RegistryLayer.DIMENSIONS, dimensions); + + try { + Class.forName(Registry.class.getName()); + } catch (ClassNotFoundException exception) { + throw new RuntimeException(exception); + } + RegistryAccess.Frozen registryCustom = layeredAccess2.compositeAccess().freeze(); + PaperRegistryAccess paperRegistryAccess = PaperRegistryAccess.instance(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + globalConfiguration.collisions = globalConfiguration.new Collisions(); + Accessors.getMethodAccessor( + GlobalConfiguration.class, + "set", + GlobalConfiguration.class).invoke(null, globalConfiguration); ReloadableServerResources dataPackResources = ReloadableServerResources.loadResources( resourceManager, @@ -177,6 +222,13 @@ private void initialize() { DedicatedServer mockedGameServer = mock(DedicatedServer.class); when(mockedGameServer.registryAccess()).thenReturn(registryCustom); + try { + FieldAccessor serverInstance = Accessors.getFieldAccessor( + MinecraftServer.class.getDeclaredField("SERVER")); + serverInstance.set(null, mockedGameServer); + } catch (NoSuchFieldException exception) { + throw new RuntimeException(exception); + } when(mockedServer.getLogger()).thenReturn(java.util.logging.Logger.getLogger("Minecraft")); when(mockedServer.getName()).thenReturn("Mock Server"); @@ -193,7 +245,7 @@ private void initialize() { }); when(mockedServer.getRegistry(any())).thenAnswer(invocation -> { Class registryType = invocation.getArgument(0); - Object registry = CraftRegistry.createRegistry(registryType, registryCustom); + Object registry = paperRegistryAccess.getRegistry(registryType); if (registry == null) { System.err.println("WARN: Missing registry for " + registryType); @@ -245,6 +297,18 @@ private void initialize() { ServerLevel nmsWorld = mock(ServerLevel.class); SpigotWorldConfig mockWorldConfig = mock(SpigotWorldConfig.class); + ConstructorAccessor worldConfigurationConstructor = Accessors.getConstructorAccessor( + WorldConfiguration.class, + SpigotWorldConfig.class, + Identifier.class); + WorldConfiguration worldConfiguration = (WorldConfiguration) worldConfigurationConstructor.invoke( + mockWorldConfig, + Identifier.parse("minecraft:test")); + worldConfiguration.entities = worldConfiguration.new Entities(); + worldConfiguration.entities.spawning = + worldConfiguration.entities.new Spawning(); + worldConfiguration.entities.spawning.despawnTime = new Reference2ObjectOpenHashMap<>(); + when(nmsWorld.paperConfig()).thenReturn(worldConfiguration); try { FieldAccessor spigotConfig = Accessors.getFieldAccessor(nmsWorld.getClass().getField("spigotConfig")); @@ -311,5 +375,35 @@ public Stream stream() { List empty = Collections.emptyList(); return empty.stream(); } + + @Override + public Stream keyStream() { + return Stream.empty(); + } + + @Override + public NamespacedKey getKey(T value) { + return null; + } + + @Override + public boolean hasTag(io.papermc.paper.registry.tag.TagKey key) { + return false; + } + + @Override + public io.papermc.paper.registry.tag.Tag getTag(io.papermc.paper.registry.tag.TagKey key) { + return null; + } + + @Override + public java.util.Collection> getTags() { + return Collections.emptyList(); + } + + @Override + public int size() { + return 0; + } } } diff --git a/src/test/java/com/comphenix/protocol/PlatformCommandRegistrarTest.java b/src/test/java/com/comphenix/protocol/PlatformCommandRegistrarTest.java new file mode 100644 index 000000000..b1b6d5e21 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/PlatformCommandRegistrarTest.java @@ -0,0 +1,53 @@ +package com.comphenix.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.comphenix.protocol.paper.PaperPlatformProvider; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; + +class PlatformCommandRegistrarTest { + + @Test + void registersLegacyExecutorAsPaperCommand() { + ProtocolLib plugin = mock(ProtocolLib.class); + CommandExecutor executor = mock(CommandExecutor.class); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(BasicCommand.class); + + new PaperPlatformProvider().registerCommand(plugin, "filter", executor); + + verify(plugin).registerCommand( + eq("filter"), + eq("Add or remove programmable filters to the packet listeners."), + eq(List.of("packet_filter")), + commandCaptor.capture()); + + CommandSourceStack source = mock(CommandSourceStack.class); + CommandSender sender = mock(CommandSender.class); + when(source.getSender()).thenReturn(sender); + + ArgumentCaptor legacyCommand = ArgumentCaptor.forClass(Command.class); + String[] arguments = {"add"}; + when(executor.onCommand(eq(sender), legacyCommand.capture(), eq("filter"), eq(arguments))) + .thenReturn(false); + + commandCaptor.getValue().execute(source, arguments); + + assertEquals("filter", legacyCommand.getValue().getName()); + assertEquals("protocol.admin", commandCaptor.getValue().permission()); + verify(sender).sendMessage("/filter add|remove name [ID start]-[ID stop]"); + } +} diff --git a/src/test/java/com/comphenix/protocol/injector/netty/manager/PlatformChannelInitializerTest.java b/src/test/java/com/comphenix/protocol/injector/netty/manager/PlatformChannelInitializerTest.java new file mode 100644 index 000000000..79033f745 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/injector/netty/manager/PlatformChannelInitializerTest.java @@ -0,0 +1,42 @@ +package com.comphenix.protocol.injector.netty.manager; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import com.comphenix.protocol.internal.PlatformProvider; +import com.comphenix.protocol.paper.PaperPlatformProvider; + +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.papermc.paper.network.ChannelInitializeListenerHolder; + +class PlatformChannelInitializerTest { + + @AfterEach + void resetHolder() { + ChannelInitializeListenerHolder.removeListener(PaperPlatformProvider.CHANNEL_INITIALIZER_KEY); + } + + @Test + void registersInvokesAndRemovesListener() { + AtomicReference initializedChannel = new AtomicReference<>(); + + PlatformProvider provider = PlatformProvider.get(); + Runnable cleanup = provider.registerChannelInitializer(initializedChannel::set); + assertTrue(provider.hasEarlyChannelInitialization()); + assertTrue(ChannelInitializeListenerHolder.hasListener(PaperPlatformProvider.CHANNEL_INITIALIZER_KEY)); + + Channel channel = new EmbeddedChannel(); + ChannelInitializeListenerHolder.callListeners(channel); + assertSame(channel, initializedChannel.get()); + + cleanup.run(); + assertFalse(ChannelInitializeListenerHolder.hasListener(PaperPlatformProvider.CHANNEL_INITIALIZER_KEY)); + } +} diff --git a/src/test/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactoryTest.java b/src/test/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactoryTest.java index 388affefe..3e97748f3 100644 --- a/src/test/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactoryTest.java +++ b/src/test/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerFactoryTest.java @@ -1,5 +1,8 @@ package com.comphenix.protocol.injector.temporary; +import java.util.UUID; + +import com.comphenix.protocol.injector.netty.Injector; import org.bukkit.Server; import org.bukkit.entity.Player; import org.junit.jupiter.api.BeforeEach; @@ -7,13 +10,18 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; public class TemporaryPlayerFactoryTest { @Mock Server server; + @Mock + Injector injector; + @BeforeEach public void initMocks() { MockitoAnnotations.openMocks(this); @@ -24,4 +32,18 @@ public void testUnavailableSocketInjector() { Player player = TemporaryPlayerFactory.createTemporaryPlayer(); assertThrows(IllegalStateException.class, player::getPlayer); } + + @Test + public void testLoginProfile() { + Player player = TemporaryPlayerFactory.createTemporaryPlayer(); + UUID uniqueId = UUID.randomUUID(); + + TemporaryPlayerFactory.setInjectorForPlayer(player, this.injector); + when(this.injector.getPlayer()).thenReturn(player); + when(this.injector.getPlayerName()).thenReturn("dmulloy2"); + when(this.injector.getPlayerUniqueId()).thenReturn(uniqueId); + + assertEquals("dmulloy2", player.getName()); + assertEquals(uniqueId, player.getUniqueId()); + } } diff --git a/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java b/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java index 901fd38c6..cc1967aee 100644 --- a/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java +++ b/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java @@ -13,6 +13,7 @@ import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; +import org.bukkit.damage.DamageType; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -63,6 +64,14 @@ public void testEither() { assertEquals(wrapped.right(), nmsEither.right()); } + @Test + public void testDamageType() { + EquivalentConverter converter = BukkitConverters.getDamageTypeConverter(); + Object generic = converter.getGeneric(DamageType.FALL); + + assertEquals(DamageType.FALL, converter.getSpecific(generic)); + } + @Test @Disabled("Fails due to shared packet classes between protocol states") public void testPacketContainerConverter() { diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedBlockDataTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedBlockDataTest.java index 8253057ba..744c3a65d 100644 --- a/src/test/java/com/comphenix/protocol/wrappers/WrappedBlockDataTest.java +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedBlockDataTest.java @@ -58,7 +58,7 @@ public void testMaterialCreation() { @Test public void testDataCreation() { BlockState nmsData = CraftMagicNumbers.getBlock(Material.CYAN_STAINED_GLASS_PANE).defaultBlockState(); - GlassPane data = (GlassPane) CraftBlockData.fromData(nmsData); + GlassPane data = (GlassPane) CraftBlockData.createData(nmsData); data.setFace(BlockFace.EAST, true); WrappedBlockData wrapper = WrappedBlockData.createData(data);