Compare commits

...
Author SHA1 Message Date
Tim Langner 9cb125fb1f Update to Minecraft 26.3 (#3681)
* Update to Minecraft 26.3

- Bump the server dependency, latest version and plugin api-version to 26.3
- Add the configuration POST_EFFECTS packet and shift the following ids
- Add the CHANGE_DESTROY_DIRECTION player dig type
- Stop resolving the legacy particle enum on 1.13+, the 26.3 particle packet has an unrelated enum
- Read optional message signatures in getMessageSignatures
- Cache parameterized structure modifiers separately from the raw field type
- Add WrappedVecDelta and WrappedPositionPath for the new entity movement packets
- Update tests for the 26.3 server classes

* Change default class type from null to void.class
2026-11-03 21:36:13 -06:00
Mark Cockram 0f7670e13d Fix recursive update in StructureCache on legacy versions (#3682)
ConcurrentHashMap.computeIfAbsent rejects re-entrant updates on Java 9+,
and determineBestCreator can re-enter the cache. On 1.8 it also recursed
through PacketRegistry.getStreamCodec into createOldRegister, so only ask
for stream codecs on 1.20.5+, where they exist.
2026-11-03 21:25:51 -06:00
Rishabh Venu 47f465d3f5 Run sync listeners of bundled packets on the main thread (#3674)
* Run sync listeners of bundled packets on the main thread

processOutbound only checked the BUNDLE packet type for main-thread listeners before the bundle was unpacked, so a synchronous listener registered for a packet inside the bundle ran on the thread the bundle was sent from.

Fixes #3665

* Keep the network marker when rescheduling a bundled packet
2026-11-03 21:24:38 -06:00
Dan Mulloy 501e187342 Update links 2026-10-26 08:41:41 -05:00
Rishabh Venu 6720a70dbd Use the stream codec in WirePacket on 1.20.5 and newer (#3676) 2026-10-24 02:42:11 +00:00
Rishabh Venu 3d8e597244 Replace the inbound chat example in the README (#3675) 2026-10-23 22:34:30 -04:00
LOOHP 58d45e79b1 Fix Early Channel Injection Ordering on Paper (#3679) 2026-10-23 21:21:29 -05:00
1wairesd d89a5deb3e Fix NoClassDefFoundError: CommandProtocol$1 on Paper plugin classloader (#3673) 2026-10-06 22:02:46 +00:00
Diogo Correia 1ea47d63ba Fix WrappedLevelChunkData in MC 26.3 (#3671) 2026-10-03 18:18:42 +00:00
Diogo Correia 3c616caf74 Fix asCraftMirror method accessor in MC 26.3 (#3672) 2026-10-03 11:18:45 -05:00
Richard Strunk 583353e9fa Fix Minecraft 26.3 play packet mappings (#3670)
Fix 26.3 play packet mappings
2026-09-24 23:02:31 -05:00
Dan Mulloy f0e959a1ad Fix WrappedDataWatcher in 1.8
Fixes #3557
2026-08-23 20:06:32 -05:00
Dan Mulloy cbe7fd87e4 Fix WrappedGameProfile in 1.21.10
Fixes #3573
2026-08-23 19:55:06 -05:00
Dan Mulloy 3fab29d3c6 Fix WrappedStatistic in modern versions
Fixes #3591
2026-08-23 19:35:28 -05:00
Dan Mulloy fec45cf8fc Restore Java 17 compat for Spigot too 2026-08-23 14:34:29 -05:00
Dan Mulloyandjustcarlux 5c6c773935 Prioritize searching for singletons when determining best creators
Fixes #3663

Co-authored-by: justcarlux <manuelitojuan691@gmail.com>
2026-08-23 14:30:03 -05:00
kik 78b10165ff Fix: Java 17 bytecode target on paper module (#3662)
Fixes #3661
2026-08-19 12:51:56 -05:00
Dan Mulloyandrkfsociety dda284cd7a Fix custom payload handling on modern servers
Co-authored-by: rkfsociety <romankuzminvital@gmail.com>
2026-08-17 21:50:35 -05:00
Dan MulloyandSpagles debdd6d9a2 Fix async packet handling and utility edge cases
Co-authored-by: Spagles <106791090+Spagles@users.noreply.github.com>
2026-08-17 21:23:40 -05:00
Dan Mulloy 2c53c29cf7 Only copy bundle packets when something changes
Fixes #3596
2026-08-17 20:58:33 -05:00
Dan Mulloy 1ef8431764 Fix some issues with bundle packets
Addresses #3596
2026-08-17 20:43:23 -05:00
Dan Mulloy 90eb2a6558 Fix item particles in 26.1+
Fixes #3658
2026-08-17 20:29:16 -05:00
Dan Mulloy 67ce937109 Create modern Paper plugin artifact (#3656) 2026-08-02 20:34:39 -05:00
Dan Mulloy 98856a8b9f Restore compatibility with Java 17
fixes #3643
2026-08-02 19:35:14 -05:00
86 changed files with 3509 additions and 519 deletions

No files matched your search

+11 -5
View File
@@ -6,15 +6,20 @@ 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.
## Updating ProtocolLib for a Minecraft Release
@@ -23,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.
@@ -44,6 +49,7 @@ serverbound and clientbound packet classes and codecs against every protocol pha
- Inspect changed fields, constructors, records, nested classes, and stream codecs instead of relying only on class names.
- Prefer structural or type-based reflection where names are unstable.
- Treat the current server API shape as primary and use older supported shapes only as narrow compatibility fallbacks.
- Put version checks at the narrowest compatibility boundary and leave the older path unchanged.
- If an NMS concept was removed, report it as unsupported or absent rather than treating a different class as equivalent.
- Preserve packet fields that are unrelated to the value being changed.
@@ -54,7 +60,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`.
+4 -2
View File
@@ -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
+5 -2
View File
@@ -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 }}
+2
View File
@@ -7,6 +7,7 @@ definitions, reflection, or public wrappers.
- The main project uses the Gradle wrapper. Do not use `TinyProtocol/pom.xml` to build ProtocolLib.
- The Java toolchain and current Spigot dependency are defined in `build.gradle.kts`.
- Keep the Java 25 build toolchain separate from the Java 17 production bytecode target.
- Production code is under `src/main/java`; JUnit tests and NMS test initialization are under `src/test/java`.
- Keep changes focused. Do not combine protocol fixes with unrelated cleanup or formatting.
- Preserve public API and binary compatibility unless the task explicitly requires a breaking change.
@@ -59,6 +60,7 @@ renamed packet can abort registration of later packets in the same registry.
## Version Compatibility
- Put version checks at the narrowest affected boundary.
- Treat the current supported server API shape as the primary path. Use older supported shapes only as narrow fallbacks.
- Keep the pre-change code path intact whenever possible.
- Use named `MinecraftVersion` constants with `atOrAbove()` instead of scattered version strings.
- A public method should keep the same signature and broad contract across versions.
+57 -11
View File
@@ -14,16 +14,19 @@ lead to more subtle bugs.
* [Spigot Page](https://spigotmc.org/resources/protocollib.1997/)
* [Releases](https://github.com/dmulloy2/ProtocolLib/releases)
* [Dev Build](https://github.com/dmulloy2/ProtocolLib/releases/tag/dev-build)
* [JavaDoc](https://dmulloy2.net/ProtocolLib/javadoc/)
* [Dev Builds](https://ci.dmulloy2.net/job/ProtocolLib/)
* [JavaDoc](https://ci.dmulloy2.net/job/ProtocolLib/javadoc/index.html)
* [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
@@ -66,6 +69,48 @@ dependencies {
}
```
#### Snapshot builds
Snapshot builds are available for testing changes before the next release. To use `5.5.0-SNAPSHOT`,
add the [public repository](https://repo.dmulloy2.net/repository/public/) and update your dependency version.
For Maven:
```xml
<repositories>
<repository>
<id>dmulloy2-public</id>
<url>https://repo.dmulloy2.net/repository/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.dmulloy2</groupId>
<artifactId>ProtocolLib</artifactId>
<version>5.5.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
For Gradle:
```gradle
repositories {
mavenCentral()
maven {
url = uri('https://repo.dmulloy2.net/repository/public/')
}
}
dependencies {
compileOnly 'net.dmulloy2:ProtocolLib:5.5.0-SNAPSHOT'
}
```
#### Accessing the API
Then get a reference to ProtocolManager in onLoad() or onEnable() and you're good to go.
````java
@@ -92,25 +137,26 @@ protocolManager.addPacketListener(new PacketAdapter(
});
````
It's also possible to read and modify the content of these packets. For instance, you can create a global
censor by listening for Packet3Chat events:
It's also possible to read and modify the content of packets, including the ones sent by a client. For
instance, you can censor the text players write on signs by listening for `UPDATE_SIGN` packets:
````java
// Censor
// Censor signs
protocolManager.addPacketListener(new PacketAdapter(
this,
ListenerPriority.NORMAL,
PacketType.Play.Client.CHAT
PacketType.Play.Client.UPDATE_SIGN
) {
@Override
public void onPacketReceiving(PacketEvent event) {
PacketContainer packet = event.getPacket();
String message = packet.getStrings().read(0);
String[] lines = packet.getStringArrays().read(0);
if (message.contains("shit") || message.contains("damn")) {
event.setCancelled(true);
event.getPlayer().sendMessage("Bad manners!");
for (int i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace("shit", "****").replace("damn", "****");
}
packet.getStringArrays().write(0, lines);
}
});
````
+21 -56
View File
@@ -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,83 +43,51 @@ 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 {
// sourceCompatibility = JavaVersion.VERSION_25
// targetCompatibility = JavaVersion.VERSION_25
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
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<String>())
resources.setSrcDirs(emptyList<String>())
}
}
tasks {
test {
dependsOn(":paper:test")
}
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"
check {
dependsOn(":paper:check", ":spigot:check")
}
/*remap {
dependsOn("shadowJar")
inputTask.set(getByName<ShadowJar>("shadowJar"))
version.set(mcVersion)
action.set(RemapTask.Action.MOJANG_TO_SPIGOT)
}
assemble {
dependsOn("remap")
}*/
javadoc {
options.encoding = "UTF-8"
}
compileJava {
options.encoding = "UTF-8"
options.release.set(17)
}
}
tasks.register("shadowJar") {
group = "build"
description = "Builds the Paper and Spigot plugin distributions."
dependsOn(":paper:shadowJar", ":spigot:shadowJar")
}
mavenPublishing {
publishToMavenCentral()
if (!isSnapshot && !isJitPack) {
+1
View File
@@ -1,4 +1,5 @@
version=5.5.0-SNAPSHOT
mcVersion=26.3
mavenCentralUsername=
mavenCentralPassword=
signing.keyId=
+104
View File
@@ -0,0 +1,104 @@
import org.gradle.api.attributes.java.TargetJvmVersion
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
configurations.matching { it.name == "compileClasspath" || it.name == "runtimeClasspath" || it.name == "testCompileClasspath" || it.name == "testRuntimeClasspath" }.configureEach {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
}
}
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
}
}
compileJava {
options.release.set(17)
}
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")
}
}
@@ -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<Channel> 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.",
"/<command> config|check|update|timings|listeners|version|dump",
"protocol.admin",
List.of());
case "packet" -> new CommandMetadata(
"Add or remove a simple packet listener.",
"/<command> 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.",
"/<command> 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",
"/<command> <protocol> <sender> <packet> [location]",
"protocol.admin",
List.of());
default -> throw new IllegalArgumentException("Unknown ProtocolLib command " + name);
};
}
private record CommandMetadata(String description, String usage, String permission, List<String> 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("<command>", 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();
}
}
}
@@ -0,0 +1 @@
com.comphenix.protocol.paper.PaperPlatformProvider
+22
View File
@@ -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.3"
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
+2
View File
@@ -3,3 +3,5 @@
*/
rootProject.name = 'ProtocolLib'
include 'paper', 'spigot'
+79
View File
@@ -0,0 +1,79 @@
import org.gradle.api.attributes.java.TargetJvmVersion
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")
}
configurations.matching { it.name == "compileClasspath" || it.name == "runtimeClasspath" || it.name == "testCompileClasspath" || it.name == "testRuntimeClasspath" }.configureEach {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
tasks {
processResources {
eachFile {
expand("version" to project.version)
}
}
test {
useJUnitPlatform()
}
compileJava {
options.release.set(17)
}
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")
}
}
@@ -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<Channel> 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);
}
}
@@ -0,0 +1 @@
com.comphenix.protocol.spigot.SpigotPlatformProvider
@@ -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
default: op
@@ -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);
}
}
@@ -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());
}
}
@@ -120,25 +120,9 @@ class CommandProtocol extends CommandBase {
return;
}
// Perform on an async thread
Runnable notify = new Runnable() {
@Override
public void run() {
if (command) {
sender.sendMessage(ChatColor.YELLOW + "[ProtocolLib] " + updater.getResult());
String remoteVersion = updater.getRemoteVersion();
if (remoteVersion != null) {
sender.sendMessage(ChatColor.YELLOW + "Remote version: " + remoteVersion);
sender.sendMessage(ChatColor.YELLOW + "Current version: " + plugin.getDescription().getVersion());
}
} else if (updater.shouldNotify() || config.isDebug()) {
sender.sendMessage(ChatColor.YELLOW + "[ProtocolLib] " + updater.getResult());
}
updater.removeListener(this);
updateFinished();
}
};
// Top-level class instead of anonymous inner class so that PaperPluginClassLoader
// can resolve the .class entry when BukkitScheduler dispatches it on the main thread.
Runnable notify = new UpdateNotifyRunnable(sender, command, updater, config, plugin, this);
updater.start(type);
updater.addListener(notify);
}
@@ -144,110 +144,113 @@ public class PacketType implements Serializable, Cloneable, Comparable<PacketTyp
public static final PacketType ENTITY_STATUS = new PacketType(PROTOCOL, SENDER, 0x22, "EntityEvent", "EntityStatus", "SPacketEntityStatus");
public static final PacketType ENTITY_POSITION_SYNC = new PacketType(PROTOCOL, SENDER, 0x23, "EntityPositionSync");
public static final PacketType EXPLOSION = new PacketType(PROTOCOL, SENDER, 0x24, "Explode", "Explosion", "SPacketExplosion");
public static final PacketType UNLOAD_CHUNK = new PacketType(PROTOCOL, SENDER, 0x25, "ForgetLevelChunk", "UnloadChunk", "SPacketUnloadChunk");
public static final PacketType GAME_STATE_CHANGE = new PacketType(PROTOCOL, SENDER, 0x26, "GameEvent", "GameStateChange", "SPacketChangeGameState");
public static final PacketType GAME_RULE_VALUES = new PacketType(PROTOCOL, SENDER, 0x27, "GameRuleValues");
public static final PacketType GAME_TEST_HIGHLIGHT_POS = new PacketType(PROTOCOL, SENDER, 0x28, "GameTestHighlightPos");
public static final PacketType OPEN_WINDOW_HORSE = new PacketType(PROTOCOL, SENDER, 0x29, "MountScreenOpen", "HorseScreenOpen", "OpenWindowHorse");
public static final PacketType HURT_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x2A, "HurtAnimation", "ClientboundHurtAnimationPacket");
public static final PacketType INITIALIZE_BORDER = new PacketType(PROTOCOL, SENDER, 0x2B, "InitializeBorder");
public static final PacketType KEEP_ALIVE = new PacketType(PROTOCOL, SENDER, 0x2C, "KeepAlive", "SPacketKeepAlive");
public static final PacketType MAP_CHUNK = new PacketType(PROTOCOL, SENDER, 0x2D, "LevelChunkWithLight", "MapChunk", "SPacketChunkData");
public static final PacketType WORLD_EVENT = new PacketType(PROTOCOL, SENDER, 0x2E, "LevelEvent", "WorldEvent", "SPacketEffect");
public static final PacketType WORLD_PARTICLES = new PacketType(PROTOCOL, SENDER, 0x2F, "LevelParticles", "WorldParticles", "SPacketParticles");
public static final PacketType LIGHT_UPDATE = new PacketType(PROTOCOL, SENDER, 0x30, "LightUpdate");
public static final PacketType LOGIN = new PacketType(PROTOCOL, SENDER, 0x31, "Login", "SPacketJoinGame");
public static final PacketType LOW_DISK_SPACE_WARNING = new PacketType(PROTOCOL, SENDER, 0x32, "LowDiskSpaceWarning");
public static final PacketType MAP = new PacketType(PROTOCOL, SENDER, 0x33, "MapItemData", "Map", "SPacketMaps");
public static final PacketType OPEN_WINDOW_MERCHANT = new PacketType(PROTOCOL, SENDER, 0x34, "MerchantOffers", "OpenWindowMerchant");
public static final PacketType REL_ENTITY_MOVE = new PacketType(PROTOCOL, SENDER, 0x35, "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Pos", "Entity$RelEntityMove", "Entity$PacketPlayOutRelEntityMove");
public static final PacketType REL_ENTITY_MOVE_LOOK = new PacketType(PROTOCOL, SENDER, 0x36, "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$PosRot", "Entity$RelEntityMoveLook", "Entity$PacketPlayOutRelEntityMoveLook");
public static final PacketType MOVE_MINECART = new PacketType(PROTOCOL, SENDER, 0x37, "MoveMinecart");
public static final PacketType ENTITY_LOOK = new PacketType(PROTOCOL, SENDER, 0x38, "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Rot", "Entity$EntityLook", "Entity$PacketPlayOutEntityLook");
public static final PacketType VEHICLE_MOVE = new PacketType(PROTOCOL, SENDER, 0x39, "MoveVehicle", "VehicleMove", "SPacketMoveVehicle");
public static final PacketType OPEN_BOOK = new PacketType(PROTOCOL, SENDER, 0x3A, "OpenBook");
public static final PacketType OPEN_WINDOW = new PacketType(PROTOCOL, SENDER, 0x3B, "OpenScreen", "OpenWindow", "SPacketOpenWindow");
public static final PacketType OPEN_SIGN_EDITOR = new PacketType(PROTOCOL, SENDER, 0x3C, "OpenSignEditor", "SPacketSignEditorOpen");
public static final PacketType PING = new PacketType(PROTOCOL, SENDER, 0x3D, "Ping");
public static final PacketType PONG_RESPONSE = new PacketType(PROTOCOL, SENDER, 0x3E, "PongResponse");
public static final PacketType AUTO_RECIPE = new PacketType(PROTOCOL, SENDER, 0x3F, "PlaceGhostRecipe", "AutoRecipe", "SPacketPlaceGhostRecipe");
public static final PacketType ABILITIES = new PacketType(PROTOCOL, SENDER, 0x40, "PlayerAbilities", "Abilities", "SPacketPlayerAbilities");
public static final PacketType CHAT = new PacketType(PROTOCOL, SENDER, 0x41, "PlayerChat", "Chat", "SPacketChat");
public static final PacketType PLAYER_COMBAT_END = new PacketType(PROTOCOL, SENDER, 0x42, "PlayerCombatEnd");
public static final PacketType PLAYER_COMBAT_ENTER = new PacketType(PROTOCOL, SENDER, 0x43, "PlayerCombatEnter");
public static final PacketType PLAYER_COMBAT_KILL = new PacketType(PROTOCOL, SENDER, 0x44, "PlayerCombatKill");
public static final PacketType PLAYER_INFO_REMOVE = new PacketType(PROTOCOL, SENDER, 0x45, "PlayerInfoRemove");
public static final PacketType PLAYER_INFO = new PacketType(PROTOCOL, SENDER, 0x46, "PlayerInfoUpdate", "PlayerInfo");
public static final PacketType LOOK_AT = new PacketType(PROTOCOL, SENDER, 0x47, "PlayerLookAt", "LookAt", "SPacketPlayerPosLook");
public static final PacketType POSITION = new PacketType(PROTOCOL, SENDER, 0x48, "PlayerPosition", "Position");
public static final PacketType PLAYER_ROTATION = new PacketType(PROTOCOL, SENDER, 0x49, "PlayerRotation");
public static final PacketType RECIPE_BOOK_ADD = new PacketType(PROTOCOL, SENDER, 0x4A, "RecipeBookAdd");
public static final PacketType RECIPE_BOOK_REMOVE = new PacketType(PROTOCOL, SENDER, 0x4B, "RecipeBookRemove");
public static final PacketType RECIPE_BOOK_SETTINGS = new PacketType(PROTOCOL, SENDER, 0x4C, "RecipeBookSettings");
public static final PacketType ENTITY_DESTROY = new PacketType(PROTOCOL, SENDER, 0x4D, "RemoveEntities", "EntityDestroy", "SPacketDestroyEntities");
public static final PacketType REMOVE_ENTITY_EFFECT = new PacketType(PROTOCOL, SENDER, 0x4E, "RemoveMobEffect", "RemoveEntityEffect", "SPacketRemoveEntityEffect");
public static final PacketType RESET_SCORE = new PacketType(PROTOCOL, SENDER, 0x4F, "ResetScore", "ResetScorePacket");
public static final PacketType REMOVE_RESOURCE_PACK = new PacketType(PROTOCOL, SENDER, 0x50, "ResourcePackPop", "ResourcePackPopPacket");
public static final PacketType ADD_RESOURCE_PACK = new PacketType(PROTOCOL, SENDER, 0x51, "ResourcePackPush", "ResourcePackPushPacket");
public static final PacketType RESPAWN = new PacketType(PROTOCOL, SENDER, 0x52, "Respawn", "SPacketRespawn");
public static final PacketType ENTITY_HEAD_ROTATION = new PacketType(PROTOCOL, SENDER, 0x53, "RotateHead", "EntityHeadRotation", "SPacketEntityHeadLook");
public static final PacketType MULTI_BLOCK_CHANGE = new PacketType(PROTOCOL, SENDER, 0x54, "SectionBlocksUpdate", "MultiBlockChange", "SPacketMultiBlockChange");
public static final PacketType SELECT_ADVANCEMENT_TAB = new PacketType(PROTOCOL, SENDER, 0x55, "SelectAdvancementsTab", "SelectAdvancementTab", "SPacketSelectAdvancementsTab");
public static final PacketType SERVER_DATA = new PacketType(PROTOCOL, SENDER, 0x56, "ServerData");
public static final PacketType SET_ACTION_BAR_TEXT = new PacketType(PROTOCOL, SENDER, 0x57, "SetActionBarText");
public static final PacketType SET_BORDER_CENTER = new PacketType(PROTOCOL, SENDER, 0x58, "SetBorderCenter");
public static final PacketType SET_BORDER_LERP_SIZE = new PacketType(PROTOCOL, SENDER, 0x59, "SetBorderLerpSize");
public static final PacketType SET_BORDER_SIZE = new PacketType(PROTOCOL, SENDER, 0x5A, "SetBorderSize");
public static final PacketType SET_BORDER_WARNING_DELAY = new PacketType(PROTOCOL, SENDER, 0x5B, "SetBorderWarningDelay");
public static final PacketType SET_BORDER_WARNING_DISTANCE = new PacketType(PROTOCOL, SENDER, 0x5C, "SetBorderWarningDistance");
public static final PacketType CAMERA = new PacketType(PROTOCOL, SENDER, 0x5D, "SetCamera", "Camera", "SPacketCamera");
public static final PacketType VIEW_CENTRE = new PacketType(PROTOCOL, SENDER, 0x5E, "SetChunkCacheCenter", "ViewCentre");
public static final PacketType VIEW_DISTANCE = new PacketType(PROTOCOL, SENDER, 0x5F, "SetChunkCacheRadius", "ViewDistance");
public static final PacketType SET_CURSOR_ITEM = new PacketType(PROTOCOL, SENDER, 0x60, "SetCursorItem");
public static final PacketType SPAWN_POSITION = new PacketType(PROTOCOL, SENDER, 0x61, "SetDefaultSpawnPosition", "SpawnPosition", "SPacketSpawnPosition");
public static final PacketType SCOREBOARD_DISPLAY_OBJECTIVE = new PacketType(PROTOCOL, SENDER, 0x62, "SetDisplayObjective", "ScoreboardDisplayObjective", "SPacketDisplayObjective");
public static final PacketType ENTITY_METADATA = new PacketType(PROTOCOL, SENDER, 0x63, "SetEntityData", "EntityMetadata", "SPacketEntityMetadata");
public static final PacketType ATTACH_ENTITY = new PacketType(PROTOCOL, SENDER, 0x64, "SetEntityLink", "AttachEntity", "SPacketEntityAttach");
public static final PacketType ENTITY_VELOCITY = new PacketType(PROTOCOL, SENDER, 0x65, "SetEntityMotion", "EntityVelocity", "SPacketEntityVelocity");
public static final PacketType ENTITY_EQUIPMENT = new PacketType(PROTOCOL, SENDER, 0x66, "SetEquipment", "EntityEquipment", "SPacketEntityEquipment");
public static final PacketType EXPERIENCE = new PacketType(PROTOCOL, SENDER, 0x67, "SetExperience", "Experience", "SPacketSetExperience");
public static final PacketType UPDATE_HEALTH = new PacketType(PROTOCOL, SENDER, 0x68, "SetHealth", "UpdateHealth", "SPacketUpdateHealth");
public static final PacketType HELD_ITEM_SLOT = new PacketType(PROTOCOL, SENDER, 0x69, "SetHeldSlot", "SetCarriedItem", "HeldItemSlot", "SPacketHeldItemChange");
public static final PacketType SCOREBOARD_OBJECTIVE = new PacketType(PROTOCOL, SENDER, 0x6A, "SetObjective", "ScoreboardObjective", "SPacketScoreboardObjective");
public static final PacketType MOUNT = new PacketType(PROTOCOL, SENDER, 0x6B, "SetPassengers", "Mount", "SPacketSetPassengers");
public static final PacketType SET_PLAYER_INVENTORY = new PacketType(PROTOCOL, SENDER, 0x6C, "SetPlayerInventory");
public static final PacketType SCOREBOARD_TEAM = new PacketType(PROTOCOL, SENDER, 0x6D, "SetPlayerTeam", "ScoreboardTeam", "SPacketTeams");
public static final PacketType SCOREBOARD_SCORE = new PacketType(PROTOCOL, SENDER, 0x6E, "SetScore", "ScoreboardScore", "SPacketUpdateScore");
public static final PacketType UPDATE_SIMULATION_DISTANCE = new PacketType(PROTOCOL, SENDER, 0x6F, "SetSimulationDistance");
public static final PacketType SET_SUBTITLE_TEXT = new PacketType(PROTOCOL, SENDER, 0x70, "SetSubtitleText");
public static final PacketType UPDATE_TIME = new PacketType(PROTOCOL, SENDER, 0x71, "SetTime", "UpdateTime", "SPacketTimeUpdate");
public static final PacketType SET_TITLE_TEXT = new PacketType(PROTOCOL, SENDER, 0x72, "SetTitleText");
public static final PacketType SET_TITLES_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x73, "SetTitlesAnimation");
public static final PacketType ENTITY_SOUND = new PacketType(PROTOCOL, SENDER, 0x74, "SoundEntity", "EntitySound", "SPacketSoundEffect");
public static final PacketType NAMED_SOUND_EFFECT = new PacketType(PROTOCOL, SENDER, 0x75, "Sound", "NamedSoundEffect");
public static final PacketType START_CONFIGURATION = new PacketType(PROTOCOL, SENDER, 0x76, "StartConfiguration");
public static final PacketType STOP_SOUND = new PacketType(PROTOCOL, SENDER, 0x77, "StopSound");
public static final PacketType STORE_COOKIE = new PacketType(PROTOCOL, SENDER, 0x78, "StoreCookie");
public static final PacketType SYSTEM_CHAT = new PacketType(PROTOCOL, SENDER, 0x79, "SystemChat");
public static final PacketType PLAYER_LIST_HEADER_FOOTER = new PacketType(PROTOCOL, SENDER, 0x7A, "TabList", "PlayerListHeaderFooter", "SPacketPlayerListHeaderFooter");
public static final PacketType NBT_QUERY = new PacketType(PROTOCOL, SENDER, 0x7B, "TagQuery", "NBTQuery");
public static final PacketType COLLECT = new PacketType(PROTOCOL, SENDER, 0x7C, "TakeItemEntity", "Collect", "SPacketCollectItem");
public static final PacketType ENTITY_TELEPORT = new PacketType(PROTOCOL, SENDER, 0x7D, "TeleportEntity", "EntityTeleport", "SPacketEntityTeleport");
public static final PacketType TEST_INSTANCE_BLOCK_STATUS = new PacketType(PROTOCOL, SENDER, 0x7E, "ClientboundTestInstanceBlockStatus");
public static final PacketType TICKING_STATE = new PacketType(PROTOCOL, SENDER, 0x7F, "TickingState", "TickingStatePacket");
public static final PacketType TICKING_STEP_STATE = new PacketType(PROTOCOL, SENDER, 0x80, "TickingStep", "TickingStepPacket");
public static final PacketType TRANSFER = new PacketType(PROTOCOL, SENDER, 0x81, "Transfer");
public static final PacketType ADVANCEMENTS = new PacketType(PROTOCOL, SENDER, 0x82, "UpdateAdvancements", "Advancements", "SPacketAdvancementInfo");
public static final PacketType UPDATE_ATTRIBUTES = new PacketType(PROTOCOL, SENDER, 0x83, "UpdateAttributes", "SPacketEntityProperties");
public static final PacketType ENTITY_EFFECT = new PacketType(PROTOCOL, SENDER, 0x84, "UpdateMobEffect", "EntityEffect", "SPacketEntityEffect");
public static final PacketType RECIPE_UPDATE = new PacketType(PROTOCOL, SENDER, 0x85, "UpdateRecipes", "RecipeUpdate");
public static final PacketType TAGS = new PacketType(PROTOCOL, SENDER, 0x86, "UpdateTags", "Tags");
public static final PacketType PROJECTILE_POWER = new PacketType(PROTOCOL, SENDER, 0x87, "ProjectilePower");
public static final PacketType REPORT_DETAILS = new PacketType(PROTOCOL, SENDER, 0x88, "CustomReportDetails");
public static final PacketType SERVER_LINKS = new PacketType(PROTOCOL, SENDER, 0x89, "ServerLinks");
public static final PacketType TRACKED_WAYPOINT = new PacketType(PROTOCOL, SENDER, 0x8A, "TrackedWaypoint");
public static final PacketType CLEAR_DIALOG = new PacketType(PROTOCOL, SENDER, 0x8B, "ClearDialog");
public static final PacketType SHOW_DIALOG = new PacketType(PROTOCOL, SENDER, 0x8C, "ShowDialog");
public static final PacketType ADD_TRANSIENT_BLOCK = new PacketType(PROTOCOL, SENDER, 0x25, MinecraftVersion.v26_3, "AddTransientBlock");
public static final PacketType UNLOAD_CHUNK = new PacketType(PROTOCOL, SENDER, 0x26, MinecraftVersion.v26_3, "ForgetLevelChunk", "UnloadChunk", "SPacketUnloadChunk");
public static final PacketType GAME_STATE_CHANGE = new PacketType(PROTOCOL, SENDER, 0x27, MinecraftVersion.v26_3, "GameEvent", "GameStateChange", "SPacketChangeGameState");
public static final PacketType GAME_RULE_VALUES = new PacketType(PROTOCOL, SENDER, 0x28, MinecraftVersion.v26_3, "GameRuleValues");
public static final PacketType GAME_TEST_HIGHLIGHT_POS = new PacketType(PROTOCOL, SENDER, 0x29, MinecraftVersion.v26_3, "GameTestHighlightPos");
public static final PacketType OPEN_WINDOW_HORSE = new PacketType(PROTOCOL, SENDER, 0x2A, MinecraftVersion.v26_3, "MountScreenOpen", "HorseScreenOpen", "OpenWindowHorse");
public static final PacketType HURT_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x2B, MinecraftVersion.v26_3, "HurtAnimation", "ClientboundHurtAnimationPacket");
public static final PacketType INITIALIZE_BORDER = new PacketType(PROTOCOL, SENDER, 0x2C, MinecraftVersion.v26_3, "InitializeBorder");
public static final PacketType KEEP_ALIVE = new PacketType(PROTOCOL, SENDER, 0x2D, MinecraftVersion.v26_3, "KeepAlive", "SPacketKeepAlive");
public static final PacketType MAP_CHUNK = new PacketType(PROTOCOL, SENDER, 0x2E, MinecraftVersion.v26_3, "LevelChunkWithLight", "MapChunk", "SPacketChunkData");
public static final PacketType WORLD_EVENT = new PacketType(PROTOCOL, SENDER, 0x2F, MinecraftVersion.v26_3, "LevelEvent", "WorldEvent", "SPacketEffect");
public static final PacketType WORLD_PARTICLES = new PacketType(PROTOCOL, SENDER, 0x30, MinecraftVersion.v26_3, "LevelParticles", "WorldParticles", "SPacketParticles");
public static final PacketType LIGHT_UPDATE = new PacketType(PROTOCOL, SENDER, 0x31, MinecraftVersion.v26_3, "LightUpdate");
public static final PacketType LOGIN = new PacketType(PROTOCOL, SENDER, 0x32, MinecraftVersion.v26_3, "Login", "SPacketJoinGame");
public static final PacketType LOW_DISK_SPACE_WARNING = new PacketType(PROTOCOL, SENDER, 0x33, MinecraftVersion.v26_3, "LowDiskSpaceWarning");
public static final PacketType MAP = new PacketType(PROTOCOL, SENDER, 0x34, MinecraftVersion.v26_3, "MapItemData", "Map", "SPacketMaps");
public static final PacketType OPEN_WINDOW_MERCHANT = new PacketType(PROTOCOL, SENDER, 0x35, MinecraftVersion.v26_3, "MerchantOffers", "OpenWindowMerchant");
public static final PacketType REL_ENTITY_MOVE = new PacketType(PROTOCOL, SENDER, 0x36, MinecraftVersion.v26_3, "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Pos", "Entity$RelEntityMove", "Entity$PacketPlayOutRelEntityMove");
public static final PacketType REL_ENTITY_MOVE_LOOK = new PacketType(PROTOCOL, SENDER, 0x37, MinecraftVersion.v26_3, "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$PosRot", "Entity$RelEntityMoveLook", "Entity$PacketPlayOutRelEntityMoveLook");
public static final PacketType MOVE_MINECART = new PacketType(PROTOCOL, SENDER, 0x38, MinecraftVersion.v26_3, "MoveMinecart");
public static final PacketType ENTITY_LOOK = new PacketType(PROTOCOL, SENDER, 0x39, MinecraftVersion.v26_3, "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Rot", "Entity$EntityLook", "Entity$PacketPlayOutEntityLook");
public static final PacketType VEHICLE_MOVE = new PacketType(PROTOCOL, SENDER, 0x3A, MinecraftVersion.v26_3, "MoveVehicle", "VehicleMove", "SPacketMoveVehicle");
public static final PacketType OPEN_BOOK = new PacketType(PROTOCOL, SENDER, 0x3B, MinecraftVersion.v26_3, "OpenBook");
public static final PacketType OPEN_WINDOW = new PacketType(PROTOCOL, SENDER, 0x3C, MinecraftVersion.v26_3, "OpenScreen", "OpenWindow", "SPacketOpenWindow");
public static final PacketType OPEN_SIGN_EDITOR = new PacketType(PROTOCOL, SENDER, 0x3D, MinecraftVersion.v26_3, "OpenSignEditor", "SPacketSignEditorOpen");
public static final PacketType PING = new PacketType(PROTOCOL, SENDER, 0x3E, MinecraftVersion.v26_3, "Ping");
public static final PacketType PONG_RESPONSE = new PacketType(PROTOCOL, SENDER, 0x3F, MinecraftVersion.v26_3, "PongResponse");
public static final PacketType AUTO_RECIPE = new PacketType(PROTOCOL, SENDER, 0x40, MinecraftVersion.v26_3, "PlaceGhostRecipe", "AutoRecipe", "SPacketPlaceGhostRecipe");
public static final PacketType ABILITIES = new PacketType(PROTOCOL, SENDER, 0x41, MinecraftVersion.v26_3, "PlayerAbilities", "Abilities", "SPacketPlayerAbilities");
public static final PacketType CHAT = new PacketType(PROTOCOL, SENDER, 0x42, MinecraftVersion.v26_3, "PlayerChat", "Chat", "SPacketChat");
public static final PacketType PLAYER_COMBAT_END = new PacketType(PROTOCOL, SENDER, 0x43, MinecraftVersion.v26_3, "PlayerCombatEnd");
public static final PacketType PLAYER_COMBAT_ENTER = new PacketType(PROTOCOL, SENDER, 0x44, MinecraftVersion.v26_3, "PlayerCombatEnter");
public static final PacketType PLAYER_COMBAT_KILL = new PacketType(PROTOCOL, SENDER, 0x45, MinecraftVersion.v26_3, "PlayerCombatKill");
public static final PacketType PLAYER_INFO_REMOVE = new PacketType(PROTOCOL, SENDER, 0x46, MinecraftVersion.v26_3, "PlayerInfoRemove");
public static final PacketType PLAYER_INFO = new PacketType(PROTOCOL, SENDER, 0x47, MinecraftVersion.v26_3, "PlayerInfoUpdate", "PlayerInfo");
public static final PacketType LOOK_AT = new PacketType(PROTOCOL, SENDER, 0x48, MinecraftVersion.v26_3, "PlayerLookAt", "LookAt", "SPacketPlayerPosLook");
public static final PacketType POSITION = new PacketType(PROTOCOL, SENDER, 0x49, MinecraftVersion.v26_3, "PlayerPosition", "Position");
public static final PacketType PLAYER_ROTATION = new PacketType(PROTOCOL, SENDER, 0x4A, MinecraftVersion.v26_3, "PlayerRotation");
public static final PacketType RECIPE_BOOK_ADD = new PacketType(PROTOCOL, SENDER, 0x4B, MinecraftVersion.v26_3, "RecipeBookAdd");
public static final PacketType RECIPE_BOOK_REMOVE = new PacketType(PROTOCOL, SENDER, 0x4C, MinecraftVersion.v26_3, "RecipeBookRemove");
public static final PacketType RECIPE_BOOK_SETTINGS = new PacketType(PROTOCOL, SENDER, 0x4D, MinecraftVersion.v26_3, "RecipeBookSettings");
public static final PacketType ENTITY_DESTROY = new PacketType(PROTOCOL, SENDER, 0x4E, MinecraftVersion.v26_3, "RemoveEntities", "EntityDestroy", "SPacketDestroyEntities");
public static final PacketType REMOVE_ENTITY_EFFECT = new PacketType(PROTOCOL, SENDER, 0x4F, MinecraftVersion.v26_3, "RemoveMobEffect", "RemoveEntityEffect", "SPacketRemoveEntityEffect");
public static final PacketType RESET_SCORE = new PacketType(PROTOCOL, SENDER, 0x50, MinecraftVersion.v26_3, "ResetScore", "ResetScorePacket");
public static final PacketType REMOVE_RESOURCE_PACK = new PacketType(PROTOCOL, SENDER, 0x51, MinecraftVersion.v26_3, "ResourcePackPop", "ResourcePackPopPacket");
public static final PacketType ADD_RESOURCE_PACK = new PacketType(PROTOCOL, SENDER, 0x52, MinecraftVersion.v26_3, "ResourcePackPush", "ResourcePackPushPacket");
public static final PacketType POST_EFFECTS = new PacketType(PROTOCOL, SENDER, 0x53, MinecraftVersion.v26_3, "PostEffects");
public static final PacketType RESPAWN = new PacketType(PROTOCOL, SENDER, 0x54, MinecraftVersion.v26_3, "Respawn", "SPacketRespawn");
public static final PacketType ENTITY_HEAD_ROTATION = new PacketType(PROTOCOL, SENDER, 0x55, MinecraftVersion.v26_3, "RotateHead", "EntityHeadRotation", "SPacketEntityHeadLook");
public static final PacketType MULTI_BLOCK_CHANGE = new PacketType(PROTOCOL, SENDER, 0x56, MinecraftVersion.v26_3, "SectionBlocksUpdate", "MultiBlockChange", "SPacketMultiBlockChange");
public static final PacketType SELECT_ADVANCEMENT_TAB = new PacketType(PROTOCOL, SENDER, 0x57, MinecraftVersion.v26_3, "SelectAdvancementsTab", "SelectAdvancementTab", "SPacketSelectAdvancementsTab");
public static final PacketType SERVER_DATA = new PacketType(PROTOCOL, SENDER, 0x58, MinecraftVersion.v26_3, "ServerData");
public static final PacketType SET_ACTION_BAR_TEXT = new PacketType(PROTOCOL, SENDER, 0x59, MinecraftVersion.v26_3, "SetActionBarText");
public static final PacketType SET_BORDER_CENTER = new PacketType(PROTOCOL, SENDER, 0x5A, MinecraftVersion.v26_3, "SetBorderCenter");
public static final PacketType SET_BORDER_LERP_SIZE = new PacketType(PROTOCOL, SENDER, 0x5B, MinecraftVersion.v26_3, "SetBorderLerpSize");
public static final PacketType SET_BORDER_SIZE = new PacketType(PROTOCOL, SENDER, 0x5C, MinecraftVersion.v26_3, "SetBorderSize");
public static final PacketType SET_BORDER_WARNING_DELAY = new PacketType(PROTOCOL, SENDER, 0x5D, MinecraftVersion.v26_3, "SetBorderWarningDelay");
public static final PacketType SET_BORDER_WARNING_DISTANCE = new PacketType(PROTOCOL, SENDER, 0x5E, MinecraftVersion.v26_3, "SetBorderWarningDistance");
public static final PacketType CAMERA = new PacketType(PROTOCOL, SENDER, 0x5F, MinecraftVersion.v26_3, "SetCamera", "Camera", "SPacketCamera");
public static final PacketType VIEW_CENTRE = new PacketType(PROTOCOL, SENDER, 0x60, MinecraftVersion.v26_3, "SetChunkCacheCenter", "ViewCentre");
public static final PacketType VIEW_DISTANCE = new PacketType(PROTOCOL, SENDER, 0x61, MinecraftVersion.v26_3, "SetChunkCacheRadius", "ViewDistance");
public static final PacketType SET_CURSOR_ITEM = new PacketType(PROTOCOL, SENDER, 0x62, MinecraftVersion.v26_3, "SetCursorItem");
public static final PacketType SPAWN_POSITION = new PacketType(PROTOCOL, SENDER, 0x63, MinecraftVersion.v26_3, "SetDefaultSpawnPosition", "SpawnPosition", "SPacketSpawnPosition");
public static final PacketType SCOREBOARD_DISPLAY_OBJECTIVE = new PacketType(PROTOCOL, SENDER, 0x64, MinecraftVersion.v26_3, "SetDisplayObjective", "ScoreboardDisplayObjective", "SPacketDisplayObjective");
public static final PacketType ENTITY_METADATA = new PacketType(PROTOCOL, SENDER, 0x65, MinecraftVersion.v26_3, "SetEntityData", "EntityMetadata", "SPacketEntityMetadata");
public static final PacketType ATTACH_ENTITY = new PacketType(PROTOCOL, SENDER, 0x66, MinecraftVersion.v26_3, "SetEntityLink", "AttachEntity", "SPacketEntityAttach");
public static final PacketType ENTITY_VELOCITY = new PacketType(PROTOCOL, SENDER, 0x67, MinecraftVersion.v26_3, "SetEntityMotion", "EntityVelocity", "SPacketEntityVelocity");
public static final PacketType ENTITY_EQUIPMENT = new PacketType(PROTOCOL, SENDER, 0x68, MinecraftVersion.v26_3, "SetEquipment", "EntityEquipment", "SPacketEntityEquipment");
public static final PacketType EXPERIENCE = new PacketType(PROTOCOL, SENDER, 0x69, MinecraftVersion.v26_3, "SetExperience", "Experience", "SPacketSetExperience");
public static final PacketType UPDATE_HEALTH = new PacketType(PROTOCOL, SENDER, 0x6A, MinecraftVersion.v26_3, "SetHealth", "UpdateHealth", "SPacketUpdateHealth");
public static final PacketType HELD_ITEM_SLOT = new PacketType(PROTOCOL, SENDER, 0x6B, MinecraftVersion.v26_3, "SetHeldSlot", "SetCarriedItem", "HeldItemSlot", "SPacketHeldItemChange");
public static final PacketType SCOREBOARD_OBJECTIVE = new PacketType(PROTOCOL, SENDER, 0x6C, MinecraftVersion.v26_3, "SetObjective", "ScoreboardObjective", "SPacketScoreboardObjective");
public static final PacketType MOUNT = new PacketType(PROTOCOL, SENDER, 0x6D, MinecraftVersion.v26_3, "SetPassengers", "Mount", "SPacketSetPassengers");
public static final PacketType SET_PLAYER_INVENTORY = new PacketType(PROTOCOL, SENDER, 0x6E, MinecraftVersion.v26_3, "SetPlayerInventory");
public static final PacketType SCOREBOARD_TEAM = new PacketType(PROTOCOL, SENDER, 0x6F, MinecraftVersion.v26_3, "SetPlayerTeam", "ScoreboardTeam", "SPacketTeams");
public static final PacketType SCOREBOARD_SCORE = new PacketType(PROTOCOL, SENDER, 0x70, MinecraftVersion.v26_3, "SetScore", "ScoreboardScore", "SPacketUpdateScore");
public static final PacketType UPDATE_SIMULATION_DISTANCE = new PacketType(PROTOCOL, SENDER, 0x71, MinecraftVersion.v26_3, "SetSimulationDistance");
public static final PacketType SET_SUBTITLE_TEXT = new PacketType(PROTOCOL, SENDER, 0x72, MinecraftVersion.v26_3, "SetSubtitleText");
public static final PacketType UPDATE_TIME = new PacketType(PROTOCOL, SENDER, 0x73, MinecraftVersion.v26_3, "SetTime", "UpdateTime", "SPacketTimeUpdate");
public static final PacketType SET_TITLE_TEXT = new PacketType(PROTOCOL, SENDER, 0x74, MinecraftVersion.v26_3, "SetTitleText");
public static final PacketType SET_TITLES_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x75, MinecraftVersion.v26_3, "SetTitlesAnimation");
public static final PacketType ENTITY_SOUND = new PacketType(PROTOCOL, SENDER, 0x76, MinecraftVersion.v26_3, "SoundEntity", "EntitySound", "SPacketSoundEffect");
public static final PacketType NAMED_SOUND_EFFECT = new PacketType(PROTOCOL, SENDER, 0x77, MinecraftVersion.v26_3, "Sound", "NamedSoundEffect");
public static final PacketType START_CONFIGURATION = new PacketType(PROTOCOL, SENDER, 0x78, MinecraftVersion.v26_3, "StartConfiguration");
public static final PacketType STOP_SOUND = new PacketType(PROTOCOL, SENDER, 0x79, MinecraftVersion.v26_3, "StopSound");
public static final PacketType STORE_COOKIE = new PacketType(PROTOCOL, SENDER, 0x7A, MinecraftVersion.v26_3, "StoreCookie");
public static final PacketType SWING_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x7B, MinecraftVersion.v26_3, "SwingAnimation");
public static final PacketType SYSTEM_CHAT = new PacketType(PROTOCOL, SENDER, 0x7C, MinecraftVersion.v26_3, "SystemChat");
public static final PacketType PLAYER_LIST_HEADER_FOOTER = new PacketType(PROTOCOL, SENDER, 0x7D, MinecraftVersion.v26_3, "TabList", "PlayerListHeaderFooter", "SPacketPlayerListHeaderFooter");
public static final PacketType NBT_QUERY = new PacketType(PROTOCOL, SENDER, 0x7E, MinecraftVersion.v26_3, "TagQuery", "NBTQuery");
public static final PacketType COLLECT = new PacketType(PROTOCOL, SENDER, 0x7F, MinecraftVersion.v26_3, "TakeItemEntity", "Collect", "SPacketCollectItem");
public static final PacketType ENTITY_TELEPORT = new PacketType(PROTOCOL, SENDER, 0x80, MinecraftVersion.v26_3, "TeleportEntity", "EntityTeleport", "SPacketEntityTeleport");
public static final PacketType TEST_INSTANCE_BLOCK_STATUS = new PacketType(PROTOCOL, SENDER, 0x81, MinecraftVersion.v26_3, "ClientboundTestInstanceBlockStatus");
public static final PacketType TICKING_STATE = new PacketType(PROTOCOL, SENDER, 0x82, MinecraftVersion.v26_3, "TickingState", "TickingStatePacket");
public static final PacketType TICKING_STEP_STATE = new PacketType(PROTOCOL, SENDER, 0x83, MinecraftVersion.v26_3, "TickingStep", "TickingStepPacket");
public static final PacketType TRANSFER = new PacketType(PROTOCOL, SENDER, 0x84, MinecraftVersion.v26_3, "Transfer");
public static final PacketType ADVANCEMENTS = new PacketType(PROTOCOL, SENDER, 0x85, MinecraftVersion.v26_3, "UpdateAdvancements", "Advancements", "SPacketAdvancementInfo");
public static final PacketType UPDATE_ATTRIBUTES = new PacketType(PROTOCOL, SENDER, 0x86, MinecraftVersion.v26_3, "UpdateAttributes", "SPacketEntityProperties");
public static final PacketType ENTITY_EFFECT = new PacketType(PROTOCOL, SENDER, 0x87, MinecraftVersion.v26_3, "UpdateMobEffect", "EntityEffect", "SPacketEntityEffect");
public static final PacketType RECIPE_UPDATE = new PacketType(PROTOCOL, SENDER, 0x88, MinecraftVersion.v26_3, "UpdateRecipes", "RecipeUpdate");
public static final PacketType TAGS = new PacketType(PROTOCOL, SENDER, 0x89, MinecraftVersion.v26_3, "UpdateTags", "Tags");
public static final PacketType PROJECTILE_POWER = new PacketType(PROTOCOL, SENDER, 0x8A, MinecraftVersion.v26_3, "ProjectilePower");
public static final PacketType REPORT_DETAILS = new PacketType(PROTOCOL, SENDER, 0x8B, MinecraftVersion.v26_3, "CustomReportDetails");
public static final PacketType SERVER_LINKS = new PacketType(PROTOCOL, SENDER, 0x8C, MinecraftVersion.v26_3, "ServerLinks");
public static final PacketType TRACKED_WAYPOINT = new PacketType(PROTOCOL, SENDER, 0x8D, MinecraftVersion.v26_3, "TrackedWaypoint");
public static final PacketType CLEAR_DIALOG = new PacketType(PROTOCOL, SENDER, 0x8E, MinecraftVersion.v26_3, "ClearDialog");
public static final PacketType SHOW_DIALOG = new PacketType(PROTOCOL, SENDER, 0x8F, MinecraftVersion.v26_3, "ShowDialog");
/**
* @deprecated Removed in 1.9
@@ -490,24 +493,24 @@ public class PacketType implements Serializable, Cloneable, Comparable<PacketTyp
public static final PacketType STEER_VEHICLE = new PacketType(PROTOCOL, SENDER, 0x2B, "PlayerInput", "SteerVehicle", "CPacketInput");
public static final PacketType PLAYER_LOADED = new PacketType(PROTOCOL, SENDER, 0x2C, "PlayerLoaded");
public static final PacketType PONG = new PacketType(PROTOCOL, SENDER, 0x2D, "Pong");
public static final PacketType RECIPE_SETTINGS = new PacketType(PROTOCOL, SENDER, 0x2E, "RecipeBookChangeSettings", "RecipeSettings");
public static final PacketType RECIPE_DISPLAYED = new PacketType(PROTOCOL, SENDER, 0x2F, "RecipeBookSeenRecipe", "RecipeDisplayed", "CPacketRecipeInfo");
public static final PacketType ITEM_NAME = new PacketType(PROTOCOL, SENDER, 0x30, "RenameItem", "ItemName");
public static final PacketType RESOURCE_PACK_STATUS = new PacketType(PROTOCOL, SENDER, 0x31, "ResourcePack", "ResourcePackStatus", "CPacketResourcePackStatus");
public static final PacketType ADVANCEMENTS = new PacketType(PROTOCOL, SENDER, 0x32, "SeenAdvancements", "Advancements", "CPacketSeenAdvancements");
public static final PacketType TR_SEL = new PacketType(PROTOCOL, SENDER, 0x33, "SelectTrade", "TrSel");
public static final PacketType BEACON = new PacketType(PROTOCOL, SENDER, 0x34, "SetBeacon", "Beacon");
public static final PacketType HELD_ITEM_SLOT = new PacketType(PROTOCOL, SENDER, 0x35, "SetCarriedItem", "HeldItemSlot", "CPacketHeldItemChange");
public static final PacketType SET_COMMAND_BLOCK = new PacketType(PROTOCOL, SENDER, 0x36, "SetCommandBlock");
public static final PacketType SET_COMMAND_MINECART = new PacketType(PROTOCOL, SENDER, 0x37, "SetCommandMinecart");
public static final PacketType SET_CREATIVE_SLOT = new PacketType(PROTOCOL, SENDER, 0x38, "SetCreativeModeSlot", "SetCreativeSlot", "CPacketCreativeInventoryAction");
public static final PacketType SET_GAME_RULE = new PacketType(PROTOCOL, SENDER, 0x39, "SetGameRule");
public static final PacketType SET_JIGSAW = new PacketType(PROTOCOL, SENDER, 0x3A, "SetJigsawBlock", "SetJigsaw");
public static final PacketType STRUCT = new PacketType(PROTOCOL, SENDER, 0x3B, "SetStructureBlock", "Struct");
public static final PacketType SET_TEST_BLOCK = new PacketType(PROTOCOL, SENDER, 0x3C, "SetTestBlock");
public static final PacketType UPDATE_SIGN = new PacketType(PROTOCOL, SENDER, 0x3D, "SignUpdate", "UpdateSign", "CPacketUpdateSign");
public static final PacketType SPECTATE_ENTITY = new PacketType(PROTOCOL, SENDER, 0x3E, "SpectatorAction", "SpectateEntity");
public static final PacketType ARM_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x3F, "Swing", "ArmAnimation", "CPacketAnimation");
public static final PacketType PUNCH = new PacketType(PROTOCOL, SENDER, 0x2E, MinecraftVersion.v26_3, "Punch");
public static final PacketType RECIPE_SETTINGS = new PacketType(PROTOCOL, SENDER, 0x2F, MinecraftVersion.v26_3, "RecipeBookChangeSettings", "RecipeSettings");
public static final PacketType RECIPE_DISPLAYED = new PacketType(PROTOCOL, SENDER, 0x30, MinecraftVersion.v26_3, "RecipeBookSeenRecipe", "RecipeDisplayed", "CPacketRecipeInfo");
public static final PacketType ITEM_NAME = new PacketType(PROTOCOL, SENDER, 0x31, MinecraftVersion.v26_3, "RenameItem", "ItemName");
public static final PacketType RESOURCE_PACK_STATUS = new PacketType(PROTOCOL, SENDER, 0x32, MinecraftVersion.v26_3, "ResourcePack", "ResourcePackStatus", "CPacketResourcePackStatus");
public static final PacketType ADVANCEMENTS = new PacketType(PROTOCOL, SENDER, 0x33, MinecraftVersion.v26_3, "SeenAdvancements", "Advancements", "CPacketSeenAdvancements");
public static final PacketType TR_SEL = new PacketType(PROTOCOL, SENDER, 0x34, MinecraftVersion.v26_3, "SelectTrade", "TrSel");
public static final PacketType BEACON = new PacketType(PROTOCOL, SENDER, 0x35, MinecraftVersion.v26_3, "SetBeacon", "Beacon");
public static final PacketType HELD_ITEM_SLOT = new PacketType(PROTOCOL, SENDER, 0x36, MinecraftVersion.v26_3, "SetCarriedItem", "HeldItemSlot", "CPacketHeldItemChange");
public static final PacketType SET_COMMAND_BLOCK = new PacketType(PROTOCOL, SENDER, 0x37, MinecraftVersion.v26_3, "SetCommandBlock");
public static final PacketType SET_COMMAND_MINECART = new PacketType(PROTOCOL, SENDER, 0x38, MinecraftVersion.v26_3, "SetCommandMinecart");
public static final PacketType SET_CREATIVE_SLOT = new PacketType(PROTOCOL, SENDER, 0x39, MinecraftVersion.v26_3, "SetCreativeModeSlot", "SetCreativeSlot", "CPacketCreativeInventoryAction");
public static final PacketType SET_GAME_RULE = new PacketType(PROTOCOL, SENDER, 0x3A, MinecraftVersion.v26_3, "SetGameRule");
public static final PacketType SET_JIGSAW = new PacketType(PROTOCOL, SENDER, 0x3B, MinecraftVersion.v26_3, "SetJigsawBlock", "SetJigsaw");
public static final PacketType STRUCT = new PacketType(PROTOCOL, SENDER, 0x3C, MinecraftVersion.v26_3, "SetStructureBlock", "Struct");
public static final PacketType SET_TEST_BLOCK = new PacketType(PROTOCOL, SENDER, 0x3D, MinecraftVersion.v26_3, "SetTestBlock");
public static final PacketType UPDATE_SIGN = new PacketType(PROTOCOL, SENDER, 0x3E, MinecraftVersion.v26_3, "SignUpdate", "UpdateSign", "CPacketUpdateSign");
public static final PacketType SPECTATE_ENTITY = new PacketType(PROTOCOL, SENDER, 0x3F, MinecraftVersion.v26_3, "SpectatorAction", "SpectateEntity");
public static final PacketType SPECTATE = new PacketType(PROTOCOL, SENDER, 0x40, "TeleportToEntity", "Spectate", "CPacketSpectate");
public static final PacketType TEST_INSTANCE_BLOCK_ACTION = new PacketType(PROTOCOL, SENDER, 0x41, "TestInstanceBlockAction");
public static final PacketType USE_ITEM_ON = new PacketType(PROTOCOL, SENDER, 0x42, "UseItemOn", "BlockPlace", "CPacketPlayerTryUseItemOnBlock");
@@ -538,6 +541,12 @@ public class PacketType implements Serializable, Cloneable, Comparable<PacketTyp
@Deprecated
public static final PacketType DEBUG_SAMPLE_SUBSCRIPTION = new PacketType(PROTOCOL, SENDER, 0x16, "DebugSampleSubscription");
/**
* @deprecated Removed in 26.3
*/
@Deprecated
public static final PacketType ARM_ANIMATION = new PacketType(PROTOCOL, SENDER, 252, "Swing", "ArmAnimation", "CPacketAnimation");
/**
* @deprecated Renamed to USE_ITEM_ON
*/
@@ -719,16 +728,17 @@ public class PacketType implements Serializable, Cloneable, Comparable<PacketTyp
public static final PacketType REGISTRY_DATA = new PacketType(PROTOCOL, SENDER, 0x07, "RegistryData");
public static final PacketType REMOVE_RESOURCE_PACK = new PacketType(PROTOCOL, SENDER, 0x08, "ResourcePackPop");
public static final PacketType ADD_RESOURCE_PACK = new PacketType(PROTOCOL, SENDER, 0x09, "ResourcePackPush");
public static final PacketType STORE_COOKIE = new PacketType(PROTOCOL, SENDER, 0x0A, "StoreCookie");
public static final PacketType TRANSFER = new PacketType(PROTOCOL, SENDER, 0x0B, "Transfer");
public static final PacketType UPDATE_ENABLED_FEATURES = new PacketType(PROTOCOL, SENDER, 0x0C, "UpdateEnabledFeatures");
public static final PacketType UPDATE_TAGS = new PacketType(PROTOCOL, SENDER, 0x0D, "UpdateTags");
public static final PacketType SELECT_KNOWN_PACKS = new PacketType(PROTOCOL, SENDER, 0x0E, "ClientboundSelectKnownPacks");
public static final PacketType REPORT_DETAILS = new PacketType(PROTOCOL, SENDER, 0x0F, "CustomReportDetails");
public static final PacketType SERVER_LINKS = new PacketType(PROTOCOL, SENDER, 0x10, "ServerLinks");
public static final PacketType CLEAR_DIALOG = new PacketType(PROTOCOL, SENDER, 0x11, "ClearDialog");
public static final PacketType SHOW_DIALOG = new PacketType(PROTOCOL, SENDER, 0x12, "ShowDialog");
public static final PacketType CODE_OF_CONDUCT = new PacketType(PROTOCOL, SENDER, 0x13, "CodeOfConduct");
public static final PacketType POST_EFFECTS = new PacketType(PROTOCOL, SENDER, 0x0A, "PostEffects");
public static final PacketType STORE_COOKIE = new PacketType(PROTOCOL, SENDER, 0x0B, "StoreCookie");
public static final PacketType TRANSFER = new PacketType(PROTOCOL, SENDER, 0x0C, "Transfer");
public static final PacketType UPDATE_ENABLED_FEATURES = new PacketType(PROTOCOL, SENDER, 0x0D, "UpdateEnabledFeatures");
public static final PacketType UPDATE_TAGS = new PacketType(PROTOCOL, SENDER, 0x0E, "UpdateTags");
public static final PacketType SELECT_KNOWN_PACKS = new PacketType(PROTOCOL, SENDER, 0x0F, "ClientboundSelectKnownPacks");
public static final PacketType REPORT_DETAILS = new PacketType(PROTOCOL, SENDER, 0x10, "CustomReportDetails");
public static final PacketType SERVER_LINKS = new PacketType(PROTOCOL, SENDER, 0x11, "ServerLinks");
public static final PacketType CLEAR_DIALOG = new PacketType(PROTOCOL, SENDER, 0x12, "ClearDialog");
public static final PacketType SHOW_DIALOG = new PacketType(PROTOCOL, SENDER, 0x13, "ShowDialog");
public static final PacketType CODE_OF_CONDUCT = new PacketType(PROTOCOL, SENDER, 0x14, "CodeOfConduct");
/**
* @deprecated Removed in 1.20.4: replaced with new packets for removing and sending resource packs
@@ -49,10 +49,11 @@ import java.util.logging.Logger;
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 @@ public class ProtocolLib extends JavaPlugin {
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));
@@ -35,12 +35,12 @@ public class ProtocolLibrary {
/**
* The maximum version ProtocolLib has been tested with.
*/
public static final String MAXIMUM_MINECRAFT_VERSION = "26.2";
public static final String MAXIMUM_MINECRAFT_VERSION = "26.3";
/**
* The date (with ISO 8601 or YYYY-MM-DD) when the most recent version (26.2) was released.
* The date (with ISO 8601 or YYYY-MM-DD) when the most recent version (26.3) was released.
*/
public static final String MINECRAFT_LAST_RELEASE_DATE = "2026-06-16";
public static final String MINECRAFT_LAST_RELEASE_DATE = "2026-09-15";
private static Plugin plugin;
private static ProtocolConfig config;
@@ -0,0 +1,69 @@
/**
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.Plugin;
import com.comphenix.protocol.updater.Updater;
/**
* Callback invoked on the main thread after an asynchronous update check completes.
* <p>
* Extracted as a top-level class (instead of an anonymous inner class) so that
* {@code PaperPluginClassLoader} can always resolve its {@code .class} entry when
* {@link org.bukkit.scheduler.BukkitScheduler#runTask} dispatches it back onto the
* main server thread — the same pattern used by {@code SpigotUpdateRunnable}.
*/
final class UpdateNotifyRunnable implements Runnable {
private final CommandSender sender;
private final boolean command;
private final Updater updater;
private final ProtocolConfig config;
private final Plugin plugin;
private final CommandProtocol owner;
UpdateNotifyRunnable(CommandSender sender, boolean command,
Updater updater, ProtocolConfig config,
Plugin plugin, CommandProtocol owner) {
this.sender = sender;
this.command = command;
this.updater = updater;
this.config = config;
this.plugin = plugin;
this.owner = owner;
}
@Override
public void run() {
if (command) {
sender.sendMessage(ChatColor.YELLOW + "[ProtocolLib] " + updater.getResult());
String remoteVersion = updater.getRemoteVersion();
if (remoteVersion != null) {
sender.sendMessage(ChatColor.YELLOW + "Remote version: " + remoteVersion);
sender.sendMessage(ChatColor.YELLOW + "Current version: " + plugin.getDescription().getVersion());
}
} else if (updater.shouldNotify() || config.isDebug()) {
sender.sendMessage(ChatColor.YELLOW + "[ProtocolLib] " + updater.getResult());
}
updater.removeListener(this);
owner.updateFinished();
}
}
@@ -309,12 +309,10 @@ public class AsyncFilterManager implements AsynchronousManager {
asyncMarker.setQueuedSendingIndex(asyncMarker.getNewSendingIndex());
// The player is only be null when they're logged out,
// so this should be a pretty safe check
Player player = syncPacket.getPlayer();
if (player != null) {
PacketSendingQueue sendingQueue = getSendingQueue(syncPacket);
if (sendingQueue != null) {
// Start the process
getSendingQueue(syncPacket).enqueue(newEvent);
sendingQueue.enqueue(newEvent);
// We know this is occurring on the main thread, so pass TRUE
getProcessingQueue(syncPacket).enqueue(newEvent, true);
@@ -323,12 +321,12 @@ public class AsyncFilterManager implements AsynchronousManager {
@Override
public Set<PacketType> getReceivingTypes() {
return serverProcessingQueue.keySet();
return clientProcessingQueue.keySet();
}
@Override
public Set<PacketType> getSendingTypes() {
return clientProcessingQueue.keySet();
return serverProcessingQueue.keySet();
}
/**
@@ -134,14 +134,18 @@ class PlayerSendingHandler {
* @return The server or client sending queue the packet belongs to.
*/
public PacketSendingQueue getSendingQueue(PacketEvent packet, boolean createNew) {
QueueContainer queues = playerSendingQueues.get(packet.getPlayer());
Player player = packet.getPlayer();
if (player == null)
return null;
QueueContainer queues = playerSendingQueues.get(player);
// Safe concurrent initialization
if (queues == null && createNew) {
final QueueContainer newContainer = new QueueContainer();
// Attempt to map the queue
queues = playerSendingQueues.putIfAbsent(packet.getPlayer(), newContainer);
queues = playerSendingQueues.putIfAbsent(player, newContainer);
if (queues == null) {
queues = newContainer;
@@ -58,6 +58,8 @@ public class IntegerMap<T> {
* @return The old associated value, or NULL.
*/
public T remove(int key) {
if (key < 0 || key >= array.length) return null;
T old = array[key];
array[key] = null;
@@ -190,7 +190,8 @@ public class ConcurrentPlayerMap<TValue> extends AbstractMap<Player, TValue> imp
Preconditions.checkNotNull(player, "player cannot be null");
Object key = keyMethod.apply(player);
Preconditions.checkState(key != null, "Unable to derive a key for the player.");
keyLookup.put(key, player);
return key;
}
@@ -56,6 +56,7 @@ import com.comphenix.protocol.wrappers.WrappedMessageSignature;
import com.comphenix.protocol.wrappers.WrappedNumberFormat;
import com.comphenix.protocol.wrappers.WrappedParticle;
import com.comphenix.protocol.wrappers.WrappedPositionMoveRotation;
import com.comphenix.protocol.wrappers.WrappedPositionPath;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
import com.comphenix.protocol.wrappers.WrappedRegistrable;
@@ -65,6 +66,7 @@ import com.comphenix.protocol.wrappers.WrappedSaltedSignature;
import com.comphenix.protocol.wrappers.WrappedServerPing;
import com.comphenix.protocol.wrappers.WrappedStatistic;
import com.comphenix.protocol.wrappers.WrappedTeamParameters;
import com.comphenix.protocol.wrappers.WrappedVecDelta;
import com.comphenix.protocol.wrappers.WrappedWatchableObject;
import com.comphenix.protocol.wrappers.nbt.NbtBase;
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
@@ -1164,9 +1166,35 @@ public abstract class AbstractStructure {
}
/**
* Since 26.3 the signature fields are optional, an absent signature is read and written as {@code null}.
*
* @return read/writer structure direct access to unsalted signature data for example in chat message (since 1.19.3)
*/
public StructureModifier<WrappedMessageSignature> getMessageSignatures() {
if (MinecraftVersion.v26_3.atOrAbove()) {
EquivalentConverter<WrappedMessageSignature> converter = BukkitConverters.getWrappedMessageSignatureConverter();
return structureModifier.withParamType(
Optional.class,
new EquivalentConverter<WrappedMessageSignature>() {
@Override
public Object getGeneric(WrappedMessageSignature specific) {
return Optional.ofNullable(converter.getGeneric(specific));
}
@Override
public WrappedMessageSignature getSpecific(Object generic) {
return generic == null ? null : ((Optional<?>) generic).map(converter::getSpecific).orElse(null);
}
@Override
public Class<WrappedMessageSignature> getSpecificType() {
return WrappedMessageSignature.class;
}
},
MinecraftReflection.getMessageSignatureClass()
);
}
return structureModifier.withType(
MinecraftReflection.getMessageSignatureClass(),
BukkitConverters.getWrappedMessageSignatureConverter()
@@ -1299,7 +1327,7 @@ public abstract class AbstractStructure {
/**
* Retrieve a read/write structure for {@link WrappedPositionMoveRotation} (available since Minecraft 1.21.2).
* <p>
* This is used in packets such as {@code ENTITY_TELEPORT} and {@code ENTITY_POSITION_SYNC}.
* This is used in packets such as {@code ENTITY_TELEPORT} and {@code ENTITY_POSITION_SYNC} (before 26.3).
*
* @return A modifier for PositionMoveRotation fields.
*/
@@ -1310,6 +1338,34 @@ public abstract class AbstractStructure {
);
}
/**
* Retrieve a read/write structure for {@link WrappedVecDelta} (available since Minecraft 26.3).
* <p>
* This is used in {@code REL_ENTITY_MOVE} and {@code REL_ENTITY_MOVE_LOOK}.
*
* @return A modifier for VecDelta fields.
*/
public StructureModifier<WrappedVecDelta> getVecDeltas() {
return structureModifier.withType(
MinecraftReflection.getVecDeltaClass().orElse(void.class),
WrappedVecDelta.getConverter()
);
}
/**
* Retrieve a read/write structure for {@link WrappedPositionPath} (available since Minecraft 26.3).
* <p>
* This is used in {@code ENTITY_POSITION_SYNC}.
*
* @return A modifier for PositionPath fields.
*/
public StructureModifier<WrappedPositionPath> getPositionPaths() {
return structureModifier.withType(
MinecraftReflection.getPositionPathClass().orElse(void.class),
WrappedPositionPath.getConverter()
);
}
/**
* Represents an equivalent converter for ItemStack arrays.
* @author Kristian
@@ -391,6 +391,8 @@ public class PacketEvent extends EventObject implements Cancellable {
* <li>getPlayer</li>
* <li>getAddress</li>
* <li>getServer</li>
* <li>getName, after the login start packet</li>
* <li>getUniqueId, when supplied by the login protocol</li>
* <li>chat</li>
* <li>sendMessage</li>
* <li>kickPlayer</li>
@@ -22,8 +22,10 @@ import java.io.ObjectInputStream;
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 @@ import org.jetbrains.annotations.Nullable;
*
* @author Kristian
*/
class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
class SerializedOfflinePlayer implements Serializable {
/**
* Generated by Eclipse.
@@ -122,22 +124,18 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
}
}
@Override
public boolean isOp() {
return operator;
}
@Override
public void setOp(boolean operator) {
this.operator = operator;
}
@Override
public Map<String, Object> serialize() {
throw new UnsupportedOperationException();
}
@Override
public Location getBedSpawnLocation() {
return bedSpawnLocation;
}
@@ -152,12 +150,10 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
return lastSeen;
}
@Override
public Location getRespawnLocation() {
return null;
}
@Override
public Location getLocation() {
return null;
}
@@ -221,42 +217,34 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
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 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
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 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
}
}
@Override
public Player getPlayer() {
try {
// Try to get the real player underneath
@@ -349,7 +333,7 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
*/
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 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
}
}
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<? extends Player> setupProxyPlayerConstructor() {
final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods();
final String[] methodNames = new String[offlinePlayerMethods.length];
@@ -426,14 +426,20 @@ public class PacketFilterManager implements ListenerManager, InternalManager {
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();
@@ -35,6 +35,7 @@ import com.comphenix.protocol.reflect.StructureModifier;
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.reflect.cloning.ImmutableDetector;
import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract;
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.comphenix.protocol.reflect.instances.DefaultInstances;
@@ -82,15 +83,26 @@ public class StructureCache {
return newInstance(packetClass);
}
// determineBestCreator can re-enter this cache (legacy 1.8 packet registry), which
// ConcurrentHashMap.computeIfAbsent rejects on Java 9+, so use get/put instead.
private static Optional<Supplier<Object>> creatorFor(Class<?> clazz) {
Optional<Supplier<Object>> creator = CACHED_INSTANCE_CREATORS.get(clazz);
if (creator == null) {
creator = Optional.ofNullable(determineBestCreator(clazz));
Optional<Supplier<Object>> existing = CACHED_INSTANCE_CREATORS.putIfAbsent(clazz, creator);
if (existing != null) {
creator = existing;
}
}
return creator;
}
public static boolean canCreateInstance(Class<?> clazz) {
Optional<Supplier<Object>> creator = CACHED_INSTANCE_CREATORS.computeIfAbsent(clazz, x ->
Optional.ofNullable(determineBestCreator(clazz)));
return creator.isPresent();
return creatorFor(clazz).isPresent();
}
public static Object newInstance(Class<?> clazz) {
Optional<Supplier<Object>> creator = CACHED_INSTANCE_CREATORS.computeIfAbsent(clazz, x ->
Optional.ofNullable(determineBestCreator(clazz)));
Optional<Supplier<Object>> creator = creatorFor(clazz);
if (!creator.isPresent()) {
throw new IllegalArgumentException("Cannot create instance of " + clazz);
}
@@ -98,15 +110,21 @@ public class StructureCache {
}
static Supplier<Object> determineBestCreator(Class<?> clazz) {
// certain packets are singletons which can't really be created
if (MinecraftReflection.isPacketClass(clazz)) {
boolean packetClass = MinecraftReflection.isPacketClass(clazz);
boolean immutableClass = ImmutableDetector.isImmutable(clazz);
// Certain packets are singletons which can't really be created. Immutable classes may also expose multiple
// constant instances as an enum replacement, which should be reused instead of invoking their constructors.
if (packetClass || immutableClass) {
FuzzyReflection fuzzy = FuzzyReflection.fromClass(clazz, false);
List<Field> singletons = fuzzy.getFieldList(FuzzyFieldContract.newBuilder()
.typeExact(clazz)
.requireModifier(Modifier.STATIC)
.requireModifier(Modifier.PUBLIC)
.build());
if (singletons.size() == 1) {
// Preserve the existing packet singleton behavior.
if (packetClass && singletons.size() == 1) {
FieldAccessor accessor = Accessors.getFieldAccessor(singletons.get(0));
try {
accessor.get(null);
@@ -114,6 +132,23 @@ public class StructureCache {
} catch (Exception ignored) {
}
}
if (immutableClass) {
for (Field singleton : singletons) {
if (!Modifier.isFinal(singleton.getModifiers())) {
continue;
}
FieldAccessor accessor = Accessors.getFieldAccessor(singleton);
try {
Object instance = accessor.get(null);
if (instance != null) {
return () -> instance;
}
} catch (Exception ignored) {
}
}
}
}
try {
@@ -124,7 +159,10 @@ public class StructureCache {
} catch (Exception ignored) {
}
WrappedStreamCodec streamCodec = PacketRegistry.getStreamCodec(clazz);
// stream codecs only exist on 1.20.5+; on legacy versions asking the registry re-enters its own
// initialization (createOldRegister builds StructureModifiers, which land back here)
WrappedStreamCodec streamCodec = MinecraftVersion.v1_20_5.atOrAbove()
? PacketRegistry.getStreamCodec(clazz) : null;
// use the new stream codec for versions above 1.20.5 if possible
if (streamCodec != null && tryInitTrickDataSerializer()) {
@@ -13,6 +13,7 @@ import com.comphenix.protocol.events.ListeningWhitelist;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.wrappers.Converters;
public class OutboundPacketListenerSet extends PacketListenerSet {
@@ -31,36 +32,42 @@ public class OutboundPacketListenerSet extends PacketListenerSet {
if (event.getPacketType() == PacketType.Play.Server.BUNDLE && !event.isCancelled()) {
// unpack the bundle and invoke for each packet in the bundle
Iterable<PacketContainer> packets = event.getPacket().getPacketBundles().read(0);
List<PacketContainer> outPackets = new ArrayList<>();
for (PacketContainer subPacket : packets) {
// ignore null packets as the will throw an error in the packet encoder
if (subPacket == null) {
continue;
List<PacketContainer> packets = Converters.toList(event.getPacket().getPacketBundles().read(0));
// lazily allocated when the first packet is removed or replaced
List<PacketContainer> outPackets = null;
for (int index = 0; index < packets.size(); index++) {
PacketContainer subPacket = packets.get(index);
PacketContainer packet = null;
if (subPacket != null) {
PacketEvent subPacketEvent = PacketEvent.fromServer(this, subPacket, event.getNetworkMarker(),
event.getPlayer(), event.isFiltered(), event);
super.invoke(subPacketEvent, priorityFilter);
if (!subPacketEvent.isCancelled()) {
PacketContainer result = subPacketEvent.getPacket();
if (result != null && result.getHandle() != null) {
packet = result;
}
}
}
PacketEvent subPacketEvent = PacketEvent.fromServer(this, subPacket, event.getNetworkMarker(),
event.getPlayer());
super.invoke(subPacketEvent, priorityFilter);
// if the packet has been cancelled, the packet will not be add to the bundle
if (subPacketEvent.isCancelled()) {
continue;
boolean changed = packet == null || packet.getHandle() != subPacket.getHandle();
if (outPackets == null && changed) {
outPackets = new ArrayList<>(packets.size());
outPackets.addAll(packets.subList(0, index));
}
PacketContainer packet = subPacketEvent.getPacket();
if (packet == null || packet.getHandle() == null) {
// super.invoke() should prevent us from getting new null packet so we just ignore it here
continue;
} else {
if (outPackets != null && packet != null) {
outPackets.add(packet);
}
}
if (!event.isReadOnly()) {
if (packets.iterator().hasNext()) {
event.getPacket().getPacketBundles().write(0, outPackets);
if (!event.isReadOnly() && outPackets != null) {
if (!outPackets.isEmpty()) {
// the original bundle may be shared between multiple recipients
PacketContainer bundle = event.getPacket().shallowClone();
bundle.getPacketBundles().write(0, outPackets);
event.setPacket(bundle);
} else {
// cancel entire packet if each individual packet has been cancelled
event.setCancelled(true);
@@ -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.
*
@@ -19,10 +19,13 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.utility.MinecraftMethods;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.utility.StreamSerializer;
import com.comphenix.protocol.wrappers.CustomPacketPayloadWrapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
@@ -81,6 +84,11 @@ public class WirePacket {
public static byte[] bytesFromPacket(PacketContainer packet) {
checkNotNull(packet, "packet cannot be null!");
// packets are written by their stream codec since 1.20.5
if (MinecraftVersion.v1_20_5.atOrAbove()) {
return bytesFromStreamCodec(packet);
}
ByteBuf buffer = PacketContainer.createPacketBuffer();
ByteBuf store = PacketContainer.createPacketBuffer();
@@ -107,6 +115,44 @@ public class WirePacket {
return bytes;
}
private static byte[] bytesFromStreamCodec(PacketContainer packet) {
// Spigot keeps the data of unknown custom payloads in a buffer and the codec reads from it,
// so the reader index is restored to keep the packet writable after this call
ByteBuf payloadData = getCustomPayloadData(packet);
int readerIndex = payloadData != null ? payloadData.readerIndex() : 0;
try {
ByteBuf buffer = (ByteBuf) packet.serializeToBuffer();
return StreamSerializer.getDefault().getBytesAndRelease(buffer);
} finally {
if (payloadData != null) {
payloadData.readerIndex(readerIndex);
}
}
}
private static ByteBuf getCustomPayloadData(PacketContainer packet) {
PacketType type = packet.getType();
if (type != PacketType.Play.Server.CUSTOM_PAYLOAD
&& type != PacketType.Play.Client.CUSTOM_PAYLOAD
&& type != PacketType.Configuration.Server.CUSTOM_PAYLOAD
&& type != PacketType.Configuration.Client.CUSTOM_PAYLOAD) {
return null;
}
Object payload = packet.getModifier()
.withType(CustomPacketPayloadWrapper.getCustomPacketPayloadClass())
.readSafely(0);
if (payload == null) {
return null;
}
StructureModifier<ByteBuf> buffers = new StructureModifier<>(payload.getClass())
.withTarget(payload)
.withType(ByteBuf.class);
return buffers.readSafely(0);
}
/**
* Creates a WirePacket from an existing Minecraft packet
*
@@ -118,6 +164,10 @@ public class WirePacket {
checkNotNull(packet, "packet cannot be null!");
checkArgument(MinecraftReflection.isPacketClass(packet), "packet must be a Minecraft packet");
if (MinecraftVersion.v1_20_5.atOrAbove()) {
return fromPacket(PacketContainer.fromPacket(packet));
}
ByteBuf buffer = PacketContainer.createPacketBuffer();
MethodAccessor write = MinecraftMethods.getPacketWriteByteBufMethod();
@@ -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 @@ final class EmptyInjector implements Injector {
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;
@@ -4,7 +4,9 @@ import java.lang.reflect.Field;
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.netty.WirePacket;
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 @@ public class NettyChannelInjector implements Injector {
// 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 class NettyChannelInjector implements Injector {
) {
// 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 class NettyChannelInjector implements Injector {
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 @@ public class NettyChannelInjector implements Injector {
// 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 @@ public class NettyChannelInjector implements Injector {
}
}
private static UUID readLoginUniqueId(PacketContainer packet) {
UUID uniqueId = packet.getUUIDs().readSafely(0);
if (uniqueId != null) {
return uniqueId;
}
StructureModifier<Optional> 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
@@ -516,15 +556,16 @@ public class NettyChannelInjector implements Injector {
return action;
}
PacketContainer packetContainer = new PacketContainer(packetType, packet);
// ensure that we are on the main thread if we need to
if (this.listenerManager.hasMainThreadListener(packetType) && !Bukkit.isPrimaryThread()) {
if (!Bukkit.isPrimaryThread() && hasMainThreadListener(this.listenerManager, protocol, packetContainer)) {
// not on the main thread but we are required to be - re-schedule the packet on the main thread
ProtocolLibrary.getScheduler().runTask(() -> this.sendClientboundPacket(packet, null, true));
ProtocolLibrary.getScheduler().runTask(() -> this.sendClientboundPacket(packet, marker, true));
return null;
}
// create event and invoke listeners
PacketContainer packetContainer = new PacketContainer(packetType, packet);
PacketEvent event = PacketEvent.fromServer(this, packetContainer, marker, this.player);
this.listenerManager.invokeOutboundPacketListeners(event);
@@ -554,6 +595,44 @@ public class NettyChannelInjector implements Injector {
return null;
}
/**
* Checks if a listener that must be called on the main thread is going to be invoked for the given outbound packet.
* The packets inside a bundle are posted to their listeners together with the bundle, so their listeners count for
* the bundle as well.
*
* @param listenerManager the listener manager holding the registered listeners.
* @param protocol the current outbound protocol of the connection.
* @param packet the outbound packet.
* @return true if the packet must be processed on the main thread, false otherwise.
*/
static boolean hasMainThreadListener(ListenerManager listenerManager, Protocol protocol, PacketContainer packet) {
if (listenerManager.hasMainThreadListener(packet.getType())) {
return true;
}
if (packet.getType() != PacketType.Play.Server.BUNDLE) {
return false;
}
Iterable<?> subPackets = packet.getModifier().<Iterable<?>>withType(Iterable.class).readSafely(0);
if (subPackets == null) {
return false;
}
for (Object subPacket : subPackets) {
if (subPacket == null) {
continue;
}
PacketType subPacketType = PacketRegistry.getPacketType(protocol, subPacket.getClass());
if (subPacketType != null && listenerManager.hasMainThreadListener(subPacketType)) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
private <T> T proxyAction(T action, PacketEvent event, NetworkMarker marker) {
// hack - we only know that the given action is either a runnable or callable, but we need to work out which thing
@@ -15,6 +15,7 @@ import com.comphenix.protocol.error.ErrorReporter;
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,7 +24,9 @@ import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.Pair;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
public class NetworkManagerInjector {
@@ -36,7 +39,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 +52,35 @@ public class NetworkManagerInjector {
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<Method> serverConnectionGetter = server.getMethodList(FuzzyMethodContract.newBuilder()
@@ -125,6 +143,23 @@ public class NetworkManagerInjector {
this.injected = true;
}
private void injectChannel(Channel channel) {
addInjectionHandler(channel, MinecraftReflection.getNetworkManagerClass(), this.injectionHandler);
}
static void addInjectionHandler(Channel channel, Class<?> networkManagerClass, ChannelHandler injectionHandler) {
for (String handlerName : channel.pipeline().names()) {
ChannelHandler handler = channel.pipeline().get(handlerName);
if (networkManagerClass.isInstance(handler)) {
channel.pipeline().addBefore(handlerName, INBOUND_INJECT_HANDLER_NAME, injectionHandler);
return;
}
}
// Preserve the existing behavior for forks with an unexpected pipeline layout.
channel.pipeline().addLast(INBOUND_INJECT_HANDLER_NAME, injectionHandler);
}
public void close() {
if (this.closed || !this.injected) {
return;
@@ -153,6 +188,13 @@ public class NetworkManagerInjector {
// clear up
this.overriddenLists.clear();
this.injectionFactory.close();
try {
if (this.paperChannelInitializerCleanup != null) {
this.paperChannelInitializerCleanup.run();
}
} finally {
this.paperChannelInitializerCleanup = null;
this.injectionFactory.close();
}
}
}
@@ -19,6 +19,7 @@ package com.comphenix.protocol.injector.temporary;
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 @@ public class TemporaryPlayerFactory {
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 @@ public class TemporaryPlayerFactory {
* <li>getPlayer()</li>
* <li>getAddress()</li>
* <li>getServer()</li>
* <li>getName()</li>
* <li>getUniqueId(), when supplied by the login protocol</li>
* <li>chat(String)</li>
* <li>sendMessage(String)</li>
* <li>sendMessage(String[])</li>
* <li>kickPlayer(String)</li>
* </ul>
* <p>
* 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.
*/
@@ -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<Channel> 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() {
}
}
}
@@ -90,6 +90,8 @@ public class StructureModifier<T> {
protected Map<FieldAccessor, Integer> defaultFields;
// Cache of previous types
protected Map<Class<?>, StructureModifier<?>> subtypeCache;
// Cache of previous parameterized types
private Map<List<Type>, StructureModifier<?>> paramTypeCache = new HashMap<>();
// Whether or subclasses should handle conversion
protected boolean customConvertHandling;
@@ -201,6 +203,7 @@ public class StructureModifier<T> {
other.defaultFields,
other.converter,
other.subtypeCache);
this.paramTypeCache = other.paramTypeCache;
}
/**
@@ -531,8 +534,18 @@ public class StructureModifier<T> {
return (StructureModifier<R>) NO_OP_MODIFIER;
}
// parameterized lookups must not share a cache entry with the raw field type
List<Type> paramTypeKey = null;
if (paramTypes.length > 0) {
paramTypeKey = new ArrayList<>(paramTypes.length + 1);
paramTypeKey.add(fieldType);
paramTypeKey.addAll(Arrays.asList(paramTypes));
}
// Do we need to update the cache?
StructureModifier<R> result = (StructureModifier<R>) this.subtypeCache.get(fieldType);
StructureModifier<R> result = (StructureModifier<R>) (paramTypeKey == null
? this.subtypeCache.get(fieldType)
: this.paramTypeCache.get(paramTypeKey));
if (result == null) {
List<FieldAccessor> fields = new ArrayList<>();
Map<FieldAccessor, Integer> defaults = new HashMap<>();
@@ -571,7 +584,11 @@ public class StructureModifier<T> {
// Cache structure modifiers
result = this.withFieldType(fieldType, fields, defaults);
this.subtypeCache.put(fieldType, result);
if (paramTypeKey == null) {
this.subtypeCache.put(fieldType, result);
} else {
this.paramTypeCache.put(paramTypeKey, result);
}
}
// Add the target too
@@ -63,6 +63,7 @@ public class ImmutableDetector implements Cloner {
add(MinecraftReflection::getBlockClass);
add(MinecraftReflection::getItemClass);
add(MinecraftReflection::getSoundEffectClass);
add(MinecraftReflection::getWorldTypeClass);
if (MinecraftVersion.AQUATIC_UPDATE.atOrAbove()) {
add(MinecraftReflection::getFluidTypeClass);
@@ -0,0 +1,58 @@
/**
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2015 dmulloy2
* <p>
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.updater;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.error.Report;
/**
* Update check worker (top-level class so the runtime always loads a single {@code .class} entry).
*/
final class SpigotUpdateRunnable implements Runnable {
private final SpigotUpdater updater;
SpigotUpdateRunnable(SpigotUpdater updater) {
this.updater = updater;
}
@Override
public void run() {
try {
String version = this.updater.getSpigotVersion();
this.updater.setRemoteVersion(version);
if (this.updater.versionCheck(version)) {
this.updater.result = Updater.UpdateResult.SPIGOT_UPDATE_AVAILABLE;
} else {
this.updater.result = Updater.UpdateResult.NO_UPDATE;
}
} catch (Throwable ex) {
if (ProtocolLibrary.getConfig().isDebug()) {
ProtocolLibrary.getErrorReporter().reportDetailed(
this.updater,
Report.newBuilder(Updater.REPORT_CANNOT_UPDATE_PLUGIN).error(ex).callerParam(this));
}
ProtocolLibrary.disableUpdates();
} finally {
for (Runnable listener : this.updater.listeners) {
ProtocolLibrary.getScheduler().runTask(listener);
}
}
}
}
@@ -16,8 +16,6 @@
*/
package com.comphenix.protocol.updater;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.error.Report;
import com.comphenix.protocol.utility.Closer;
import org.bukkit.plugin.Plugin;
@@ -44,7 +42,7 @@ public final class SpigotUpdater extends Updater {
waitForThread();
this.type = type;
this.thread = new Thread(new SpigotUpdateRunnable());
this.thread = new Thread(new SpigotUpdateRunnable(this));
this.thread.start();
}
@@ -54,36 +52,8 @@ public final class SpigotUpdater extends Updater {
return String.format(result.toString(), remoteVersion, plugin.getDescription().getVersion(), RESOURCE_URL);
}
private class SpigotUpdateRunnable implements Runnable {
@Override
public void run() {
try {
String version = getSpigotVersion();
remoteVersion = version;
if (versionCheck(version)) {
result = UpdateResult.SPIGOT_UPDATE_AVAILABLE;
} else {
result = UpdateResult.NO_UPDATE;
}
} catch (Throwable ex) {
if (ProtocolLibrary.getConfig().isDebug()) {
ProtocolLibrary.getErrorReporter().reportDetailed(
SpigotUpdater.this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(ex).callerParam(this));
} else {
// People don't care
// plugin.getLogger().log(Level.WARNING, "Failed to check for updates: " + ex);
}
ProtocolLibrary.disableUpdates();
} finally {
// Invoke the listeners on the main thread
for (Runnable listener : listeners) {
ProtocolLibrary.getScheduler().runTask(listener);
}
}
}
void setRemoteVersion(String remoteVersion) {
this.remoteVersion = remoteVersion;
}
private static final String RESOURCE_URL = "https://www.spigotmc.org/resources/protocollib.1997/";
@@ -57,21 +57,25 @@ public final class MinecraftMethods {
}
public static Function<ByteBuf, Object> getFriendlyBufBufConstructor() {
if (friendlyBufBufConstructor == null) {
Optional<Class<?>> 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<Class<?>> 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;
}
/**
@@ -101,6 +101,7 @@ public final class MinecraftProtocolVersion {
map.put(new MinecraftVersion(26, 1, 0), 775);
map.put(new MinecraftVersion(26, 2, 0), 776);
map.put(new MinecraftVersion(26, 3, 0), 777);
return map;
}
@@ -928,6 +928,10 @@ public final class MinecraftReflection {
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");
}
@@ -1426,8 +1430,13 @@ public final class MinecraftReflection {
try {
Method asMirrorMethod = getCraftItemStackClass().getMethod("asCraftMirror", getItemStackClass());
asCraftMirror = Accessors.getMethodAccessor(asMirrorMethod);
} catch (ReflectiveOperationException ex) {
throw new RuntimeException("Failed to obtain CraftItemStack.asCraftMirror", ex);
} catch (ReflectiveOperationException ex1) {
try {
Method asMirrorMethod = getCraftItemStackClass().getMethod("asBukkitMirror", getItemStackClass());
asCraftMirror = Accessors.getMethodAccessor(asMirrorMethod);
} catch (ReflectiveOperationException ex2) {
throw new RuntimeException("Failed to obtain CraftItemStack.asCraftMirror", ex2);
}
}
}
@@ -1933,4 +1942,22 @@ public final class MinecraftReflection {
public static Class<?> getPositionMoveRotationClass() {
return getMinecraftClass("world.entity.PositionMoveRotation");
}
/**
* Retrieves the VecDelta class (since 26.3).
*
* @return The VecDelta class.
*/
public static Optional<Class<?>> getVecDeltaClass() {
return getOptionalNMS("network.protocol.game.VecDelta");
}
/**
* Retrieves the PositionPath class (since 26.3).
*
* @return The PositionPath class.
*/
public static Optional<Class<?>> getPositionPathClass() {
return getOptionalNMS("world.entity.PositionPath");
}
}
@@ -36,6 +36,11 @@ import org.bukkit.Server;
* @author Kristian
*/
public final class MinecraftVersion implements Comparable<MinecraftVersion>, Serializable {
/**
* Version 26.3 - wilderness bound
*/
public static final MinecraftVersion v26_3 = new MinecraftVersion("26.3");
/**
* Version 26.2 - chaos cubed
*/
@@ -194,7 +199,7 @@ public final class MinecraftVersion implements Comparable<MinecraftVersion>, Ser
/**
* The latest release version of minecraft.
*/
public static final MinecraftVersion LATEST = v26_2;
public static final MinecraftVersion LATEST = v26_3;
// used when serializing
private static final long serialVersionUID = -8695133558996459770L;
@@ -326,15 +326,9 @@ public class StreamSerializer {
public byte[] getBytesAndRelease(ByteBuf buf) {
try {
if (buf.hasArray()) {
// heap buffer, we can access the array directly
return buf.array();
} else {
// direct buffer, we need to copy the bytes into an array
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
return bytes;
}
byte[] bytes = new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), bytes);
return bytes;
} finally {
ReferenceCountUtil.safeRelease(buf);
}
@@ -1436,6 +1436,142 @@ public class BukkitConverters {
});
}
// 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<Object, Object> 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<org.bukkit.damage.DamageType> getDamageTypeConverter() {
return ignoreNull(new EquivalentConverter<org.bukkit.damage.DamageType>() {
@Override
public Class<org.bukkit.damage.DamageType> 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<Dimension> dimensionConverter;
private static FauxEnumConverter<DimensionImpl> dimensionImplConverter;
@@ -18,6 +18,8 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
@@ -33,8 +35,8 @@ import net.bytebuddy.matcher.ElementMatchers;
* 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.
* <p>
* 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 either retained raw byte data, such as {@code DiscardedPayload},
* or a writable serialization method on the concrete payload type.
*
* @author Pasqual Koschmieder
*/
@@ -46,7 +48,16 @@ public final class CustomPacketPayloadWrapper {
private static final ConstructorAccessor PAYLOAD_WRAPPER_CONSTRUCTOR;
private static final MethodAccessor GET_ID_PAYLOAD_METHOD;
private static final MethodAccessor GET_ID_HOLDER_PAYLOAD_METHOD;
private static final MethodAccessor GET_ID_PAYLOAD_HOLDER_METHOD;
private static final MethodAccessor SERIALIZE_PAYLOAD_METHOD;
private static final Class<?> PAYLOAD_SERIALIZER_CLASS;
private static final ConcurrentHashMap<Class<?>, Optional<MethodAccessor>> SERIALIZE_PAYLOAD_METHODS =
new ConcurrentHashMap<>();
/** True when running on the nested-ID API 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<CustomPacketPayloadWrapper> CONVERTER;
@@ -55,22 +66,73 @@ 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 getPayloadId = null;
Method getPayloadIdHolder = null;
Method getPayloadIdFromHolder = 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) {
// Paper 26: the payload exposes its identifier through one intermediate record.
Method[] accessors = findChainedIdAccessors(CUSTOM_PACKET_PAYLOAD_CLASS, MINECRAFT_KEY_CLASS);
getPayloadIdHolder = accessors[0];
getPayloadIdFromHolder = accessors[1];
useDiscardedPayload = true;
}
GET_ID_PAYLOAD_METHOD = getPayloadId != null ? Accessors.getMethodAccessor(getPayloadId) : null;
GET_ID_HOLDER_PAYLOAD_METHOD = getPayloadIdHolder != null
? Accessors.getMethodAccessor(getPayloadIdHolder) : null;
GET_ID_PAYLOAD_HOLDER_METHOD = getPayloadIdFromHolder != null
? Accessors.getMethodAccessor(getPayloadIdFromHolder) : null;
USE_DISCARDED_PAYLOAD = useDiscardedPayload;
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);
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) {
// Paper 26 declares the write method on each concrete payload instead.
}
SERIALIZE_PAYLOAD_METHOD = serializePayloadData != null
? Accessors.getMethodAccessor(serializePayloadData) : null;
if (serializePayloadData == null) {
ByteBuf buffer = Unpooled.buffer();
try {
PAYLOAD_SERIALIZER_CLASS = MinecraftReflection.getPacketDataSerializer(buffer).getClass();
} finally {
if (buffer.refCnt() > 0) {
buffer.release();
}
}
} else {
PAYLOAD_SERIALIZER_CLASS = null;
}
Constructor<?> payloadWrapperConstructor = makePayloadWrapper();
Constructor<?> payloadWrapperConstructor;
boolean discardedPayloadUsesByteArray = false;
if (useDiscardedPayload) {
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<CustomPacketPayloadWrapper>() {
@@ -94,6 +156,41 @@ public final class CustomPacketPayloadWrapper {
}
}
private static Method[] findChainedIdAccessors(Class<?> payloadClass, Class<?> keyClass) {
Method payloadAccessor = null;
Method keyAccessor = null;
FuzzyMethodContract keyAccessorContract = FuzzyMethodContract.newBuilder()
.banModifier(Modifier.STATIC)
.returnTypeExact(keyClass)
.parameterCount(0)
.build();
for (Method candidate : payloadClass.getMethods()) {
if (Modifier.isStatic(candidate.getModifiers()) || candidate.getParameterCount() != 0) {
continue;
}
Method candidateKeyAccessor;
try {
candidateKeyAccessor = FuzzyReflection.fromClass(candidate.getReturnType(), true)
.getMethod(keyAccessorContract);
} catch (IllegalArgumentException ignored) {
continue;
}
if (payloadAccessor != null) {
throw new IllegalArgumentException("Multiple CustomPacketPayload identifier accessors found");
}
payloadAccessor = candidate;
keyAccessor = candidateKeyAccessor;
}
if (payloadAccessor == null) {
throw new IllegalArgumentException("Unable to find a CustomPacketPayload identifier accessor");
}
return new Method[]{payloadAccessor, keyAccessor};
}
private static Constructor<?> makePayloadWrapper() throws Exception {
return new ByteBuddy()
.subclass(Object.class)
@@ -116,6 +213,64 @@ public final class CustomPacketPayloadWrapper {
.getConstructor(MinecraftReflection.getMinecraftKeyClass(), byte[].class);
}
private static Optional<MethodAccessor> findSerializePayloadMethod(Class<?> payloadClass) {
Method serializeMethod = null;
for (Method candidate : FuzzyReflection.fromClass(payloadClass, true).getMethods()) {
if (Modifier.isStatic(candidate.getModifiers())
|| candidate.getReturnType() != void.class
|| candidate.getParameterCount() != 1) {
continue;
}
Class<?> parameterClass = candidate.getParameterTypes()[0];
if (parameterClass == Object.class || !parameterClass.isAssignableFrom(PAYLOAD_SERIALIZER_CLASS)) {
continue;
}
if (serializeMethod != null) {
throw new IllegalArgumentException("Multiple CustomPacketPayload serialization methods found on "
+ payloadClass.getName());
}
serializeMethod = candidate;
}
if (serializeMethod == null) {
return Optional.empty();
}
serializeMethod.setAccessible(true);
return Optional.of(Accessors.getMethodAccessor(serializeMethod));
}
private static MethodAccessor getSerializePayloadMethod(Object payload) {
if (SERIALIZE_PAYLOAD_METHOD != null) {
return SERIALIZE_PAYLOAD_METHOD;
}
return SERIALIZE_PAYLOAD_METHODS
.computeIfAbsent(payload.getClass(), CustomPacketPayloadWrapper::findSerializePayloadMethod)
.orElse(null);
}
private static byte[] serializePayload(Object payload) {
MethodAccessor serializeMethod = getSerializePayloadMethod(payload);
if (serializeMethod == null) {
throw new UnsupportedOperationException(
"Cannot extract raw custom payload bytes from " + payload.getClass().getName()
+ ": payload has no byte array, ByteBuf, or serialization method");
}
ByteBuf buffer = Unpooled.buffer();
try {
Object serializer = MinecraftReflection.getPacketDataSerializer(buffer);
serializeMethod.invoke(payload, serializer);
byte[] result = new byte[buffer.readableBytes()];
buffer.getBytes(buffer.readerIndex(), result);
return result;
} finally {
if (buffer.refCnt() > 0) {
buffer.release();
}
}
}
// ====== api methods ======
/**
@@ -171,29 +326,39 @@ public final class CustomPacketPayloadWrapper {
* @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<Object> 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(() -> serializePayload(payload));
}
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 idHolder = GET_ID_HOLDER_PAYLOAD_METHOD.invoke(payload);
messageId = GET_ID_PAYLOAD_HOLDER_METHOD.invoke(idHolder);
}
return MinecraftKey.getConverter().getSpecific(messageId);
}
/**
* Get the generic id of the wrapped message id.
*
@@ -230,6 +395,13 @@ public final class CustomPacketPayloadWrapper {
* @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);
}
@@ -187,7 +187,8 @@ public abstract class EnumWrappers {
DROP_ITEM,
RELEASE_USE_ITEM,
SWAP_HELD_ITEMS("SWAP_ITEM_WITH_OFFHAND"),
STAB;
STAB,
CHANGE_DESTROY_DIRECTION;
final String[] aliases;
PlayerDigType(String... aliases) {
@@ -803,7 +804,10 @@ public abstract class EnumWrappers {
PLAYER_DIG_TYPE_CLASS = getEnum(PacketType.Play.Client.BLOCK_DIG.getPacketClass(), 1);
PLAYER_ACTION_CLASS = getEnum(PacketType.Play.Client.ENTITY_ACTION.getPacketClass(), 0);
SCOREBOARD_ACTION_CLASS = getEnum(PacketType.Play.Server.SCOREBOARD_SCORE.getPacketClass(), 0);
PARTICLE_CLASS = getEnum(PacketType.Play.Server.WORLD_PARTICLES.getPacketClass(), 0);
// the particle enum was removed in 1.13, later packets may contain unrelated enums
if (!MinecraftVersion.AQUATIC_UPDATE.atOrAbove()) {
PARTICLE_CLASS = getEnum(PacketType.Play.Server.WORLD_PARTICLES.getPacketClass(), 0);
}
PLAYER_INFO_ACTION_CLASS = getEnum(PacketType.Play.Server.PLAYER_INFO.getPacketClass(), 0);
if (PLAYER_INFO_ACTION_CLASS == null) {
@@ -105,17 +105,22 @@ public class LegacyDataWatcher extends AbstractWrapper implements IDataWatcher {
*/
@Deprecated
public LegacyDataWatcher(List<WrappedWatchableObject> objects) {
this(newHandle(fakeEntity(), objects));
this(newHandle(fakeEntity()));
for (WrappedWatchableObject object : objects) {
WrappedDataWatcherObject watcherObject = MinecraftReflection.watcherObjectExists()
? object.getWatcherObject()
: WrappedDataWatcherObject.fromIndex(object.getIndex());
setObject(watcherObject, object, false);
}
}
private static Object newHandle(Object entity, List<WrappedWatchableObject> objects) {
private static Object newHandle(Object entity) {
if (constructor == null) {
constructor = Accessors.getConstructorAccessor(HANDLE_TYPE, MinecraftReflection.getEntityClass(),
MinecraftReflection.getArrayClass(MinecraftReflection.getDataWatcherItemClass()));
constructor = Accessors.getConstructorAccessor(HANDLE_TYPE, MinecraftReflection.getEntityClass());
}
Object[] genericItems = new Object[0];// (Object[]) ITEMS_CONVERTER.getGeneric(objects);
return constructor.invoke(entity, genericItems);
return constructor.invoke(entity);
}
private static Object fakeEntity() {
@@ -41,23 +41,16 @@ public class WrappedGameProfile extends AbstractWrapper {
private static final FieldAccessor GET_UUID_STRING = Accessors.getFieldAccessorOrNull(
GAME_PROFILE, "id", String.class);
private static MethodAccessor GET_ID;
private static MethodAccessor GET_NAME;
private static MethodAccessor GET_PROPERTIES;
private static MethodAccessor IS_COMPLETE;
private static final MethodAccessor GET_ID = findMethodAccessor(GAME_PROFILE, "id", "getId");
private static final MethodAccessor GET_NAME = findMethodAccessor(GAME_PROFILE, "name", "getName");
private static final MethodAccessor GET_PROPERTIES = findMethodAccessor(
GAME_PROFILE, "properties", "getProperties");
private static final MethodAccessor IS_COMPLETE = findMethodAccessor(
GAME_PROFILE, "complete", "isComplete");
static {
GET_ID = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "getId");
if (GET_ID == null) {
GET_ID = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "id");
GET_NAME = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "name");
GET_PROPERTIES = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "properties");
IS_COMPLETE = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "complete");
} else {
GET_NAME = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "getName");
GET_PROPERTIES = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "getProperties");
IS_COMPLETE = Accessors.getMethodAccessorOrNull(GAME_PROFILE, "isComplete");
}
static MethodAccessor findMethodAccessor(Class<?> type, String methodName, String fallbackName) {
MethodAccessor accessor = Accessors.getMethodAccessorOrNull(type, methodName);
return accessor != null ? accessor : Accessors.getMethodAccessorOrNull(type, fallbackName);
}
// Fetching game profile
@@ -3,8 +3,11 @@ package com.comphenix.protocol.wrappers;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import com.comphenix.protocol.reflect.EquivalentConverter;
import org.jetbrains.annotations.Nullable;
@@ -49,11 +52,19 @@ public final class WrappedLevelChunkData {
static {
FuzzyReflection reflection = FuzzyReflection.fromClass(HANDLE_TYPE, true);
LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR = Accessors.getConstructorAccessor(reflection.getConstructor(FuzzyMethodContract.newBuilder()
.parameterDerivedOf(MinecraftReflection.getPacketDataSerializerClass())
.parameterExactType(int.class)
.parameterExactType(int.class)
.build()));
if (MinecraftVersion.v26_3.atOrAbove()) {
LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR = Accessors.getConstructorAccessor(reflection.getConstructor(FuzzyMethodContract.newBuilder()
.parameterExactType(Map.class)
.parameterExactType(byte[].class)
.parameterExactType(List.class)
.build()));
} else {
LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR = Accessors.getConstructorAccessor(reflection.getConstructor(FuzzyMethodContract.newBuilder()
.parameterDerivedOf(MinecraftReflection.getPacketDataSerializerClass())
.parameterExactType(int.class)
.parameterExactType(int.class)
.build()));
}
BLOCK_ENTITIES_DATA_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder()
.typeExact(List.class)
.build()));
@@ -180,7 +191,12 @@ public final class WrappedLevelChunkData {
*/
@Deprecated
public static ChunkData fromValues(NbtCompound heightmapsTag, byte[] buffer, List<BlockEntityInfo> blockEntityInfo) {
ChunkData data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(StructureCache.newNullDataSerializer(), 0, 0));
ChunkData data;
if (MinecraftVersion.v26_3.atOrAbove()) {
data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(Collections.emptyMap(), new byte[0], Collections.emptyList()));
} else {
data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(StructureCache.newNullDataSerializer(), 0, 0));
}
data.setHeightmapsTag(heightmapsTag);
data.setBuffer(buffer);
@@ -198,7 +214,12 @@ public final class WrappedLevelChunkData {
* @return a newly created wrapper
*/
public static ChunkData fromValues(Map<EnumWrappers.HeightmapType, long[]> heightmaps, byte[] buffer, List<BlockEntityInfo> blockEntityInfo) {
ChunkData data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(StructureCache.newNullDataSerializer(), 0, 0));
ChunkData data;
if (MinecraftVersion.v26_3.atOrAbove()) {
data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(Collections.emptyMap(), new byte[0], Collections.emptyList()));
} else {
data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(StructureCache.newNullDataSerializer(), 0, 0));
}
data.setHeightmaps(heightmaps);
data.setBuffer(buffer);
@@ -433,18 +454,29 @@ public final class WrappedLevelChunkData {
static {
FuzzyReflection reflection = FuzzyReflection.fromClass(HANDLE_TYPE, true);
List<Field> posFields = reflection.getFieldList(FuzzyFieldContract.newBuilder().typeExact(int.class).build());
BLOCK_ENTITY_INFO_CONSTRUCTOR = Accessors.getConstructorAccessor(HANDLE_TYPE, int.class, int.class,
MinecraftReflection.getBlockEntityTypeClass(), MinecraftReflection.getNBTCompoundClass());
PACKED_XZ_ACCESSOR = Accessors.getFieldAccessor(posFields.get(0));
Y_ACCESSOR = Accessors.getFieldAccessor(posFields.get(1));
if (MinecraftVersion.v26_3.atOrAbove()) {
BLOCK_ENTITY_INFO_CONSTRUCTOR = Accessors.getConstructorAccessor(HANDLE_TYPE, byte.class, short.class,
MinecraftReflection.getBlockEntityTypeClass(), Optional.class);
PACKED_XZ_ACCESSOR = new IntegerFieldAccessor(Accessors.getFieldAccessor(HANDLE_TYPE, byte.class, true), Number::byteValue);
Y_ACCESSOR = new IntegerFieldAccessor(Accessors.getFieldAccessor(HANDLE_TYPE, short.class, true), Number::shortValue);
TAG_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder()
.typeExact(Optional.class)
.build()));
} else {
List<Field> posFields = reflection.getFieldList(FuzzyFieldContract.newBuilder().typeExact(int.class).build());
BLOCK_ENTITY_INFO_CONSTRUCTOR = Accessors.getConstructorAccessor(HANDLE_TYPE, int.class, int.class,
MinecraftReflection.getBlockEntityTypeClass(), MinecraftReflection.getNBTCompoundClass());
PACKED_XZ_ACCESSOR = Accessors.getFieldAccessor(posFields.get(0));
Y_ACCESSOR = Accessors.getFieldAccessor(posFields.get(1));
TAG_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder()
.typeExact(MinecraftReflection.getNBTCompoundClass())
.build()));
}
TYPE_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder()
.typeExact(MinecraftReflection.getBlockEntityTypeClass())
.build()));
TAG_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder()
.typeExact(MinecraftReflection.getNBTCompoundClass())
.build()));
}
public BlockEntityInfo(Object handle) {
@@ -533,6 +565,9 @@ public final class WrappedLevelChunkData {
@Nullable
public NbtCompound getAdditionalData() {
Object tagHandle = TAG_ACCESSOR.get(handle);
if (MinecraftVersion.v26_3.atOrAbove()) {
tagHandle = ((Optional<Object>) tagHandle).orElse(null);
}
return tagHandle == null ? null : NbtFactory.fromNMSCompound(tagHandle);
}
@@ -543,7 +578,11 @@ public final class WrappedLevelChunkData {
* @param additionalData the additional data for this block entity, can be {@code null}
*/
public void setAdditionalData(@Nullable NbtCompound additionalData) {
TAG_ACCESSOR.set(handle, additionalData == null ? null : NbtFactory.fromBase(additionalData).getHandle());
if (MinecraftVersion.v26_3.atOrAbove()) {
TAG_ACCESSOR.set(handle, Optional.ofNullable(additionalData).map(data -> NbtFactory.fromBase(data).getHandle()));
} else {
TAG_ACCESSOR.set(handle, additionalData == null ? null : NbtFactory.fromBase(additionalData).getHandle());
}
}
/**
@@ -568,12 +607,46 @@ public final class WrappedLevelChunkData {
* @param additionalData An NBT-Tag containing additional information. Can be {@code null}.
*/
public static BlockEntityInfo fromValues(int sectionX, int sectionZ, int y, MinecraftKey typeKey, @Nullable NbtCompound additionalData) {
return new BlockEntityInfo(BLOCK_ENTITY_INFO_CONSTRUCTOR.invoke(
if (MinecraftVersion.v26_3.atOrAbove()) {
return new BlockEntityInfo(BLOCK_ENTITY_INFO_CONSTRUCTOR.invoke(
(byte) (sectionX << 4 | sectionZ),
(short) y,
REGISTRY.get(typeKey),
Optional.ofNullable(additionalData).map(data -> NbtFactory.fromBase(data).getHandle())
));
} else {
return new BlockEntityInfo(BLOCK_ENTITY_INFO_CONSTRUCTOR.invoke(
sectionX << 4 | sectionZ,
y,
REGISTRY.get(typeKey),
additionalData == null ? null : NbtFactory.fromBase(additionalData).getHandle()
));
));
}
}
}
private final static class IntegerFieldAccessor implements FieldAccessor {
private final FieldAccessor inner;
private final Function<Number, Object> toType;
public IntegerFieldAccessor(FieldAccessor inner, Function<Number, Object> toType) {
this.inner = inner;
this.toType = toType;
}
@Override
public Object get(Object instance) {
return ((Number) this.inner.get(instance)).intValue();
}
@Override
public void set(Object instance, Object value) {
this.inner.set(instance, this.toType.apply((Integer) value));
}
@Override
public Field getField() {
return this.inner.getField();
}
}
}
@@ -27,6 +27,15 @@ public class WrappedParticle<T> {
private static MethodAccessor toNMS;
private static MethodAccessor toCraftData;
private static final class ItemStackTemplateAccessor {
private static final Class<?> TYPE = MinecraftReflection.getMinecraftClass("world.item.ItemStackTemplate");
private static final MethodAccessor CREATE_ITEM_STACK = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(TYPE).getMethod(FuzzyMethodContract.newBuilder()
.returnTypeExact(MinecraftReflection.getItemStackClass())
.parameterCount(0)
.build()));
}
private static void ensureMethods() {
if (toBukkit != null && toNMS != null) {
return;
@@ -176,6 +185,15 @@ public class WrappedParticle<T> {
}
private static Object getItem(Object handle) {
if (MinecraftVersion.v26_1.atOrAbove()) {
Object template = new StructureModifier<>(handle.getClass())
.withTarget(handle)
.withType(ItemStackTemplateAccessor.TYPE)
.read(0);
Object itemStack = ItemStackTemplateAccessor.CREATE_ITEM_STACK.invoke(template);
return BukkitConverters.getItemStackConverter().getSpecific(itemStack);
}
return new StructureModifier<>(handle.getClass())
.withTarget(handle)
.withType(MinecraftReflection.getItemStackClass(), BukkitConverters.getItemStackConverter())
@@ -0,0 +1,207 @@
package com.comphenix.protocol.wrappers;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.google.common.base.Preconditions;
import org.bukkit.util.Vector;
/**
* Wrapper around the NMS {@code PositionPath} type, introduced in Minecraft 26.3.
* <p>
* Used by {@code ENTITY_POSITION_SYNC} to store the entity position. A path is either linear (a single position) or
* stepped (several positions reached at given ticks).
* <pre>{@code
* packet.getPositionPaths().write(0, WrappedPositionPath.linear(new Vector(x, y, z)));
* }</pre>
*/
public final class WrappedPositionPath extends AbstractWrapper {
private static Class<?> STEPPED_CLASS;
private static ConstructorAccessor LINEAR_CONSTRUCTOR;
private static ConstructorAccessor STEPPED_CONSTRUCTOR;
private static ConstructorAccessor STEP_CONSTRUCTOR;
private static StructureModifier<Object> LINEAR_MODIFIER;
private static StructureModifier<Object> STEPPED_MODIFIER;
private static StructureModifier<Object> STEP_MODIFIER;
private static volatile boolean initialized;
private WrappedPositionPath(Object handle) {
super(getPositionPathClass());
setHandle(handle);
}
private static Class<?> getPositionPathClass() {
return MinecraftReflection.getPositionPathClass()
.orElseThrow(() -> new UnsupportedOperationException("PositionPath requires Minecraft 26.3 or newer"));
}
private static void initialize() {
if (initialized) {
return;
}
Class<?> pathClass = getPositionPathClass();
Class<?> vec3Class = MinecraftReflection.getVec3DClass();
Class<?> linearClass = null;
Class<?> stepClass = null;
for (Class<?> nested : pathClass.getDeclaredClasses()) {
if (!pathClass.isAssignableFrom(nested)) {
continue;
}
Field steps = getListField(nested);
if (steps != null) {
STEPPED_CLASS = nested;
stepClass = (Class<?>) ((ParameterizedType) steps.getGenericType()).getActualTypeArguments()[0];
} else {
linearClass = nested;
}
}
Preconditions.checkState(linearClass != null && STEPPED_CLASS != null, "Unable to find PositionPath implementations");
LINEAR_CONSTRUCTOR = Accessors.getConstructorAccessor(linearClass, vec3Class);
STEPPED_CONSTRUCTOR = Accessors.getConstructorAccessor(STEPPED_CLASS, List.class);
STEP_CONSTRUCTOR = Accessors.getConstructorAccessor(stepClass, vec3Class, int.class);
LINEAR_MODIFIER = new StructureModifier<>(linearClass);
STEPPED_MODIFIER = new StructureModifier<>(STEPPED_CLASS);
STEP_MODIFIER = new StructureModifier<>(stepClass);
initialized = true;
}
private static Field getListField(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (List.class.equals(field.getType())) {
return field;
}
}
return null;
}
public static WrappedPositionPath fromHandle(Object handle) {
return new WrappedPositionPath(handle);
}
/**
* Creates a linear path.
*
* @param position - the absolute position.
* @return the path.
*/
public static WrappedPositionPath linear(Vector position) {
Preconditions.checkNotNull(position, "position cannot be NULL");
initialize();
return new WrappedPositionPath(LINEAR_CONSTRUCTOR.invoke(BukkitConverters.getVectorConverter().getGeneric(position)));
}
/**
* Creates a stepped path ending at the position of the last step.
*
* @param steps - the steps in order, must not be empty.
* @return the path.
*/
public static WrappedPositionPath stepped(List<Step> steps) {
Preconditions.checkArgument(steps != null && !steps.isEmpty(), "steps cannot be empty");
initialize();
EquivalentConverter<Vector> vectorConverter = BukkitConverters.getVectorConverter();
List<Object> handles = new ArrayList<>(steps.size());
for (Step step : steps) {
handles.add(STEP_CONSTRUCTOR.invoke(vectorConverter.getGeneric(step.getPosition()), step.getTickOffset()));
}
return new WrappedPositionPath(STEPPED_CONSTRUCTOR.invoke(Collections.unmodifiableList(handles)));
}
public boolean isStepped() {
initialize();
return STEPPED_CLASS.isInstance(handle);
}
/**
* @return the final position of the path.
*/
public Vector getEndPosition() {
StructureModifier<Object> modifier = isStepped() ? STEPPED_MODIFIER : LINEAR_MODIFIER;
return modifier.withTarget(handle)
.withType(MinecraftReflection.getVec3DClass(), BukkitConverters.getVectorConverter())
.read(0);
}
/**
* @return the steps of a stepped path.
* @throws IllegalStateException if the path is linear.
*/
public List<Step> getSteps() {
Preconditions.checkState(isStepped(), "Linear paths have no steps");
List<?> handles = STEPPED_MODIFIER.withTarget(handle).<List<?>>withType(List.class).read(0);
List<Step> steps = new ArrayList<>(handles.size());
for (Object stepHandle : handles) {
StructureModifier<Object> step = STEP_MODIFIER.withTarget(stepHandle);
Vector position = step.withType(MinecraftReflection.getVec3DClass(), BukkitConverters.getVectorConverter()).read(0);
steps.add(new Step(position, step.<Integer>withType(int.class).read(0)));
}
return Collections.unmodifiableList(steps);
}
public static EquivalentConverter<WrappedPositionPath> getConverter() {
return Converters.ignoreNull(Converters.handle(AbstractWrapper::getHandle,
WrappedPositionPath::fromHandle, WrappedPositionPath.class));
}
public static final class Step {
private final Vector position;
private final int tickOffset;
public Step(Vector position, int tickOffset) {
this.position = Preconditions.checkNotNull(position, "position cannot be NULL").clone();
this.tickOffset = tickOffset;
}
public Vector getPosition() {
return position.clone();
}
public int getTickOffset() {
return tickOffset;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Step)) return false;
Step that = (Step) obj;
return tickOffset == that.tickOffset && position.equals(that.position);
}
@Override
public int hashCode() {
return Objects.hash(position, tickOffset);
}
@Override
public String toString() {
return "Step[position=" + position + ", tickOffset=" + tickOffset + "]";
}
}
}
@@ -16,15 +16,26 @@ import com.comphenix.protocol.utility.MinecraftReflection;
public class WrappedStatistic extends AbstractWrapper {
private static final Class<?> STATISTIC = MinecraftReflection.getStatisticClass();
private static final Class<?> STATISTIC_LIST = MinecraftReflection.getStatisticListClass();
private static final FieldAccessor GET_NAME_FIELD;
private static final MethodAccessor GET_NAME_METHOD;
static {
FieldAccessor[] nameFields = Accessors.getFieldAccessorArray(STATISTIC, String.class, true);
if (nameFields.length > 0) {
GET_NAME_FIELD = nameFields[0];
GET_NAME_METHOD = null;
} else {
GET_NAME_FIELD = null;
GET_NAME_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(STATISTIC.getSuperclass(), true)
.getMethodByReturnTypeAndParameters("getName", String.class));
}
try {
FIND_STATISTICS = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(STATISTIC_LIST).getMethodByReturnTypeAndParameters(
"findStatistic", STATISTIC, new Class<?>[]{String.class}));
MAP_ACCESSOR = Accessors.getFieldAccessor(STATISTIC_LIST, Map.class, true);
GET_NAME = Accessors.getFieldAccessor(STATISTIC, String.class, true);
} catch (Exception ex) {
// TODO - find an alternative
}
@@ -32,7 +43,6 @@ public class WrappedStatistic extends AbstractWrapper {
private static MethodAccessor FIND_STATISTICS;
private static FieldAccessor MAP_ACCESSOR;
private static FieldAccessor GET_NAME;
private final String name;
@@ -40,7 +50,9 @@ public class WrappedStatistic extends AbstractWrapper {
super(STATISTIC);
setHandle(handle);
this.name = (String) GET_NAME.get(handle);
this.name = GET_NAME_FIELD != null
? (String) GET_NAME_FIELD.get(handle)
: (String) GET_NAME_METHOD.invoke(handle);
}
/**
@@ -0,0 +1,237 @@
package com.comphenix.protocol.wrappers;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.google.common.base.Preconditions;
/**
* Wrapper around the NMS {@code VecDelta} type, introduced in Minecraft 26.3.
* <p>
* Used by {@code REL_ENTITY_MOVE} and {@code REL_ENTITY_MOVE_LOOK} to store the position change. A delta is either
* linear (a single offset) or stepped (several offsets reached at given ticks). Offsets use the same encoding as the
* short fields of older versions.
* <pre>{@code
* packet.getVecDeltas().write(0, WrappedVecDelta.linear(deltaX, deltaY, deltaZ));
* }</pre>
*/
public final class WrappedVecDelta extends AbstractWrapper {
private static Class<?> STEPPED_CLASS;
private static ConstructorAccessor LINEAR_CONSTRUCTOR;
private static ConstructorAccessor STEPPED_CONSTRUCTOR;
private static ConstructorAccessor STEP_CONSTRUCTOR;
private static StructureModifier<Object> LINEAR_MODIFIER;
private static StructureModifier<Object> STEPPED_MODIFIER;
private static StructureModifier<Object> STEP_MODIFIER;
private static volatile boolean initialized;
private WrappedVecDelta(Object handle) {
super(getVecDeltaClass());
setHandle(handle);
}
private static Class<?> getVecDeltaClass() {
return MinecraftReflection.getVecDeltaClass()
.orElseThrow(() -> new UnsupportedOperationException("VecDelta requires Minecraft 26.3 or newer"));
}
private static void initialize() {
if (initialized) {
return;
}
Class<?> vecDeltaClass = getVecDeltaClass();
Class<?> linearClass = null;
Class<?> stepClass = null;
for (Class<?> nested : vecDeltaClass.getDeclaredClasses()) {
if (!vecDeltaClass.isAssignableFrom(nested)) {
continue;
}
Field steps = getListField(nested);
if (steps != null) {
STEPPED_CLASS = nested;
stepClass = (Class<?>) ((ParameterizedType) steps.getGenericType()).getActualTypeArguments()[0];
} else {
linearClass = nested;
}
}
Preconditions.checkState(linearClass != null && STEPPED_CLASS != null, "Unable to find VecDelta implementations");
LINEAR_CONSTRUCTOR = Accessors.getConstructorAccessor(linearClass, short.class, short.class, short.class);
STEPPED_CONSTRUCTOR = Accessors.getConstructorAccessor(STEPPED_CLASS, List.class);
STEP_CONSTRUCTOR = Accessors.getConstructorAccessor(stepClass, short.class, short.class, short.class, int.class);
LINEAR_MODIFIER = new StructureModifier<>(linearClass);
STEPPED_MODIFIER = new StructureModifier<>(STEPPED_CLASS);
STEP_MODIFIER = new StructureModifier<>(stepClass);
initialized = true;
}
private static Field getListField(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (List.class.equals(field.getType())) {
return field;
}
}
return null;
}
public static WrappedVecDelta fromHandle(Object handle) {
return new WrappedVecDelta(handle);
}
/**
* Creates a linear delta.
*
* @param deltaX - the encoded x offset.
* @param deltaY - the encoded y offset.
* @param deltaZ - the encoded z offset.
* @return the delta.
*/
public static WrappedVecDelta linear(short deltaX, short deltaY, short deltaZ) {
initialize();
return new WrappedVecDelta(LINEAR_CONSTRUCTOR.invoke(deltaX, deltaY, deltaZ));
}
/**
* Creates a stepped delta.
*
* @param steps - the steps in order, must not be empty.
* @return the delta.
*/
public static WrappedVecDelta stepped(List<Step> steps) {
Preconditions.checkArgument(steps != null && !steps.isEmpty(), "steps cannot be empty");
initialize();
List<Object> handles = new ArrayList<>(steps.size());
for (Step step : steps) {
handles.add(STEP_CONSTRUCTOR.invoke(step.getDeltaX(), step.getDeltaY(), step.getDeltaZ(), step.getTicks()));
}
return new WrappedVecDelta(STEPPED_CONSTRUCTOR.invoke(Collections.unmodifiableList(handles)));
}
public boolean isStepped() {
initialize();
return STEPPED_CLASS.isInstance(handle);
}
/**
* @return the encoded x offset of a linear delta.
* @throws IllegalStateException if the delta is stepped.
*/
public short getDeltaX() {
return readLinear(0);
}
/**
* @return the encoded y offset of a linear delta.
* @throws IllegalStateException if the delta is stepped.
*/
public short getDeltaY() {
return readLinear(1);
}
/**
* @return the encoded z offset of a linear delta.
* @throws IllegalStateException if the delta is stepped.
*/
public short getDeltaZ() {
return readLinear(2);
}
private short readLinear(int index) {
Preconditions.checkState(!isStepped(), "Stepped deltas have no single offset");
return LINEAR_MODIFIER.withTarget(handle).<Short>withType(short.class).read(index);
}
/**
* @return the steps of a stepped delta.
* @throws IllegalStateException if the delta is linear.
*/
public List<Step> getSteps() {
Preconditions.checkState(isStepped(), "Linear deltas have no steps");
List<?> handles = STEPPED_MODIFIER.withTarget(handle).<List<?>>withType(List.class).read(0);
List<Step> steps = new ArrayList<>(handles.size());
for (Object stepHandle : handles) {
StructureModifier<Object> step = STEP_MODIFIER.withTarget(stepHandle);
StructureModifier<Short> shorts = step.withType(short.class);
steps.add(new Step(shorts.read(0), shorts.read(1), shorts.read(2), step.<Integer>withType(int.class).read(0)));
}
return Collections.unmodifiableList(steps);
}
public static EquivalentConverter<WrappedVecDelta> getConverter() {
return Converters.ignoreNull(Converters.handle(AbstractWrapper::getHandle,
WrappedVecDelta::fromHandle, WrappedVecDelta.class));
}
public static final class Step {
private final short deltaX;
private final short deltaY;
private final short deltaZ;
private final int ticks;
public Step(short deltaX, short deltaY, short deltaZ, int ticks) {
this.deltaX = deltaX;
this.deltaY = deltaY;
this.deltaZ = deltaZ;
this.ticks = ticks;
}
public short getDeltaX() {
return deltaX;
}
public short getDeltaY() {
return deltaY;
}
public short getDeltaZ() {
return deltaZ;
}
public int getTicks() {
return ticks;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Step)) return false;
Step that = (Step) obj;
return deltaX == that.deltaX && deltaY == that.deltaY && deltaZ == that.deltaZ && ticks == that.ticks;
}
@Override
public int hashCode() {
return Objects.hash(deltaX, deltaY, deltaZ, ticks);
}
@Override
public String toString() {
return "Step[deltaX=" + deltaX + ", deltaY=" + deltaY + ", deltaZ=" + deltaZ + ", ticks=" + ticks + "]";
}
}
}
@@ -3,16 +3,19 @@ package com.comphenix.protocol;
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.core.registries.Registries;
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.LevelBasedPermissionSet;
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,30 +132,69 @@ public class BukkitInitialization {
// 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<RegistryLayer> layeredAccess1 = RegistryLayer.createRegistryAccess();
List<net.minecraft.core.Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(resourceManager, layeredAccess1.getLayer(RegistryLayer.STATIC));
RegistryAccess.Frozen access1 = layeredAccess1.getAccessForLoading(RegistryLayer.WORLDGEN);
RegistryAccess.Frozen access1 = layeredAccess1.getAccessForLoading(RegistryLayer.WORLD);
List<HolderLookup.RegistryLookup<?>> list1 = TagLoader.buildUpdatedLookups(access1, tags);
RegistryAccess.Frozen access2;
try {
access2 = RegistryDataLoader.load(resourceManager, list1, RegistryDataLoader.WORLDGEN_REGISTRIES, Executors.newSingleThreadExecutor()).get();
access2 = RegistryDataLoader.load(
resourceManager,
list1,
RegistryDataLoader.WORLD_REGISTRIES,
Util.backgroundExecutor()).get();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
LayeredRegistryAccess<RegistryLayer> layeredAccess2 = layeredAccess1.replaceFrom(RegistryLayer.WORLDGEN, access2);
LayeredRegistryAccess<RegistryLayer> layeredAccess2 =
layeredAccess1.replaceFrom(RegistryLayer.WORLD, access2);
List<HolderLookup.RegistryLookup<?>> 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 @@ public class BukkitInitialization {
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 @@ public class BukkitInitialization {
});
when(mockedServer.getRegistry(any())).thenAnswer(invocation -> {
Class<Keyed> 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 @@ public class BukkitInitialization {
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 class BukkitInitialization {
List<T> empty = Collections.emptyList();
return empty.stream();
}
@Override
public Stream<NamespacedKey> keyStream() {
return Stream.empty();
}
@Override
public NamespacedKey getKey(T value) {
return null;
}
@Override
public boolean hasTag(io.papermc.paper.registry.tag.TagKey<T> key) {
return false;
}
@Override
public io.papermc.paper.registry.tag.Tag<T> getTag(io.papermc.paper.registry.tag.TagKey<T> key) {
return null;
}
@Override
public java.util.Collection<io.papermc.paper.registry.tag.Tag<T>> getTags() {
return Collections.emptyList();
}
@Override
public int size() {
return 0;
}
}
}
@@ -16,6 +16,7 @@ package com.comphenix.protocol;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -35,6 +36,8 @@ import com.comphenix.protocol.injector.packet.PacketRegistry;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import net.minecraft.network.protocol.common.ClientboundPostEffectsPacket;
import net.minecraft.network.protocol.common.ClientboundStoreCookiePacket;
import net.minecraft.network.protocol.login.ServerboundHelloPacket;
/**
@@ -254,6 +257,27 @@ public class PacketTypeTest {
assertEquals(type, roundTrip);
}
}
@Test
public void testMinecraft263PacketMappings() {
assertEquals(0x25, PacketType.Play.Server.ADD_TRANSIENT_BLOCK.getCurrentId());
assertEquals(0x26, PacketType.Play.Server.UNLOAD_CHUNK.getCurrentId());
assertNotEquals(PacketType.Play.Server.ADD_TRANSIENT_BLOCK, PacketType.Play.Server.UNLOAD_CHUNK);
assertEquals(0x53, PacketType.Play.Server.POST_EFFECTS.getCurrentId());
assertEquals(0x7B, PacketType.Play.Server.SWING_ANIMATION.getCurrentId());
assertEquals(0x2E, PacketType.Play.Client.PUNCH.getCurrentId());
assertNotEquals(PacketType.Play.Client.PUNCH, PacketType.Play.Client.RECIPE_SETTINGS);
}
@Test
public void testMinecraft263ConfigurationPacketMappings() {
assertEquals(0x0A, PacketType.Configuration.Server.POST_EFFECTS.getCurrentId());
assertEquals(ClientboundPostEffectsPacket.class, PacketType.Configuration.Server.POST_EFFECTS.getPacketClass());
assertEquals(0x0B, PacketType.Configuration.Server.STORE_COOKIE.getCurrentId());
assertEquals(ClientboundStoreCookiePacket.class, PacketType.Configuration.Server.STORE_COOKIE.getPacketClass());
assertEquals(0x14, PacketType.Configuration.Server.CODE_OF_CONDUCT.getCurrentId());
}
@Test
public void testPacketCodec() {
@@ -0,0 +1,54 @@
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 com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
class PlatformCommandRegistrarTest {
@Test
void registersLegacyExecutorAsPaperCommand() throws CommandSyntaxException {
ProtocolLib plugin = mock(ProtocolLib.class);
CommandExecutor executor = mock(CommandExecutor.class);
ArgumentCaptor<BasicCommand> 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<Command> 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]");
}
}
@@ -0,0 +1,36 @@
package com.comphenix.protocol.async;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.scheduler.ProtocolScheduler;
import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
class AsyncFilterManagerTest {
@BeforeAll
static void initializeBukkit() {
BukkitInitialization.initializeAll();
}
@Test
void testSendingTypesUseServerProcessingQueue() {
AsyncFilterManager manager = new AsyncFilterManager(
mock(ErrorReporter.class), mock(ProtocolScheduler.class));
manager.setManager(mock(ProtocolManager.class));
manager.registerAsyncHandler(new PacketAdapter(
mock(Plugin.class), PacketType.Play.Server.SYSTEM_CHAT) { }, false);
assertTrue(manager.getSendingTypes().contains(PacketType.Play.Server.SYSTEM_CHAT));
assertFalse(manager.getReceivingTypes().contains(PacketType.Play.Server.SYSTEM_CHAT));
}
}
@@ -5,6 +5,7 @@ import com.comphenix.protocol.utility.IntegerMath;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class IntegerMapTest {
@Test
@@ -21,4 +22,15 @@ class IntegerMapTest {
}
assertEquals(map.size(), 512);
}
}
@Test
public void testRemoveOutsideBounds() {
IntegerMap<Boolean> map = new IntegerMap<>();
map.put(0, true);
assertNull(map.remove(-1));
assertEquals(1, map.size());
assertNull(map.remove(8));
assertEquals(1, map.size());
}
}
@@ -0,0 +1,20 @@
package com.comphenix.protocol.concurrency;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
class ConcurrentPlayerMapTest {
@Test
void testPutIfAbsentWithUnavailableAddress() {
ConcurrentPlayerMap<Object> map = ConcurrentPlayerMap.usingAddress();
Player player = mock(Player.class);
assertThrows(IllegalStateException.class, () -> map.putIfAbsent(player, new Object()));
assertTrue(map.isEmpty());
}
}
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -41,6 +42,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
import org.apache.commons.lang.SerializationUtils;
import org.bukkit.ChatColor;
@@ -70,6 +73,7 @@ import com.comphenix.protocol.utility.MinecraftMethods;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.BlockPosition;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.Converters;
import com.comphenix.protocol.wrappers.ComponentConverter;
import com.comphenix.protocol.wrappers.CustomPacketPayloadWrapper;
import com.comphenix.protocol.wrappers.EnumWrappers;
@@ -101,6 +105,10 @@ import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.common.custom.BrandPayload;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.network.protocol.common.custom.DiscardedPayload;
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket;
import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket.AttributeSnapshot;
@@ -177,8 +185,8 @@ public class PacketContainerTest {
@Test
public void testGetShorts() {
PacketContainer itemData = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE);
this.testPrimitive(itemData.getShorts(), 0, (short) 0, (short) 1);
PacketContainer creativeSlot = new PacketContainer(PacketType.Play.Client.SET_CREATIVE_SLOT);
this.testPrimitive(creativeSlot.getShorts(), 0, (short) 0, (short) 1);
}
@Test
@@ -423,6 +431,64 @@ public class PacketContainerTest {
Assertions.assertNotSame(payload, cloned);
}
@Test
public void testCustomPayloadConcreteSerialization() {
BrandPayload payload = new BrandPayload("Hello World!");
CustomPacketPayloadWrapper wrapper = CustomPacketPayloadWrapper.fromUnknownPayload(payload);
assertEquals(BrandPayload.TYPE.id().toString(), wrapper.getId().getFullKey());
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.wrappedBuffer(wrapper.getPayload()));
try {
assertEquals("Hello World!", buffer.readUtf());
assertEquals(0, buffer.readableBytes());
} finally {
buffer.release();
}
}
@Test
public void testCustomPayloadSerializationFailureReleasesBuffer() {
FailingPayload payload = new FailingPayload();
assertThrows(RuntimeException.class, () -> CustomPacketPayloadWrapper.fromUnknownPayload(payload));
assertEquals(0, payload.buffer.refCnt());
}
@Test
public void testCustomPayloadCreatesDiscardedPayload() {
byte[] data = {0x00, 0x01, 0x05, 0x07};
com.comphenix.protocol.wrappers.MinecraftKey id =
new com.comphenix.protocol.wrappers.MinecraftKey("protocollib", "test");
CustomPacketPayloadWrapper wrapper = new CustomPacketPayloadWrapper(data, id);
Object handle = wrapper.newHandle();
assertInstanceOf(DiscardedPayload.class, handle);
CustomPacketPayloadWrapper converted = CustomPacketPayloadWrapper.fromUnknownPayload(handle);
assertEquals(id, converted.getId());
assertArrayEquals(data, converted.getPayload());
}
private static final class FailingPayload implements CustomPacketPayload {
private static final Type<FailingPayload> TYPE =
new Type<>(Identifier.fromNamespaceAndPath("protocollib", "failing"));
private ByteBuf buffer;
@SuppressWarnings("unused")
private void write(FriendlyByteBuf buffer) {
this.buffer = buffer;
throw new IllegalStateException("expected failure");
}
@Override
public Type<FailingPayload> type() {
return TYPE;
}
}
/*
@Test
public void testUnknownPayloadDeserialize() {
@@ -878,6 +944,32 @@ public class PacketContainerTest {
assertArrayEquals(signature, read.getBytes());
}
@Test
public void testAbsentChatMessageSignature() {
PacketContainer chatPacket = new PacketContainer(PacketType.Play.Client.CHAT);
StructureModifier<WrappedMessageSignature> signatures = chatPacket.getMessageSignatures();
signatures.write(0, new WrappedMessageSignature(new byte[256]));
signatures.write(0, null);
assertNull(signatures.read(0));
assertEquals(Optional.empty(), chatPacket.getModifier().withType(Optional.class).read(0));
}
@Test
public void testMessageSignaturesDoNotShadowOptionals() {
PacketContainer chatPacket = new PacketContainer(PacketType.Play.Server.CHAT);
byte[] signature = new byte[256];
signature[0] = 42;
assertEquals(1, chatPacket.getMessageSignatures().size());
assertEquals(2, chatPacket.getOptionals(Converters.passthrough(Object.class)).size());
assertEquals(1, chatPacket.getMessageSignatures().size());
chatPacket.getMessageSignatures().write(0, new WrappedMessageSignature(signature));
assertArrayEquals(signature, chatPacket.getMessageSignatures().read(0).getBytes());
}
private void assertPacketsEqualAndSerializable(PacketContainer constructed, PacketContainer cloned) {
StructureModifier<Object> firstMod = constructed.getModifier(), secondMod = cloned.getModifier();
assertEquals(firstMod.size(), secondMod.size());
@@ -1,10 +1,16 @@
package com.comphenix.protocol.injector;
import java.math.BigInteger;
import java.util.concurrent.atomic.AtomicBoolean;
import com.comphenix.protocol.BukkitInitialization;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class StructureCacheTests {
@@ -23,4 +29,42 @@ public class StructureCacheTests {
assertTrue(ex.getMessage().contains("Cannot inject already loaded type"));
}
}
@Test
public void testReusesImmutableConstant() {
assertSame(BigInteger.ZERO, StructureCache.newInstance(BigInteger.class));
}
@Test
public void testDoesNotReuseMutableStaticInstance() {
assertNotSame(MutableStaticInstance.INSTANCE, StructureCache.newInstance(MutableStaticInstance.class));
}
@Test
public void testCreatorLookupCanReenterCache() {
assertTrue(StructureCache.canCreateInstance(ReentrantConstructor.class));
assertNull(ReentrantConstructor.failure);
}
private static final class ReentrantConstructor {
private static final AtomicBoolean ENTERED = new AtomicBoolean();
private static volatile RuntimeException failure;
private ReentrantConstructor() {
if (ENTERED.compareAndSet(false, true)) {
try {
StructureCache.canCreateInstance(ReentrantConstructor.class);
} catch (RuntimeException ex) {
failure = ex;
}
}
}
}
private static final class MutableStaticInstance {
public static final MutableStaticInstance INSTANCE = new MutableStaticInstance();
private MutableStaticInstance() {
}
}
}
@@ -10,8 +10,22 @@ import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.injector.netty.WirePacket;
import com.comphenix.protocol.wrappers.CustomPacketPayloadWrapper;
import com.comphenix.protocol.wrappers.MinecraftKey;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.protocol.common.ServerboundKeepAlivePacket;
import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
import net.minecraft.network.protocol.game.ClientboundOpenBookPacket;
import net.minecraft.world.InteractionHand;
import org.bukkit.Material;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -52,6 +66,65 @@ public class WirePacketTest {
assertEquals(failures, new ArrayList<>());
}
@Test
public void testBytesFromClientboundPacket() {
ClientboundOpenBookPacket handle = new ClientboundOpenBookPacket(InteractionHand.OFF_HAND);
PacketContainer packet = new PacketContainer(PacketType.Play.Server.OPEN_BOOK, handle);
assertArrayEquals(new byte[] {1}, WirePacket.bytesFromPacket(packet));
}
@Test
public void testBytesFromServerboundPacket() {
ServerboundKeepAlivePacket handle = new ServerboundKeepAlivePacket(0x0102030405060708L);
PacketContainer packet = new PacketContainer(PacketType.Play.Client.KEEP_ALIVE, handle);
assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, WirePacket.bytesFromPacket(packet));
}
@Test
public void testBytesFromPacketWithItemStack() {
ClientboundContainerSetSlotPacket handle = new ClientboundContainerSetSlotPacket(1, 2, 3,
CraftItemStack.asNMSCopy(new ItemStack(Material.GOLDEN_SHOVEL)));
PacketContainer packet = new PacketContainer(PacketType.Play.Server.SET_SLOT, handle);
// the item stack can only be written to a buffer which knows the registries
RegistryFriendlyByteBuf expected = new RegistryFriendlyByteBuf(Unpooled.buffer(),
CraftRegistry.getMinecraftRegistry());
ClientboundContainerSetSlotPacket.STREAM_CODEC.encode(expected, handle);
assertArrayEquals(ByteBufUtil.getBytes(expected), WirePacket.bytesFromPacket(packet));
}
@Test
public void testFromPacketHandle() {
ClientboundOpenBookPacket handle = new ClientboundOpenBookPacket(InteractionHand.MAIN_HAND);
PacketContainer packet = new PacketContainer(PacketType.Play.Server.OPEN_BOOK, handle);
WirePacket wire = WirePacket.fromPacket(handle);
assertEquals(PacketType.Play.Server.OPEN_BOOK.getCurrentId(), wire.getId());
assertArrayEquals(new byte[] {0}, wire.getBytes());
assertEquals(WirePacket.fromPacket(packet), wire);
}
@Test
public void testBytesFromCustomPayloadPacket() {
byte[] data = {0x00, 0x01, 0x05, 0x07};
PacketContainer packet = new PacketContainer(PacketType.Play.Server.CUSTOM_PAYLOAD);
packet.getCustomPacketPayloads().write(0,
new CustomPacketPayloadWrapper(data, new MinecraftKey("protocollib", "test")));
FriendlyByteBuf expected = new FriendlyByteBuf(Unpooled.buffer());
expected.writeUtf("protocollib:test");
expected.writeBytes(data);
assertArrayEquals(ByteBufUtil.getBytes(expected), WirePacket.bytesFromPacket(packet));
// the packet has to stay writable, it is still sent after it was logged
assertArrayEquals(ByteBufUtil.getBytes(expected), WirePacket.bytesFromPacket(packet));
assertArrayEquals(data, packet.getCustomPacketPayloads().read(0).getPayload());
}
@Test
public void testSerialization() {
int id = 42;
@@ -0,0 +1,150 @@
package com.comphenix.protocol.injector.collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.Converters;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
public class OutboundPacketListenerSetTest {
@BeforeAll
public static void beforeClass() {
BukkitInitialization.initializeAll();
}
@Test
public void testCancelledSubPacketsCancelBundle() {
OutboundPacketListenerSet listenerSet = createListenerSet(event -> event.setCancelled(true));
PacketEvent bundleEvent = createBundleEvent();
listenerSet.invoke(bundleEvent);
assertTrue(bundleEvent.isCancelled());
}
@Test
public void testSubPacketEventReferencesBundle() {
AtomicReference<PacketEvent> subPacketEvent = new AtomicReference<>();
OutboundPacketListenerSet listenerSet = createListenerSet(subPacketEvent::set);
PacketEvent bundleEvent = createBundleEvent();
listenerSet.invoke(bundleEvent);
assertSame(bundleEvent, subPacketEvent.get().getBundle());
}
@Test
public void testUnchangedBundleIsNotCopied() {
OutboundPacketListenerSet listenerSet = createListenerSet(event -> { });
PacketEvent bundleEvent = createBundleEvent();
PacketContainer bundle = bundleEvent.getPacket();
Object packets = getRawPackets(bundle);
listenerSet.invoke(bundleEvent);
assertSame(bundle, bundleEvent.getPacket());
assertSame(packets, getRawPackets(bundleEvent.getPacket()));
}
@Test
public void testCancelledSubPacketDoesNotModifySharedBundle() {
PacketContainer cancelled = new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer retained = new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer sharedBundle = createBundle(cancelled, retained);
PacketEvent modifiedEvent = createBundleEvent(sharedBundle);
PacketEvent sharedEvent = createBundleEvent(PacketContainer.fromPacket(sharedBundle.getHandle()));
Object sharedPackets = getRawPackets(sharedBundle);
OutboundPacketListenerSet listenerSet = createListenerSet(event -> {
if (event.getPacket().getHandle() == cancelled.getHandle()) {
event.setCancelled(true);
}
});
listenerSet.invoke(modifiedEvent);
assertNotSame(sharedBundle.getHandle(), modifiedEvent.getPacket().getHandle());
assertSame(sharedBundle.getHandle(), sharedEvent.getPacket().getHandle());
assertSame(sharedPackets, getRawPackets(sharedEvent.getPacket()));
assertPacketHandles(modifiedEvent.getPacket(), retained);
assertPacketHandles(sharedEvent.getPacket(), cancelled, retained);
}
@Test
public void testReplacedSubPacketDoesNotModifySharedBundle() {
PacketContainer replaced = new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer retained = new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer replacement = new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer sharedBundle = createBundle(replaced, retained);
PacketEvent modifiedEvent = createBundleEvent(sharedBundle);
PacketEvent sharedEvent = createBundleEvent(PacketContainer.fromPacket(sharedBundle.getHandle()));
Object sharedPackets = getRawPackets(sharedBundle);
OutboundPacketListenerSet listenerSet = createListenerSet(event -> {
if (event.getPacket().getHandle() == replaced.getHandle()) {
event.setPacket(replacement);
}
});
listenerSet.invoke(modifiedEvent);
assertNotSame(sharedBundle.getHandle(), modifiedEvent.getPacket().getHandle());
assertSame(sharedBundle.getHandle(), sharedEvent.getPacket().getHandle());
assertSame(sharedPackets, getRawPackets(sharedEvent.getPacket()));
assertPacketHandles(modifiedEvent.getPacket(), replacement, retained);
assertPacketHandles(sharedEvent.getPacket(), replaced, retained);
}
private static OutboundPacketListenerSet createListenerSet(Consumer<PacketEvent> callback) {
OutboundPacketListenerSet listenerSet = new OutboundPacketListenerSet(null, mock(ErrorReporter.class));
listenerSet.addListener(new PacketAdapter(mock(Plugin.class), PacketType.Play.Server.SYSTEM_CHAT) {
@Override
public void onPacketSending(PacketEvent event) {
callback.accept(event);
}
});
return listenerSet;
}
private static PacketEvent createBundleEvent() {
return createBundleEvent(createBundle(new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT)));
}
private static PacketEvent createBundleEvent(PacketContainer bundle) {
return PacketEvent.fromServer(OutboundPacketListenerSetTest.class, bundle, mock(Player.class));
}
private static PacketContainer createBundle(PacketContainer... packets) {
PacketContainer bundle = new PacketContainer(PacketType.Play.Server.BUNDLE);
bundle.getPacketBundles().write(0, List.of(packets));
return bundle;
}
private static Object getRawPackets(PacketContainer bundle) {
return bundle.getModifier().withType(Iterable.class).read(0);
}
private static void assertPacketHandles(PacketContainer bundle, PacketContainer... expected) {
List<PacketContainer> packets = Converters.toList(bundle.getPacketBundles().read(0));
assertEquals(expected.length, packets.size());
for (int index = 0; index < expected.length; index++) {
assertSame(expected[index].getHandle(), packets.get(index).getHandle());
}
}
}
@@ -0,0 +1,170 @@
package com.comphenix.protocol.injector.netty.channel;
import java.lang.reflect.Field;
import java.util.List;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.PacketType.Protocol;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.events.ConnectionSide;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketPostListener;
import com.comphenix.protocol.injector.ListenerManager;
import com.comphenix.protocol.scheduler.ProtocolScheduler;
import io.netty.channel.Channel;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.network.protocol.Packet;
import org.bukkit.Bukkit;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NettyChannelInjectorTest {
@BeforeAll
public static void beforeClass() {
BukkitInitialization.initializeAll();
}
@Test
public void testBundleNeedsMainThreadForSubPacketListener() {
ListenerManager listenerManager = createListenerManager(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer bundle = createBundle(
new PacketContainer(PacketType.Play.Server.KEEP_ALIVE),
new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT));
assertTrue(NettyChannelInjector.hasMainThreadListener(listenerManager, Protocol.PLAY, bundle));
}
@Test
public void testBundleNeedsMainThreadForBundleListener() {
ListenerManager listenerManager = createListenerManager(PacketType.Play.Server.BUNDLE);
PacketContainer bundle = createBundle(new PacketContainer(PacketType.Play.Server.KEEP_ALIVE));
assertTrue(NettyChannelInjector.hasMainThreadListener(listenerManager, Protocol.PLAY, bundle));
}
@Test
public void testBundleWithoutListenedSubPacketsStaysOffMainThread() {
ListenerManager listenerManager = createListenerManager(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer bundle = createBundle(
new PacketContainer(PacketType.Play.Server.KEEP_ALIVE),
new PacketContainer(PacketType.Play.Server.KEEP_ALIVE));
assertFalse(NettyChannelInjector.hasMainThreadListener(listenerManager, Protocol.PLAY, bundle));
}
@Test
public void testSinglePacketOnlyChecksItsOwnType() {
ListenerManager listenerManager = createListenerManager(PacketType.Play.Server.SYSTEM_CHAT);
assertTrue(NettyChannelInjector.hasMainThreadListener(listenerManager, Protocol.PLAY,
new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT)));
assertFalse(NettyChannelInjector.hasMainThreadListener(listenerManager, Protocol.PLAY,
new PacketContainer(PacketType.Play.Server.KEEP_ALIVE)));
}
@Test
public void testRescheduledBundleKeepsNetworkMarker() throws Exception {
ListenerManager listenerManager = createListenerManager(PacketType.Play.Server.SYSTEM_CHAT);
PacketContainer bundle = createBundle(
new PacketContainer(PacketType.Play.Server.KEEP_ALIVE),
new PacketContainer(PacketType.Play.Server.SYSTEM_CHAT));
SendAction action = new SendAction((Packet<?>) bundle.getHandle());
NetworkMarker marker = new NetworkMarker(ConnectionSide.SERVER_SIDE, PacketType.Play.Server.BUNDLE);
PacketPostListener postListener = mock(PacketPostListener.class);
marker.addPostListener(postListener);
// the real Connection can't be loaded in tests, so the injector gets a stand-in and a mocked invoker
NettyChannelInjector injector;
try (MockedConstruction<PacketListenerInvoker> ignored = mockConstruction(PacketListenerInvoker.class)) {
injector = spy(new NettyChannelInjector(null, new FakeNetworkManager(), new EmbeddedChannel(),
listenerManager, mock(InjectionFactory.class), mock(ErrorReporter.class)));
}
doReturn(Protocol.PLAY).when(injector).getCurrentProtocol(PacketType.Sender.SERVER);
Field injected = NettyChannelInjector.class.getDeclaredField("injected");
injected.setAccessible(true);
injected.setBoolean(injector, true);
ProtocolScheduler scheduler = mock(ProtocolScheduler.class);
try (MockedStatic<ProtocolLibrary> protocolLibrary = mockStatic(ProtocolLibrary.class, CALLS_REAL_METHODS)) {
protocolLibrary.when(ProtocolLibrary::getScheduler).thenReturn(scheduler);
// a plugin sends the bundle with a marker off the main thread
when(Bukkit.getServer().isPrimaryThread()).thenReturn(false);
injector.sendClientboundPacket(action.packet, marker, true);
assertNull(injector.processOutbound(action));
ArgumentCaptor<Runnable> rescheduled = ArgumentCaptor.forClass(Runnable.class);
verify(scheduler).runTask(rescheduled.capture());
verify(listenerManager, never()).invokeOutboundPacketListeners(any());
// the rescheduled send runs on the main thread and is written again
when(Bukkit.getServer().isPrimaryThread()).thenReturn(true);
rescheduled.getValue().run();
Runnable sent = injector.processOutbound(action);
assertNotNull(sent);
sent.run();
} finally {
when(Bukkit.getServer().isPrimaryThread()).thenReturn(true);
}
assertTrue(action.ran);
verify(postListener).onPostEvent(any(PacketEvent.class));
}
private static final class FakeNetworkManager {
private Channel channel;
}
private static final class SendAction implements Runnable {
private final Packet<?> packet;
private boolean ran;
private SendAction(Packet<?> packet) {
this.packet = packet;
}
@Override
public void run() {
this.ran = true;
}
}
private static ListenerManager createListenerManager(PacketType mainThreadPacketType) {
ListenerManager listenerManager = mock(ListenerManager.class);
when(listenerManager.hasMainThreadListener(mainThreadPacketType)).thenReturn(true);
return listenerManager;
}
private static PacketContainer createBundle(PacketContainer... packets) {
PacketContainer bundle = new PacketContainer(PacketType.Play.Server.BUNDLE);
bundle.getPacketBundles().write(0, List.of(packets));
return bundle;
}
}
@@ -0,0 +1,78 @@
package com.comphenix.protocol.injector.netty.manager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.embedded.EmbeddedChannel;
class NetworkManagerInjectorTest {
@Test
void addsInjectionHandlerBeforeNetworkManager() {
EmbeddedChannel channel = new EmbeddedChannel();
channel.pipeline().addLast("before", new ChannelInboundHandlerAdapter());
channel.pipeline().addLast("network_manager", new TestNetworkManager());
channel.pipeline().addLast("after", new ChannelInboundHandlerAdapter());
NetworkManagerInjector.addInjectionHandler(
channel,
TestNetworkManager.class,
new ChannelInboundHandlerAdapter());
assertEquals(
channel.pipeline().names().indexOf("network_manager") - 1,
channel.pipeline().names().indexOf("protocol_lib_inbound_inject"));
}
@Test
void injectsAfterNetworkManagerFinishesChannelActivation() {
AtomicBoolean networkManagerInitialized = new AtomicBoolean();
AtomicBoolean observedInitializedNetworkManager = new AtomicBoolean();
EmbeddedChannel channel = new EmbeddedChannel();
channel.pipeline().addLast("network_manager", new TestNetworkManager() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.fireChannelActive();
networkManagerInitialized.set(true);
}
});
NetworkManagerInjector.addInjectionHandler(
channel,
TestNetworkManager.class,
new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.fireChannelActive();
observedInitializedNetworkManager.set(networkManagerInitialized.get());
}
});
channel.pipeline().fireChannelActive();
assertTrue(observedInitializedNetworkManager.get());
}
@Test
void addsInjectionHandlerLastWhenNetworkManagerIsMissing() {
EmbeddedChannel channel = new EmbeddedChannel();
channel.pipeline().addLast("existing", new ChannelInboundHandlerAdapter());
NetworkManagerInjector.addInjectionHandler(
channel,
TestNetworkManager.class,
new ChannelInboundHandlerAdapter());
assertEquals(
channel.pipeline().names().indexOf("existing") + 1,
channel.pipeline().names().indexOf("protocol_lib_inbound_inject"));
}
private static class TestNetworkManager extends ChannelInboundHandlerAdapter {
}
}
@@ -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<Channel> 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));
}
}
@@ -5,6 +5,7 @@ import java.util.List;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.utility.MinecraftVersion;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -29,7 +30,8 @@ public class PacketRegistryTests {
public void testAllPacketsRegistered() {
List<PacketType> missing = new ArrayList<>();
for (PacketType type : PacketType.values()) {
if (type.isDeprecated()) {
if (type.isDeprecated()
|| type.getCurrentVersion().compareTo(MinecraftVersion.getCurrentVersion()) > 0) {
continue;
}
if (!PacketRegistry.tryGetPacketClass(type).isPresent()) {
@@ -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.junit.jupiter.api.Test;
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 class TemporaryPlayerFactoryTest {
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());
}
}
@@ -131,7 +131,7 @@ public class MinecraftReflectionTest {
// The NMS handle for CraftItemStack is null with Material.AIR, make sure it is handled correctly
assertNotNull(
MinecraftReflection.getMinecraftItemStack(CraftItemStack.asCraftCopy(new ItemStack(Material.AIR))));
MinecraftReflection.getMinecraftItemStack(CraftItemStack.getCraftStack(new ItemStack(Material.AIR))));
}
@Test
@@ -10,6 +10,8 @@ import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
@@ -19,6 +21,7 @@ import org.junit.jupiter.api.Test;
import static com.comphenix.protocol.utility.TestUtils.assertItemsEqual;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class StreamSerializerTest {
@@ -86,4 +89,23 @@ public class StreamSerializerTest {
assertItemsEqual(initial, deserialized);
}
@Test
public void testGetBytesAndReleasePreservesReaderIndex() {
StreamSerializer serializer = new StreamSerializer();
ByteBuf[] buffers = {Unpooled.buffer(16), Unpooled.directBuffer(16)};
for (ByteBuf buffer : buffers) {
buffer.writeBytes(new byte[] {0, 1, 2, 3, 4});
buffer.readerIndex(2);
buffer.retain();
try {
assertArrayEquals(new byte[] {2, 3, 4}, serializer.getBytesAndRelease(buffer));
assertEquals(2, buffer.readerIndex());
} finally {
buffer.release();
}
}
}
}
@@ -45,22 +45,18 @@ public class AutoWrapperTest {
display.announceChat = false;
display.showToast = true;
display.hidden = true;
display.x = 5f;
display.y = 67f;
DisplayInfo nms = (DisplayInfo) displayWrapper().unwrap(display);
assertTrue(nms.shouldShowToast());
assertTrue(nms.isHidden());
assertFalse(nms.shouldAnnounceChat());
assertTrue(nms.getBackground().isPresent());
assertEquals("test", nms.getBackground().get().id().getPath());
validateRawText(nms.getTitle(), "Test123");
validateRawText(nms.getDescription(), "Test567");
assertSame(AdvancementType.CHALLENGE, nms.getType());
assertSame(getItemStackTemplateConverter().getSpecific(nms.getIcon()).getType(), Material.GOLD_INGOT);
assertEquals(5f, nms.getX(), 0f);
assertEquals(67f, nms.getY(), 0f);
assertTrue(nms.showToast());
assertTrue(nms.hidden());
assertFalse(nms.announceToChat());
assertTrue(nms.background().isPresent());
assertEquals("test", nms.background().get().id().getPath());
validateRawText(nms.title(), "Test123");
validateRawText(nms.description(), "Test567");
assertSame(AdvancementType.CHALLENGE, nms.type());
assertSame(getItemStackTemplateConverter().getSpecific(nms.icon()).getType(), Material.GOLD_INGOT);
}
@Test
@@ -75,7 +71,6 @@ public class AutoWrapperTest {
false,
true
);
display.setLocation(5f, 67f);
WrappedAdvancementDisplay wrapped = displayWrapper().wrap(display);
@@ -88,16 +83,14 @@ public class AutoWrapperTest {
assertEquals("\"Test567\"", wrapped.description.getJson());
assertSame(WrappedFrameType.CHALLENGE, wrapped.frameType);
assertSame(Material.ENDER_EYE, wrapped.item.getType());
assertEquals(5f, wrapped.x, 0f);
assertEquals(67f, wrapped.y, 0f);
}
private AutoWrapper<WrappedAdvancementDisplay> displayWrapper() {
return AutoWrapper
.wrap(WrappedAdvancementDisplay.class, "advancements.AdvancementDisplay", "advancements.DisplayInfo")
.field(0, BukkitConverters.getWrappedChatComponentConverter())
.field(0, getItemStackTemplateConverter())
.field(1, BukkitConverters.getWrappedChatComponentConverter())
.field(2, getItemStackTemplateConverter())
.field(2, BukkitConverters.getWrappedChatComponentConverter())
.field(3, Converters.optional(Converters.passthrough(ClientAsset.class)))
.field(4, EnumWrappers.getGenericConverter(
getMinecraftClass("advancements.AdvancementType", "advancements.AdvancementFrameType", "advancements.FrameType"),
@@ -140,15 +133,13 @@ public class AutoWrapperTest {
public static final class WrappedAdvancementDisplay {
public ItemStack item;
public WrappedChatComponent title;
public WrappedChatComponent description;
public ItemStack item;
public Optional<ClientAsset> background;
public WrappedFrameType frameType;
public boolean showToast;
public boolean announceChat;
public boolean hidden;
public float x;
public float y;
}
}
@@ -13,6 +13,7 @@ import org.apache.commons.lang.builder.EqualsBuilder;
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 class BukkitConvertersTest {
assertEquals(wrapped.right(), nmsEither.right());
}
@Test
public void testDamageType() {
EquivalentConverter<DamageType> 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() {
@@ -58,7 +58,7 @@ public class WrappedBlockDataTest {
@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);
@@ -67,6 +67,20 @@ public class WrappedGameProfileTest {
assertEquals(property.getSignature(), signature);
}
@Test
void testMixedAccessorNames() {
MixedGameProfile profile = new MixedGameProfile();
assertEquals(profile.getId(), WrappedGameProfile.findMethodAccessor(
MixedGameProfile.class, "id", "getId").invoke(profile));
assertEquals(profile.name(), WrappedGameProfile.findMethodAccessor(
MixedGameProfile.class, "name", "getName").invoke(profile));
assertSame(profile.properties(), WrappedGameProfile.findMethodAccessor(
MixedGameProfile.class, "properties", "getProperties").invoke(profile));
assertEquals(profile.isComplete(), WrappedGameProfile.findMethodAccessor(
MixedGameProfile.class, "complete", "isComplete").invoke(profile));
}
@Test
void testAddProperties() {
String name = "test";
@@ -85,4 +99,28 @@ public class WrappedGameProfileTest {
assertEquals(property.value(), value);
assertEquals(property.signature(), signature);
}
private static final class MixedGameProfile {
private final UUID id = UUID.randomUUID();
public UUID getId() {
return this.id;
}
public String name() {
return "ProtocolLib";
}
public String getName() {
return "Legacy";
}
public PropertyMap properties() {
return PropertyMap.EMPTY;
}
public boolean isComplete() {
return true;
}
}
}
@@ -37,8 +37,7 @@ public class WrappedParticleTest {
assertEquals(before.getData(), after.getData());
}
// @Test
// TODO: changed to ItemStackTemplate
@Test
public void testItemStacks() {
PacketContainer packet = new PacketContainer(PacketType.Play.Server.WORLD_PARTICLES);
WrappedParticle before = WrappedParticle.create(Particle.ITEM, new ItemStack(Material.FLINT_AND_STEEL));
@@ -41,31 +41,11 @@ public class WrappedPositionMoveRotationTest {
}
@Test
public void testEntityPositionSyncPacket() {
public void testEntityPositionSyncPacketHasNoPositionMoveRotation() {
PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_POSITION_SYNC);
Vector position = new Vector(10.0, 70.0, -5.0);
Vector delta = new Vector(0.0, 0.0, 0.0);
float yRot = 90.0f;
float xRot = 0.0f;
packet.getPositionMoveRotation().write(0,
WrappedPositionMoveRotation.create(position, delta, yRot, xRot));
WrappedPositionMoveRotation result = packet.getPositionMoveRotation().read(0);
assertNotNull(result);
assertEquals(position.getX(), result.getPosition().getX(), 1e-6);
assertEquals(position.getY(), result.getPosition().getY(), 1e-6);
assertEquals(position.getZ(), result.getPosition().getZ(), 1e-6);
assertEquals(delta.getX(), result.getDeltaMovement().getX(), 1e-6);
assertEquals(delta.getY(), result.getDeltaMovement().getY(), 1e-6);
assertEquals(delta.getZ(), result.getDeltaMovement().getZ(), 1e-6);
assertEquals(yRot, result.getYRot(), 1e-6f);
assertEquals(xRot, result.getXRot(), 1e-6f);
assertEquals(0, packet.getPositionMoveRotation().size());
assertNull(packet.getPositionMoveRotation().readSafely(0));
}
@Test
@@ -0,0 +1,100 @@
package com.comphenix.protocol.wrappers;
import java.util.List;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket;
import net.minecraft.world.entity.PositionPath;
import net.minecraft.world.phys.Vec3;
import org.bukkit.util.Vector;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class WrappedPositionPathTest {
@BeforeAll
public static void beforeClass() {
BukkitInitialization.initializeAll();
}
@Test
public void testLinear() {
WrappedPositionPath path = WrappedPositionPath.linear(new Vector(1.5, 64.0, -3.5));
assertFalse(path.isStepped());
assertEquals(new Vector(1.5, 64.0, -3.5), path.getEndPosition());
assertThrows(IllegalStateException.class, path::getSteps);
assertEquals(PositionPath.of(new Vec3(1.5, 64.0, -3.5)), path.getHandle());
}
@Test
public void testStepped() {
List<WrappedPositionPath.Step> steps = List.of(
new WrappedPositionPath.Step(new Vector(1, 2, 3), 1),
new WrappedPositionPath.Step(new Vector(4, 5, 6), 3));
WrappedPositionPath path = WrappedPositionPath.stepped(steps);
assertTrue(path.isStepped());
assertEquals(steps, path.getSteps());
assertEquals(new Vector(4, 5, 6), path.getEndPosition());
}
@Test
public void testSteppedRequiresSteps() {
assertThrows(IllegalArgumentException.class, () -> WrappedPositionPath.stepped(List.of()));
}
@Test
public void testStepIsImmutable() {
Vector position = new Vector(1, 2, 3);
WrappedPositionPath.Step step = new WrappedPositionPath.Step(position, 1);
position.setX(10);
step.getPosition().setY(20);
assertEquals(new Vector(1, 2, 3), step.getPosition());
}
@Test
public void testEquality() {
WrappedPositionPath first = WrappedPositionPath.linear(new Vector(1, 2, 3));
WrappedPositionPath second = WrappedPositionPath.fromHandle(PositionPath.of(new Vec3(1, 2, 3)));
assertEquals(first, second);
assertEquals(first.hashCode(), second.hashCode());
assertNotEquals(first, WrappedPositionPath.linear(new Vector(1, 2, 4)));
}
@Test
public void testEntityPositionSyncPacketRoundTrip() {
PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_POSITION_SYNC);
packet.getIntegers().write(0, 42);
packet.getFloat().write(0, 90.0f).write(1, -15.0f);
packet.getBooleans().write(0, true);
WrappedPositionPath path = WrappedPositionPath.stepped(List.of(
new WrappedPositionPath.Step(new Vector(10.0, 70.0, -5.0), 1),
new WrappedPositionPath.Step(new Vector(10.5, 70.0, -5.5), 2)));
packet.getPositionPaths().write(0, path);
assertEquals(path, packet.getPositionPaths().read(0));
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
ClientboundEntityPositionSyncPacket.STREAM_CODEC.encode(buf, (ClientboundEntityPositionSyncPacket) packet.getHandle());
ClientboundEntityPositionSyncPacket decoded = ClientboundEntityPositionSyncPacket.STREAM_CODEC.decode(buf);
PacketContainer roundTrip = PacketContainer.fromPacket(decoded);
assertEquals(path, roundTrip.getPositionPaths().read(0));
assertEquals(42, roundTrip.getIntegers().read(0));
assertEquals(90.0f, roundTrip.getFloat().read(0), 1e-6f);
assertEquals(-15.0f, roundTrip.getFloat().read(1), 1e-6f);
assertTrue(roundTrip.getBooleans().read(0));
}
}
@@ -0,0 +1,42 @@
package com.comphenix.protocol.wrappers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Map;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.network.protocol.game.ClientboundAwardStatsPacket;
import net.minecraft.resources.Identifier;
import net.minecraft.stats.Stat;
import net.minecraft.stats.Stats;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class WrappedStatisticTest {
@BeforeAll
static void initializeBukkit() {
BukkitInitialization.initializeAll();
}
@Test
void testStatisticMap() {
Stat<Identifier> statistic = Stats.CUSTOM.get(Stats.JUMP);
Object2IntMap<Stat<?>> statistics = new Object2IntOpenHashMap<>();
statistics.put(statistic, 12);
PacketContainer packet = new PacketContainer(
PacketType.Play.Server.STATISTIC,
new ClientboundAwardStatsPacket(statistics));
Map<WrappedStatistic, Integer> wrappedStatistics = packet.getStatisticMaps().read(0);
WrappedStatistic wrappedStatistic = wrappedStatistics.keySet().iterator().next();
assertEquals(statistic.getName(), wrappedStatistic.getName());
assertEquals(12, wrappedStatistics.get(wrappedStatistic));
}
}
@@ -50,6 +50,6 @@ public class WrappedStreamCodecTests {
codec.encode(buf, packet);
ClientboundOpenBookPacket roundTrip = (ClientboundOpenBookPacket) codec.decode(buf);
assertEquals(InteractionHand.OFF_HAND, roundTrip.getHand());
assertEquals(InteractionHand.OFF_HAND, roundTrip.hand());
}
}
@@ -0,0 +1,111 @@
package com.comphenix.protocol.wrappers;
import java.util.List;
import com.comphenix.protocol.BukkitInitialization;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket;
import net.minecraft.network.protocol.game.VecDelta;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class WrappedVecDeltaTest {
@BeforeAll
public static void beforeClass() {
BukkitInitialization.initializeAll();
}
@Test
public void testLinear() {
WrappedVecDelta delta = WrappedVecDelta.linear((short) 1, (short) -2, (short) 4096);
assertFalse(delta.isStepped());
assertEquals(1, delta.getDeltaX());
assertEquals(-2, delta.getDeltaY());
assertEquals(4096, delta.getDeltaZ());
assertThrows(IllegalStateException.class, delta::getSteps);
assertEquals(new VecDelta.Linear((short) 1, (short) -2, (short) 4096), delta.getHandle());
}
@Test
public void testStepped() {
List<WrappedVecDelta.Step> steps = List.of(
new WrappedVecDelta.Step((short) 10, (short) 0, (short) -10, 1),
new WrappedVecDelta.Step((short) 5, (short) 6, (short) 7, 3));
WrappedVecDelta delta = WrappedVecDelta.stepped(steps);
assertTrue(delta.isStepped());
assertEquals(steps, delta.getSteps());
assertThrows(IllegalStateException.class, delta::getDeltaX);
VecDelta handle = (VecDelta) delta.getHandle();
assertEquals(2, handle.stepCount());
assertTrue(handle.hasDeltaX());
}
@Test
public void testSteppedRequiresSteps() {
assertThrows(IllegalArgumentException.class, () -> WrappedVecDelta.stepped(List.of()));
}
@Test
public void testEquality() {
WrappedVecDelta first = WrappedVecDelta.linear((short) 1, (short) 2, (short) 3);
WrappedVecDelta second = WrappedVecDelta.fromHandle(new VecDelta.Linear((short) 1, (short) 2, (short) 3));
assertEquals(first, second);
assertEquals(first.hashCode(), second.hashCode());
assertNotEquals(first, WrappedVecDelta.linear((short) 1, (short) 2, (short) 4));
}
@Test
public void testRelativeMovePacket() {
PacketContainer packet = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE);
packet.getIntegers().write(0, 123);
packet.getBooleans().write(0, true);
WrappedVecDelta delta = WrappedVecDelta.linear((short) 100, (short) 200, (short) -300);
packet.getVecDeltas().write(0, delta);
assertEquals(1, packet.getVecDeltas().size());
assertEquals(delta, packet.getVecDeltas().read(0));
assertEquals(123, packet.getIntegers().read(0));
assertTrue(packet.getBooleans().read(0));
}
@Test
public void testSteppedRelativeMovePacketRoundTrip() {
PacketContainer packet = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE);
packet.getIntegers().write(0, 7);
packet.getBooleans().write(0, true);
WrappedVecDelta delta = WrappedVecDelta.stepped(List.of(
new WrappedVecDelta.Step((short) 1, (short) 2, (short) 3, 1),
new WrappedVecDelta.Step((short) -4, (short) 5, (short) -6, 2)));
packet.getVecDeltas().write(0, delta);
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
ClientboundMoveEntityPacket.Pos.STREAM_CODEC.encode(buf, (ClientboundMoveEntityPacket.Pos) packet.getHandle());
ClientboundMoveEntityPacket.Pos decoded = ClientboundMoveEntityPacket.Pos.STREAM_CODEC.decode(buf);
PacketContainer roundTrip = PacketContainer.fromPacket(decoded);
assertEquals(delta.getSteps(), roundTrip.getVecDeltas().read(0).getSteps());
assertEquals(7, roundTrip.getIntegers().read(0));
assertTrue(roundTrip.getBooleans().read(0));
}
@Test
public void testNoShortsOnRelativeMovePacket() {
PacketContainer packet = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE_LOOK);
assertEquals(0, packet.getShorts().size());
assertEquals(1, packet.getVecDeltas().size());
}
}